設計模式中的 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 怎麼運作的。
首先我們找到 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() ,這個是 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 也很明顯了。
首先要在 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 的方法,不過一個個建也是很麻煩,於是貼心的 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 就比較不容易發現這種問題。