深刻淺出 Laravel 的 Facade 外觀系統

本文首發於 深刻淺出 Laravel 的 Facade 外觀系統,轉載請註明出處。

今天咱們將學習 Laravel 核心架構中的另外一個主題「Facade(外觀)」。php

本文將從如下幾個方面出發,全面講解 Laravel 中 Facade 的運行原理,爲了便於理解後續中全部 Facade 譯做「外觀」:html

  • 簡單介紹「外觀」設計模式;
  • Laravel「外觀」的加載原理;
  • Laravel「外觀」基本使用。

什麼是「外觀」設計模式

外觀模式定義

爲子系統中的一組接口提供一個統一的入口。外觀模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。java

外觀模式是一種使用頻率很是高的結構型設計模式,它經過引入一個外觀角色來簡化客戶端與子系統之間的交互,
爲複雜的子系統調用提供一個統一的入口,下降子系統與客戶端的耦合度,且客戶端調用很是方便。 - 設計模式 Java 版

核心 就是在 客戶端(使用者)子系統(接口或服務) 之間引入一個「外觀」角色。laravel

將使用者與子系統從直接耦合,轉變成由「外觀」類提供統一的接口給使用者使用,以下降客戶端與子系統之間的耦合度。git

結構示意圖:

design pattern of facade

關於「外觀模式」能夠閱讀 設計模式 Java 版 - 外觀模式github

Laravel 外觀組件

Laravel 中的「外觀」組件其實是服務容器中底層類的「靜態代理」,它將 Laravel 內核中定義的「Contracts(在 Laravel 中又
稱爲服務、契約或者一般咱們所說的接口)」,以靜態可調用的方式封裝到各個「外觀」服務中供咱們使用。bootstrap

外觀加載原理

在講解如何使用外觀組件以前,咱們依舊先去深刻分析「外觀」組件是如何被 Laravel 加載到項目中的。這一步是
用好「外觀」組件的前提。設計模式

外觀組件配置

全部內置的外觀組件的配置數據,同 Laravel 其它服務同樣被定義在 config/app.php 文件中。讓咱們來瀏覽一下 aliases 節點的配置數據吧:數組

...
    'aliases' => [

        'App' => Illuminate\Support\Facades\App::class,
        'Artisan' => Illuminate\Support\Facades\Artisan::class,
        ...
    ],
    ...

外觀配置定義格式遵循 「別名」:「外觀類」 的數據格式。當一個 HTTP 請求被接收時,將在處理請求階段將這些「外觀」組件加載到服務中。架構

接下來將深刻分析外觀服務的加載過程。

加載外觀服務

「外觀」服務的加載工做由定義在 Illuminate\Foundation\Http\Kernel 內核中的 \Illuminate\Foundation\Bootstrap\RegisterFacades::class 啓動程序完成。

引導啓動外觀服務

若是你已經閱讀個人另外一篇文章 深刻剖析 Laravel 服務提供者實現原理,你應該對引導程序不會太陌生。

引導程序將在處理 HTTP 請求是完成引導啓動 bootstrap()。因此這裏咱們須要深刻到 RegisterFacades 類的內部去了解更多細節上的處理。

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Contracts\Foundation\Application;

/**
 * @link https://github.com/laravel/framework/blob/56a58e0fa3d845bb992d7c64ac9bb6d0c24b745a/src/Illuminate/Foundation/Bootstrap/RegisterFacades.php
 */
class RegisterFacades
{
    /**
     * Bootstrap the given application. 引導啓動服務
     */
    public function bootstrap(Application $app)
    {
        // 清除已解析的「外觀」服務實例
        Facade::clearResolvedInstances();

        // 將 Laravel 服務容器注入到「外觀」服務
        Facade::setFacadeApplication($app);

        // 加載全部外觀服務
        AliasLoader::getInstance(array_merge(
            $app->make('config')->get('app.aliases', []),
            $app->make(PackageManifest::class)->aliases()
        ))->register();
    }
}

