本文首發於 深刻淺出 Laravel 的 Facade 外觀系統,轉載請註明出處。
今天咱們將學習 Laravel 核心架構中的另外一個主題「Facade(外觀)」。php
本文將從如下幾個方面出發,全面講解 Laravel 中 Facade 的運行原理,爲了便於理解後續中全部 Facade 譯做「外觀」:html
爲子系統中的一組接口提供一個統一的入口。外觀模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。java
外觀模式是一種使用頻率很是高的結構型設計模式,它經過引入一個外觀角色來簡化客戶端與子系統之間的交互,
爲複雜的子系統調用提供一個統一的入口,下降子系統與客戶端的耦合度,且客戶端調用很是方便。 - 設計模式 Java 版
核心 就是在 客戶端(使用者) 與 子系統(接口或服務) 之間引入一個「外觀」角色。laravel
將使用者與子系統從直接耦合,轉變成由「外觀」類提供統一的接口給使用者使用,以下降客戶端與子系統之間的耦合度。git
關於「外觀模式」能夠閱讀 設計模式 Java 版 - 外觀模式github
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 組件完成:
最後咱們來瞧瞧 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 兩個知識的應用:
➤ 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 的神祕面紗,研究一下 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 是如何獲取實際的服務並調用響應的方法的吧。
<?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 基類去一探究竟。
另外補充一個知識點就是關於 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); } }
外觀服務的一個典型使用場景是在定義路由時使用 Route::get('/', ...)。這樣一看彷佛「Laravel 別名服務」也就不這麼神祕了。