Facades

设计模式中的 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


<<:  Day23 - 在 XState 中的平行式状态 Parallel States

>>:  Day23:终於要进去新手村了-Javascript-物件建立

DAY 6 『 TableView 』Part1

TableView:Storyboard + Table View + Table View Cel...

[ Day 11] Forensics 小暖身

哈罗,今天是一周的第一天 我们来试试 Forensics 吧 放心,一定从简单的题开始 又是拚手速的...

如何在Python GUI 中随时记录log并显示在UI上呢?

生活中的每个细节,有些人习惯使用图像的方式做纪录;有些人更喜欢使用文字去做纪录。 那在资讯领域中呢?...

Day 27 - 从零开始导入Terraform,Infrastructure as Code Terraform Atlantis

本文同步刊登於个人技术部落格,有兴趣关注更多 Kubernetes、DevOps 相关资源的读者,请...

Day24. 发动魔法卡,融合 - Composite (中)

昨天了解了 Composite 是什麽後,一如我们本来的安排,今天要来介绍的是 Composites...