加載外觀服務有 AliasLoader 組件完成:

  • 首先,會從配置文件 config/app.php 中讀取全部的「外觀」服務配置 aliases
  • 再從清單文件中讀取別名服務 $app->make(PackageManifest::class)->aliases()
  • 將兩個配置數組合並後注入到 AliasLoader 完成 註冊(register)

註冊外觀服務

最後咱們來瞧瞧 AliasLoader 加載器是如何將全部的「外觀」服務加載到系統中的。

<?php

namespace Illuminate\Foundation;

/**
 * @link https://github.com/laravel/framework/blob/56a58e0fa3d845bb992d7c64ac9bb6d0c24b745a/src/Illuminate/Foundation/AliasLoader.php
 */
class AliasLoader
{
    /**
     * Get or create the singleton alias loader instance. 獲取或建立「別名加載器」單例實例。
     */
    public static function getInstance(array $aliases = [])
    {
        if (is_null(static::$instance)) {
            return static::$instance = new static($aliases);
        }

        $aliases = array_merge(static::$instance->getAliases(), $aliases);

        static::$instance->setAliases($aliases);

        return static::$instance;
    }

    /**
     * Set the registered aliases. 設置需註冊別名數據。
     */
    public function setAliases(array $aliases)
    {
        $this->aliases = $aliases;
    }

    /**
     * Register the loader on the auto-loader stack. 將加載器註冊到自動加載中。
     */
    public function register()
    {
        if (! $this->registered) {
            $this->prependToLoaderStack();

            $this->registered = true;
        }
    }

    /**
     * Prepend the load method to the auto-loader stack. 設置自動加載方法。
     */
    protected function prependToLoaderStack()
    {
        // 將 AliasLoader 的 load 方法做爲 __autoload 的實現
        spl_autoload_register([$this, 'load'], true, true);
    }

    /**
     * Load a class alias if it is registered.從註冊過的服務中加載這個「外觀」服務。
     */
    public function load($alias)
    {
        if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
            $this->loadFacade($alias);

            return true;
        }

        if (isset($this->aliases[$alias])) {
            return class_alias($this->aliases[$alias], $alias);
        }
    }
}

注意 這裏是知識點,在 AliasLoader->register() 完成「外服服務註冊」涉及 PHP 兩個知識的應用:

  • PHP 內置魔術方法 __autoload 的使用;
  • PHP 如何給類建立別名。

➤ 1. 外觀服務的動態引入

咱們知道 __autoload 魔術方法的做用是嘗試加載未經定義的類,這樣當咱們使用一個未經引入的類時,則會自動的給咱們引入這個類。

更優的解決方案是經過 spl_autoload_register 函數,將自定義的類加載程序做爲 __autoload 的實現,以替代默認 __autoload() 模式函數或方法的行爲。

全部 prependToLoaderStack() 方法:

/**
     * Prepend the load method to the auto-loader stack. 設置自動加載方法。
     */
    protected function prependToLoaderStack()
    {
        // 將 AliasLoader 的 load 方法做爲 __autoload 的實現
        spl_autoload_register([$this, 'load'], true, true);
    }

就是去完成這樣的做用,將 AliasLoader->load() 方法做爲自動加載程序的實現,在使用「外觀」服務時動態引入這個類。

➤ 2. 支持外觀服務別名

咱們已經瞭解到當「外觀」服務被使用時,由 AliasLoader->load() 去自動加載這個類。

與此同時,load 方法經過 class_alias($original, $alias) 函數完成別名註冊。

這樣,當咱們使用 App 類時實際上就是在使用 Illuminate\Support\Facades\App 類。

很完美麼,咱們的「狗蛋」終於與「世界上最好的語言」畫上了等號。你就是我,我就是你。

到這裏其實已經完成了「外觀」服務工做原理分析工做的 70%

探祕 Facade

最後咱們將揭開 Facade 的神祕面紗,研究一下 Laravel 是如何實現 Facade 設計模式的。

咱們拿 IlluminateSupportFacadesApp 外觀服務開刀,去解開相似 App::make() 靜態方法使用的奧祕。

