LaravelのFacadeってなんですか?なぜ動くんですか?教えてもらっていいですか?
はじめに
近日公開予定の記事を書くにあたって見ていた英文記事にて、Facade について一石を投じていたものがありました。
そこで自分自身の復習と、近日公開予定の記事の補足をかねて Facade がなぜ動いているのかについて書いて行こうと思います。
ただ、以前公開したこちらの Middleware 記事に比べると初心者向けな内容です。
あとこの記事では Facade の使い方そのものについては説明しません。
(使い方は調べればいくらでも出てくるので)
記事タイトル
若干煽りっぽくなっちゃってますが、こちらの作品名のオマージュです(笑)
『終末なにしてますか? 忙しいですか? 救ってもらっていいですか?』
(こうやって毎記事、何かしらの作品名のオマージュにすれば推し活できるんじゃないだろうか…🤔)
ところで、Facade ってなんでしょう?
大体の人の答えはこうではないでしょうか。
「こうやってなんかしらのサービスを実行してくれる便利なやつでしょ?」
DB::select('select 1');
これは、Laraveler であれば絶対に見たことがある、DB クラス を使った select 処理なわけで、答えとして間違ってはいないのですがずいぶん抽象的です。
私が知りたいのは、DB::select('select 1')の裏側でどんな処理が走っていて、Facade がどういった仕事をしているのか?という本質的なところなのです。
そもそも、これがなぜ動くのか?
DB::select('select 1');
ちゃんと疑問に感じていますか?
だって DB クラスを見てみると、
class DB extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'db';
}
}
DB クラスには static の getFacadeAccessor しかなく、select なんていうメソッドはありません。
…とここでなにやら声が聞こえた気がします。
「Facade クラスを継承してるんだから、そこに書いてあるんでしょ?」
では、vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php クラスの中で「select」で検索してみましょう。
そりゃそうですよね。
Facade クラスが具象メソッドとして select を持っているのであれば、Log::debug() とか Route::get() もあるので、debug とか get とかも全部持ってないとおかしいですよね?
しかし逆説的に言えば、「static コールで存在しないメソッドを呼んでいる」という事実を観測することが重要なのです。
そしてこの疑問を解決するためには、Laravel の知識ではなく、PHP の知識が必要です。
__callStatic
では、さっき観測した「static コールで存在しないメソッドを呼んでいる」という事実をぐぐってみます。
どうやら存在しない staic メソッドを呼んだ際に実行されるマジックメソッドが存在するようです。
そう、まさにこの __callStatic マジックメソッドが Facade の心臓になっているのです。
__callStatic は二つの変数を引数にとります。
- 第一引数 = static コールしようとしたメソッド名
- 第二引数 = static コールしようとしたメソッドへ渡されるはずの引数配列
では、あらためて vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php の中に__callStatic というメソッドがあるかを見てみます。
/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
はい、見つかりましたね。
DB::select('select 1')を実行した今回の場合、$method = select, $args = ['select 1']となっています。
そして、Facade クラスの__callStatic マジックメソッド内部からさらに static::getFacadeRoot()を実行することでインスタンスを取得していますね。
その結果、インスタンスが取得できなければ例外をスローする。取得できれば、当初 static コールしたメソッド名と引数を使って処理を実行し、実行結果を return するようです。
では次は、static::getFacadeRoot()の中を見てみます。
static::getFacadeRoot
/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/**
* Get the registered name of the component.
*
* @return string
*
* @throws \RuntimeException
*/
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
getFacadeRoot()を見ると処理は一行しかありません。
ここでまず実行されるのは、static::getFacadeAccessor()ということで、一見そのすぐ下に定義されているそれが呼ばれそうです。
しかしそうだった場合、ここで例外を吐いて処理が終わってしまいます。
そう、呼ばれているのはそれではありません。
ここで、そもそも実行しようとしていた処理を思い出してください。
DB::select('select 1');
vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php の getFacadeAccessor()は Facade クラスのメンバメソッドであって、実行しようとしている DB クラスのメンバメソッドではありません。
つまり、ここで実行されるのはサブクラス(DB クラス)の getFacadeAccessor()となります。
一応補足しておくと、スーパークラス(Facade クラス)の getFacadeAccessor()が呼ばれるのは、サブクラス(DB クラス)の getFacadeAccessor()が定義されていない場合です。(この場合に例外を吐いて処理を終了します)
class DB extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'db';
}
}
これにより取得されるのは、'db'という文字列です。
'db'という文字列を取得したうえで、この値を次の resolveFacadeInstance の引数として渡し、resolveFacadeInstance の処理結果が最終的に return されます。
この return されたインスタンスに定義されたメソッド=select が最終的に__callStatic で実行され、その結果が return されます。
では次は resolveFacadeInstance でどのようにしてインスタンスを取得しているのか確認しましょう。
resolveFacadeInstance
/**
* Resolve the facade root instance from the container.
*
* @param string $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
if (static::$cached) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
return static::$app[$name];
}
}
はい、やっていることはとても単純ですね。
一応補足しておくと、$app['文字列']というのは$app->make('文字列')のシンタックスシュガーです。
つまり、resolveFacadeInstance はサービスコンテナからインスタンスを取得しているだけです。
ちなみにこのコードからわかるように、Facade は毎回$app->make('文字列')しているわけではなく、解決済みのインスタンスについては$resolvedInstance というキャッシュからインスタンスを取得します。
(ただし、例えばサブクラスから$cached を false に書き換えることで、キャッシュさせないことも可能)
これは重要な事実を表しています。それはFacade を使ったインスタンスは実質シングルトンであるということです。
実質というのは、$cached を false に書き換えることもできるためです。
例えば以下の例だと、DB クラスについては Fadade 呼び出しの際にキャッシュされません。
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
self::$cached = false;
return 'db';
}
他にも Facade の動きについて説明している記事は見かけますが、この実質シングルトンという重要な事実まで書いている記事はあまり見かけない気がします(単に自分の目に留まってないだけかもしれないですが…)
この事実を把握せず Facade を使っている場合、インスタンスを上書きしたいところが意図に反して上書きされない、というバグを招く可能性があります。
答え
Facade は基本的には DI のためのコードのシンタックスシュガー。
ただし実質シングルトンとしてインスタンスを取得し、処理を実行し、結果を返すもの。
//AとBの処理に基本的には違いはない
//A
DB::select('select 1');
//B
$app = app()
$app->make('db')->select('select 1');
おわりに
今回ですが、当初は Facade の動きについて解説するつもりはありませんでした。
理由は、既に多くの人が解説しているからです。しかし、この記事で最も伝えたかった「Facade は実質シングルトン」という事実に触れている記事はあまり見かけられず、そのことを伝えたいがために書いたと言っても過言ではありません。
そして Facade の動きについて把握すると、次の疑問として「わざわざ$app = app()の 1 行(厳密には use Illuminate\Foundation\Application;も書かれているはずなので 2 行)を省略するために Facade クラスを継承したサブクラスとエイリアスを登録する意味ってあるの?」という話になってくるわけです。
そして、「DB::select('select 1');と$app->make('db')->select('select 1');はどう違うんだろうか?」という疑問に発展していくわけです。
これらの疑問について一石を投じてる人を見かけたので、次回の記事ではここで確認した「Facade がなぜ動くのか?」を踏まえて Facade の問題点について見てみたいと思います。
メンバー募集中!
サーバーサイド Kotlin コミュニティを作りました!
Kotlin ユーザーはぜひご参加ください!!
また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。
よろしければ Conpass からメンバー登録よろしくお願いいたします。
Discussion