昨天了解 Facade 基本原理後,可能會覺得奇妙的關鍵不過就是 Magic Method 而已,但其實 Laravel 還有更神奇的。
下面這兩個呼叫的結果是一樣的:
\Illuminate\Support\Facades\Request::ip();
\Request::ip();
正因為有這個設計,所以預設 routes
下的設定可以這樣寫:
<?php
Route::get('/', function () {
return view('welcome');
});
這功能是由 AliasLoader 實作的,我們來看看它如何被使用:
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
這裡會把 config/app.php
的 aliases 設定與 PackageManifest 所取得的 aliases 合併,然後當做參數傳給 getInstance()
,接著再註冊。
這裡注意到,PackageManifest 是會有機會覆蓋原有 config 的設定。
以上面的 Request 與 Route 為例,aliases 設定如下:
[
'Request' => Illuminate\Support\Facades\Request::class,
'Route' => Illuminate\Support\Facades\Route::class,
];
再來就看 getInstance()
是如何處理這份設定:
public static function getInstance(array $aliases = [])
{
// 單例模式,初始化只是把設定找個地方放而已
if (is_null(static::$instance)) {
return static::$instance = new static($aliases);
}
// 新舊設定合併,並會以新設定覆蓋舊設定
$aliases = array_merge(static::$instance->getAliases(), $aliases);
// 重新設定 aliases
static::$instance->setAliases($aliases);
// 回傳實例
return static::$instance;
}
private function __clone()
{
// 單例模式必須將 clone 設定成 private
}
其實它也沒做什麼,單純產生實例,與將設定更新而已。一般單例很單純,就是產生實例而已,不過這裡有個特別的設計是:它會更新實例的設定。會這麼做的理由也很明顯,正是為了測試。
只是一樣是產實例,為何它是特別設計一個單例,而不是被 Container 管控了,接著看下去就會了解了。
產生實例後,會呼叫 register()
方法:
public function register()
{
// 確保註冊行為只會有一次
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
prependToLoaderStack()
方法:
protected function prependToLoaderStack()
{
spl_autoload_register([$this, 'load'], true, true);
}
spl_autoload_register()
實現了自動載入機制。Composer 正是使用這個函式實作了自動載入,可以參考它的 AutoloadGenerator 是如何實作的。
PHP 的自動載入機制,是使用一個 list,裡面每個元素都是實作自動載入的方法。當要使用某個類別,可是它還沒被載入時,就會拿出這個 list 嘗試載入。載入的結果會有成功或失敗,當載入成功後,後面的方法就不需要再執行了;失敗才需要換下一個。
某種程度蠻像 Routing 的設計:RouteCollection = list、Route = 自動載入方法、Request = 未載入的類別、而 spl_autoload_register() 的任務就類似 Router。
spl_autoload_register()
的參數有三個,第一個是 callable,也就是自動載入的方法;第二個是註冊失敗要不要丟例外;第三個是要不要把自動載入的順序往前移。
從原始碼可以知道,這個載入方法會提前到第一個順位,並且使用 load()
當作自動載入方法:
public function load($alias)
{
// 實作 Real-Time Facades 的程式碼片段,如果符合條件才會執行
if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
$this->loadFacade($alias);
return true;
}
// 如果 aliases 存在,就使用 class_alias() 讓別名跟實際類別可以直接對應
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
回到一開始的設定範例:
[
'Request' => Illuminate\Support\Facades\Request::class,
'Route' => Illuminate\Support\Facades\Route::class,
];
使用 class_alias()
之後,就能讓對 Request
的靜態操作,可以跟操作 Illuminate\Support\Facades\Request
完全一模一樣。
接著看更神奇的 Real-Time Facades,筆者也是今天才發現有這個更 magic 的東西。Facade 雖然好用,但很麻煩的是,因為它是靜態操作,意謂著需要做一些靜態的前置作業。說更簡單一點就是,要事前準備好程式才能用。Real-Time Facades 正是解決這個麻煩事,它可以動態產生 Facade。
5.4 版之後開始支援 Real-Time Facades
說明這麼多,不如直接看範例,下面這四段程式碼回傳的結果是一樣的:
\Illuminate\Support\Facades\Request::ip();
\Request::ip();
\Illuminate\Http\Request::capture()->ip();
\Facades\Illuminate\Http\Request::ip();
前兩個範例在前面已經說明了。\Illuminate\Http\Request::capture()->ip()
這個為何能正常執行,可以參考一開始分析 bootstrap 流程是如何產生 request 實例的。
最後一個就是神奇的 Real-Time Facades 使用方法。回到剛剛的 load()
程式片段:
// static::$facadeNamespace 預設是 `Facades\\`,只要開頭是這串字的就會執行
if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
// 載入 Real-Time Facade
$this->loadFacade($alias);
return true;
}
protected function loadFacade($alias)
{
// 載入的方法很簡單,直接 require 動態產生出來的 file 即可
require $this->ensureFacadeExists($alias);
}
從上面的程式碼看,可以猜想得到 ensureFacadeExists()
會產生檔案,並回傳檔案路徑:
protected function ensureFacadeExists($alias)
{
// 檔案將會放在 storage/framework/cache 裡。如果檔案存在,就直接回傳路徑
if (file_exists($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) {
return $path;
}
// 不存在,就現場生一個
file_put_contents($path, $this->formatFacadeStub(
$alias, file_get_contents(__DIR__.'/stubs/facade.stub')
));
return $path;
}
protected function formatFacadeStub($alias, $stub)
{
// 程式碼產生的實作,實際上就是把 stub 裡,對應的文字換成 Real-Time Facade 的內容
$replacements = [
str_replace('/', '\\', dirname(str_replace('\\', '/', $alias))),
class_basename($alias),
substr($alias, strlen(static::$facadeNamespace)),
];
return str_replace(
['DummyNamespace', 'DummyClass', 'DummyTarget'], $replacements, $stub
);
}
以 Facades\Illuminate\Http\Request
為例,三個會被置換的字串如下:
程式碼很單純,換來看一下 stub 裡面長什麼樣:
<?php
namespace DummyNamespace;
use Illuminate\Support\Facades\Facade;
/**
* @see \DummyTarget
*/
class DummyClass extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'DummyTarget';
}
}
getFacadeAccessor()
最後回傳的會是 Illuminate\Http\Request
,再來就如同 Facade 的分析--會去 Container 取得實例並呼叫。
整個分析下來後,其實可以發現 Facade 是一個活用 PHP 特性的好範例,Real-Time Facades 也相當有趣,非常值得大家參考。