iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 23
1
Software Development

Laravel 原始碼分析系列 第 23

分析 AliasLoader

  • 分享至 

  • xImage
  •  

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

  • 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 也相當有趣,非常值得大家參考。


上一篇
分析 Facade
下一篇
分析 Auth(1)
系列文
Laravel 原始碼分析46
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言