Laravel 的 Facade 是個很神奇的設計。使用的時候是靜態呼叫,但實質上是對某個實例呼叫。也因這個特性,所以有辦法做測試替身(Test Double)。
Facade 利用了 PHP 的幾個特性並搭配 Laravel 元件實作出來的,首先是 Magic Method,不一定要為類別定義方法,就能呼叫;再來是靜態方法可以被覆寫,讓靜態類別可以被繼承的;最後它跟 Container 是互相搭配的好夥伴。
剛有提到靜態方法可以覆寫,那個方法是 getFacadeAccessor()
:
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
這個方法預設會丟例外。意指如果子類沒有覆寫,將無法使用這個功能。
我們來看一個基本的範例 Request:
class Request extends Facade
{
protected static function getFacadeAccessor()
{
return 'request';
}
}
它繼承的實作是回傳了 request
字串。而當我們使用這個 Facade,如 Request::ip()
時, __callStatic()
即會被觸發:
public static function __callStatic($method, $args)
{
// 取得實例
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
// 對實例呼叫方法,以 Request::ip() 為例,即會呼叫 $instance->ip()
return $instance->$method(...$args);
}
再來就是看 getFacadeRoot()
是如何解析實例的:
public static function getFacadeRoot()
{
// 取得 accessor 並解析 Facade 實例
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
protected static function resolveFacadeInstance($name)
{
// 如果已經是實例的話就回傳
if (is_object($name)) {
return $name;
}
// 如果已經被解析過就回傳
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
// 使用 static::$app 解析
return static::$resolvedInstance[$name] = static::$app[$name];
}
這個 $app
正是剛剛提到要搭配的 Container,使用 setFacadeApplication()
即可設定。只要有 Container 就能建構所有實例。至於究竟什麼地方被呼叫到了?在分析 bootstrap 流程時,有一小段細節並沒有提到:RegisterFacades 的實作:
public function bootstrap(Application $app)
{
// 清除所有已被解析過的實例,這是用在 feature 測試上
Facade::clearResolvedInstances();
// 就是這裡
Facade::setFacadeApplication($app);
// 設定 alias loader
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
這下謎底全揭曉了,因此我們可以知道,下面這些程式碼得到的結果都是一樣的:
Request::ip();
request()->ip();
app()->make('request')->ip();
一樣都是從 Container 取得 request
並呼叫對應的方法與回傳。
但需注意是,預設的 Facade 初始化的時機點在哪,像 Request 是在進 route middleware 之前才初始化的,global middleware 或在更之前,如設定 service provider 時,Container 還沒有存放實例,這時 Facade 的 magic 就會失效了。
另一個類別 AliasLoader 留到明天繼續討論。