Magic for Artisan (1) - Facade

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 方法的內部設計到了另外兩個方法:getFacadeAccessorresolveFacadeInstance,因此我們在這裡也一起看看:

/** // [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

本文中的文字部分使用 Creative Commons Attribution 4.0 International 授權。作者標識為「Cornch」。

本文中若有提供範例程式碼,將使用 MIT License 授權。作者標識為「Cornch」。

本文中的範例程式碼可能有部分來自 Laravel Framework。Laravel Framework 使用 MIT License。作者標識為「Taylor Otwell」。