iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0

設計模式中的 Facade pattern (外觀模式),指的是將整組的介面包裝起來,提供統一的介面方便取用各個介面的功能。

在 Laravel 中已經定義好的 Facade 類別都定義在 Illuminate\Support\Facades 命名空間底下,資料夾目錄的話是在 vendor/laravel/framework/src/Illuminate/Support/Facades/ 中。

我們可以引用這些類別並直接使用該類別的方法,即使是靜態(static)的方法,先看一下使用範例。

/app/Http/Controllers/TodoController.php

use Illuminate\Support\Facades\Auth;

public function store(Request $request)
{
    $data = $request->all(); 

    $user = Auth::user();  //藉由 Auth 的 Facade 直接使用 user 函式

    $this->todoService->create([
        'name' => $data['name']
    ]);

}

前面我們就已經藉由 Auth 的 Facade 類別取用到登入的 user 資訊,簡單的一行程式其實底下也一路連結到 Laravel 的 Service Container 產生的實例,接著就來抽絲剝繭看看 Facade 怎麼運作的。

Facade 類別

首先我們找到 Facades/Auth ,可以看到是繼承了 Facade 類別,所有的 Facade 都是繼承這個類別而來

vendor/laravel/framework/src/Illuminate/Support/Facades/Auth.php

<?php

namespace Illuminate\Support\Facades;

//...

class Auth extends Facade
{
 
    protected static function getFacadeAccessor()
    {
        return 'auth';
    }
    
    //...
    
}

Auth 裡的內容相當少,主要就是定義了 getFacadeAccessor 方法,功能只有回傳了 auth 字串,不知道幹嘛用。

再來找到 Facade 類別,跟 Auth 在同一個目錄

vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php

<?php

namespace Illuminate\Support\Facades;
 
 //...

abstract class Facade
{
    //...

    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);
    }
    
    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }
    
        
    protected static function getFacadeAccessor()
    {
        throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
    }
    
    protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            return static::$resolvedInstance[$name] = static::$app[$name];
        }
    }
}

Facade 裡面定義了許多方法,不過要介紹 Facade 魔法般功能的話只要看上面幾個函式,我調換一下順序方便解說。

__callStatic()

首先是 __callStatic() ,這個是 PHP 原生定義的魔法函式,當一個類別有定義 __callStatic() 的時候,如果試圖直接從類別呼叫靜態(static)方法或屬性,就會變成呼叫 __callStatic()。

static 屬性跟方法正常只能在類別被建成實例後才能經由實例取用,直接從類別取用會報錯

也就是當我們呼叫 Auth::user() 時,其實我們是在呼叫 Facade 的 __callStatic() ,並解 user 以字串作為 $method 傳入,如果有參數的話經由 $args 傳入。

接著,__callStatic() 試圖藉由 getFacadeRoot 取得實例好執行該實例的 $method 方法。

   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);
    }

那怎麼知道要創建哪個類別的實例呢? 首先看到這兩個方法

    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }
    
        
    protected static function getFacadeAccessor()
    {
        throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
    }

有沒有發現 getFacadeAccessor 很眼熟? 在 Auth 繼承 Facade 之後已經覆寫了這個函式,所以在 Auth 中這邊的功能變成了回傳 'auth' 字串。

接著將這個 auth 字串作為 $name 傳入 resolveFacadeInstance 方法。

    
    protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            return static::$resolvedInstance[$name] = static::$app[$name];
        }
    }

放大看最後一行

return static::$resolvedInstance[$name] = static::$app[$name];

$app 指的就是 Laravel App 實例,也就是我們好朋友 Service Container,所以這邊就是回傳了在 Service Container 中註冊為 auth 的實例,接著在 __callStatic 中才能藉由這個實例使用靜態屬性。

至於 auth 在哪邊註冊的,自然是 Service Provider 囉。

vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php

<?php

namespace Illuminate\Auth;

//...

class AuthServiceProvider extends ServiceProvider
{
    //...
    
    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', function ($app) {
            return new AuthManager($app);
        });

        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }

    //...
}

自定義 Facade

看完上面 Facade 的運作流程,要如何自訂 Facade 也很明顯了。

首先要在 Service Container 註冊好類別或介面。

接著建一個繼承 Facade 的類別,並覆蓋 getFacadeAccessor 方法,讓他回傳你註冊的類別或介面名稱。

class Response extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return ResponseFactoryContract::class;
    }
}

這樣就能直接從新建立的 Facade 類別取用靜態功能了。

即時 Facade

上面說了自製 Facade 的方法,不過一個個建也是很麻煩,於是貼心的 Laravel 設想了能夠動態建立 Facade 的方法。

先看一般的依賴注入方法

<?php

namespace App\Models;

use App\Contracts\Publisher;
use Illuminate\Database\Eloquent\Model;

class Podcast extends Model
{
    public function publish(Publisher $publisher)
    {
        $this->update(['publishing' => now()]);

        $publisher->publish($this);
    }
}

再來看看 Facade 版

<?php

namespace App\Models;

use Facades\App\Contracts\Publisher;  //原本的類別命名域前面加上了 Facades
use Illuminate\Database\Eloquent\Model;

class Podcast extends Model
{
    /**
     * Publish the podcast.
     *
     * @return void
     */
    public function publish()
    {
        $this->update(['publishing' => now()]);

        Publisher::publish($this);
    }
}

可以看到引用的 Publisher 其命名域前面被加上了 Facades ,這樣宣告的話 Laravel 就會以 Facades\ 之後的字串作為 name 來產生 Facade 類別。

缺點

因為 Facade 不用像依賴注入一樣額外用 __construct 方法來定義,當一個類別依賴的 Facade 越來越多時,是不容易發現的。

反過來說用依賴注入的話 __construct 就會越依賴越大包,看到 __construct 過大就要知道該拆分類別的功能了,而用 Facade 就比較不容易發現這種問題。

Reference

Laravel Facades


上一篇
依賴注入
下一篇
身分驗證
系列文
Laravel 實務筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言