分析 AliasLoader

昨天了解 Facade 基本原理後,可能會覺得奇妙的關鍵不過就是 Magic Method 而已,但其實 Laravel 還有更神奇的。

下面這兩個呼叫的結果是一樣的:

\Illuminate\Support\Facades\Request::ip();
\Request::ip();

正因為有這個設計,所以預設 routes 下的設定可以這樣寫:

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 為例,三個會被置換的字串如下:

  • DummyNamespace - Facades\Illuminate\Http
  • DummyClass - Request
  • DummyTarget - 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 也相當有趣,非常值得大家參考。