Magic for Artisan 是一個系列的文章,在這系列的文章中,我們將探討 Laravel 是如何使用到各種 PHP 的動態 (Dynamic) 特性。雖然這些動態的部分被許多人所詬病,但筆者作為 Laravel 的粉絲,對於這些功能抱持著正面的態度,希望透過這一系列的文章能讓大家更瞭解 Laravel 的運作原理。
初次接觸 Laravel 的人一定有遇過這樣的狀況:手冊上叫你呼叫 Request::input('foo');
,但是我們打開 \Illuminate\Support\Facades\Request
,卻發現這個 Class 上根本沒實作 input
方法!
許多人想,既然 Request
Class 上沒有,那我們看看他的上層 (Parent) Class,應該就可以找到了吧!?Request
的上層方法是 Illuminate\Support\Facades\Facade
。但打開該 Class 後會發現裡面定義的是一堆跟 Request
毫無關聯的方法,也沒看到 input
方法。
究竟這是怎麼一回事呢?今天,就讓我們來談談 Facade 的運作原理。
Facade
根據《譯典通英漢雙向字典》,Facade (/fǝˈsɑːd/
) 的字面意思是指「建築物的正面、前面」,或是「表面、 外觀」。從這個字的字義上來看其實也可以看出端倪。Facade 並不真的是我們要使用的那個 Class,而只是該 Class 的「表面」、「門面」。那麼究竟我們要使用的那個 Class 到底在哪裡呢?
在官方說明文件的 Facade 一節中其實有對 Facade 的功能作出詳細說明。不過本文主要想針對剛入門 PHP 或 PHP 框架的朋友介紹這些重點概念,因此在接下來的部分筆者會說明一些 Facade 相關的重點概念。不過,不論讀者的程度如何,都可以考慮先試著閱讀一次官方文件中關於 Facade 的部分,即使看不懂也可以先留下一些印象,然後再來繼續閱讀本文。
Facade 的組成
讓我們先以剛才提到的 Request
Facade 為例子,讓我們來看一下程式碼:
<?php
namespace Illuminate\Support\Facades;
class Request extends Facade
{
protected static function getFacadeAccessor()
{
return 'request';
}
}
我將其中註解的部分刪除,讓我們來專注在程式碼的部分。
在這個 Class 中其實沒有真正實作到我們會在 Request
上呼叫的各種方法,比如 Request::input()
或 Request::file()
等。該 Class 中只有實作了一個 getFacadeAccessor()
。有些讀者看到這裡可能已經猜到,Laravel 會根據 getFacadeAccessor()
來找到「真正的 Request」。而這個 Facade 不過就只是個替身。
那麼究竟 Laravel 是怎麼從 'request'
這個字串來找出「真正的 Request」的呢?讓我們來繼續看一下去。
我們打開 Request
Facade 所繼承的 Facade
Class。由於這個檔案比較長,筆者將分段說明。有興趣的讀者可以先在 GitHub 上閱讀一邊 Facade Class 的原始碼。
在 Facade 類別中,包含了許多跟 Mock 相關的程式碼。但我們在此先不討論這些方法,以後再專門寫一篇文章來介紹 Mock。
我們可以看到 Facade 中有一些 getFacadeRoot
等名稱跟 Facade 有關係的方法。但我們可以先注意到該檔案中最後一個定義的方法:__callStatic
。
/** // [tl! reindex(321)] [tl! collapse:start]
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/ // [tl! collapse:end]
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);
}
__callStatic
對於 PHP 特性還不熟悉的朋友可能沒看過這個方法,但讀者應該知道,在 PHP 中保留了許多以兩個底線 (__
) 開頭的變數、方法,這些保留的特殊變數、方法都有特殊的功能,稱為「魔法變數 (Magic Variable)」與「魔法方法 (Magic Method)」。而 __callStatic
方法就是屬於這種有特殊功能的魔法方法。
有興趣的讀者可以先打開 PHP 手冊中關於 __callStatic
的頁面瞭解更多資訊。
總之,__callStatic
這個方法的功能是讓我們能以「靜態」方式呼叫不存在的方法。
舉例來說,我們假設有下列 Class:
<?php
class Foo {
public function bar(): string
{
return 'baz';
}
}
若我們要呼叫 Foo::bar()
,PHP 就會去找出 Foo
類別中的 bar
方法來呼叫。而若我們呼叫一個不存在的方法,則會回傳錯誤。
不過,如果我們在該類別上定義 __callStatic
方法:
<?php
class Foo {
public function bar(): string
{
return 'baz';
}
public static function __callStatic(string $name, array $arguments) // [tl! highlight:start]
{
return 'hello, ' . $name;
} // [tl! highlight:end]
}
當我們呼叫 Foo::cornch()
時,就會得到 'hello, cornch'
的回傳值。
此外,__callStatic
中的第二個引數 $arguments
內會包含呼叫該方法時所代入的參數,如呼叫 Foo::hello('world', 'cornch.dev')
時,$arguments
的內容會是 ['world', 'cornch.dev']
。
讓我們再回到 Facade 類別中的 __callStatic
實作:
/** // [tl! reindex(321)] [tl! collapse:start]
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/ // [tl! collapse:end]
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);
}
若我們繼續以 Request::input('foo')
為例子的話,按照我們先前說的,Facade 是幫我們呼叫「真正的 Request 類別」。因此我們從上面這段程式碼可以推測出,其中的 $instance
變數顯然就是「真正的 Request 類別」,而之後 $instance->$method(...$args);
就是 Facade 幫我們在「真正的 Request 類別」上呼叫 $method
(在此例子中為 'input'
),並代入 ...$args
(在此例子中為 ...['foo']
)。
為了幫助讀者理解,我們可以將這些變數代入我們已知的實際值來看:
$instance = '真正的 Request 類別'; // [tl! reindex(332)]
if (! $instance) { // [tl! collapse:start]
throw new RuntimeException('A facade root has not been set.');
} // [tl! collapse:end]
return $instance->input('foo');
瞭解了這一段程式碼後,剩下的問題就是「真正的 Request 類別」是從哪裡來的了。因此,我們接著來看看 getFacadeRoot
方法。
getFacadeRoot
getFacadeRoot
方法的內部設計到了另外兩個方法:getFacadeAccessor
與 resolveFacadeInstance
,因此我們在這裡也一起看看:
/** // [tl! collapse:start] [tl! reindex(186)]
* Get the root object behind the facade.
*
* @return mixed
*/ // [tl! collapse:end]
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/** // [tl! collapse:start]
* Get the registered name of the component.
*
* @return string
*
* @throws \RuntimeException
*/ // [tl! collapse:end]
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
/** // [tl! collapse:start]
* Resolve the facade root instance from the container.
*
* @param string $name
* @return mixed
*/ // [tl! collapse:end]
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];
}
}
在 getFacadeRoot
中,先呼叫了 getFacadeAccessor
,我們在本文最開始就看到了,Request
Facade 中有實作這個方法,該方法回傳的是 'request'
字串。因此,代入 'request'
後的程式碼會長這樣,以 'request'
值呼叫 resolveFacadeInstance()
:
public static function getFacadeRoot() // [tl! reindex(191)]
{
return static::resolveFacadeInstance('request');
}
接著我們看看 resolveFacadeInstance
,這個方法就比較難理解了。我們可以看到,該方法先檢查 $resolvedInstance['request']
存不存在,如果存在的話直接回傳該值。從這點上可以看出這是某種 Cache 機制。
接著,若 $resolvedInstance['request']
不存在,則接著會檢查 static::$app
是否為 True 或等價值,然後 回傳 static::$app['request']
。我們也可以看到,這個方法也會判斷 static::$cache
來決定是否要在回傳該值前將其值寫入 $resolvedInstance['request']
。
讀到這裡,我們已經知道,「真正的 Request 類別」其實是存在 static::$app['request']
中。那麼 static::$app
是哪來的呢?型別又是什麼呢?
讓我們來看看 Facade 類別中的 $app
定義:
/** // [tl! reindex(16)]
* The application instance being facaded.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app;
phpdoc 註解中說,$app
存放的是「Application Instance」,應用程式的實體,而其型別是 \Illuminate\Contracts\Foundation\Application
。
在 Laravel Framework 的啟動 (Bootstrap) 過程中,Laravel 會自動設定這個值。有興趣的讀者可以看看這個檔案:Illuminate/Foundation/Bootstrap/RegisterFacades.php。
\Illuminate\Contracts\Foundation\Application
是一個「Service Container」。有關 Service Container 的說明,可以參考官方說明文件中有關 Service Container 的這一頁。
簡單來說,Service Container 是一個容器,用來處理 Inversion of Control (控制反轉) 我們可以在 Service Container 裡設定各個類別的實體,讓這些類別的實體化邏輯與實際要使用這些實體的邏輯分開,以降低程式碼耦合度。不過,關於 Service Container 的說明不在本文的討論範圍內。筆者之後有機會會再專門寫一篇文章討論 Service Container。
在此,讀者只需要知道,在 Laravel 中的某個地方,會有一段程式碼告訴 Service Container:「當有人向 Service Container 要求 'request'
時,給他 Request 實體」。
版權

本文中的文字部分使用 Creative Commons Attribution 4.0 International 授權。作者標識為「Cornch」。
本文中若有提供範例程式碼,將使用 MIT License 授權。作者標識為「Cornch」。
本文中的範例程式碼可能有部分來自 Laravel Framework。Laravel Framework 使用 MIT License。作者標識為「Taylor Otwell」。