深刻 FacadesApp

<?php

namespace Illuminate\Support\Facades;

class App extends Facade
{
    /**
     * Get the registered name of the component.
     */
    protected static function getFacadeAccessor()
    {
        return 'app';
    }
}

咱們看到它的實現內部僅僅定義了一個 getFacadeAccessor 方法,該方法的功能是獲取已註冊組件的名稱 app;除此以外,一無全部。

看來在這裏咱們得不到什麼有用的信息了。繼續調查基類 IlluminateSupportFacadesFacade。若是你有去通便瀏覽所有的源碼。

<?php

namespace Illuminate\Support\Facades;

use Mockery;
use RuntimeException;
use Mockery\MockInterface;

/**
 * @link https://github.com/laravel/framework/blob/5.6/src/Illuminate/Support/Facades/Facade.php
 */
abstract class Facade
{
    /**
     * Handle dynamic, static calls to the object.
     */
    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);
    }
}

你會發現這個 Facade 基類並無定義相似 make 的方法,那麼這裏可以靜態調用 App::make() 看來是須要從 __callStatic 着手才行。

不過在這裏咱們須要再次釐清一個事實:「外觀」模式的功能是什麼?

將使用者與子系統從直接耦合,轉變成由「外觀」類提供統一的接口給使用者使用,以下降客戶端與子系統之間的耦合度。

這句話的意思就是我「外觀」啥也不提供,就是一層對服務(或者說組件或接口)的封裝,而後以統一的方式提供給大家外部調用。

好了如今咱們來看看 Facade::__callStatic 是如何獲取實際的服務並調用響應的方法的吧。

  • 首先,經過 getFacadeRoot 靜態方法獲取實際服務的實例對象;
  • 而後,調用實例對象的相關方法並返回處理結果。
<?php

namespace Illuminate\Support\Facades;

use Mockery;
use RuntimeException;
use Mockery\MockInterface;

abstract class Facade
{
    /**
     * Get the root object behind the facade. 從 facade 中解析出真實服務的對象
     */
    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }

    /**
     * Resolve the facade root instance from the container.me 從 Laravel 服務容器中解析出真實服務的對象
     */
    protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

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

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

getFacadeRoot 解析對象的功能中咱們能夠看到:它會調用實現「外觀」的 getFacadeAccessor 方法獲取到組件(服務或者說接口)的名稱;而後從 Laravel 服務容器 static::$app[$name](app 是在 RegisterFacades 中註冊到「外觀」中) 中解析出相關服務。

到這裏,咱們就將「外觀」服務的基本工做原理給分析透徹了。

另外有關「外觀」組件的一些細枝末節,如:

仍是須要你自行深刻到 Facade 基類去一探究竟。

掃盲 ArrayAccess 接口

另外補充一個知識點就是關於 static::$app[$name] 這一句代碼。你不經要問,這有啥好補充的呢,不就是一個簡單獲取數據麼。

獲取數據不假,簡單也不假。

不過你仔細看一下,你會發現 static::$app 靜態成員變量難道不是一個 \Illuminate\Contracts\Foundation\Application 實現實例麼,怎麼能夠從對象中以數組的方式獲取值呢?

這是由於咱們的服務容器 Illuminate\Container\Container 實現了 ArrayAccess 接口。

該接口的功能是提供像訪問數組同樣訪問對象的能力的接口,這樣就能夠像數組同樣訪問對象訪問成員。

/**
 *@link https://github.com/laravel/framework/blob/5.6/src/Illuminate/Container/Container.php
 */
class Container implements ArrayAccess, ContainerContract
{
    /**
     * Get the value at a given offset. 獲取一個偏移位置的值,實際上從容器中解析出服務。
     */
    public function offsetGet($key)
    {
        return $this->make($key);
    }
}

Laravel「外觀」基本使用

外觀服務的一個典型使用場景是在定義路由時使用 Route::get('/', ...)。這樣一看彷佛「Laravel 別名服務」也就不這麼神祕了。

相關文章
相關標籤/搜索