在開始以前,歡迎關注我本身的博客:www.leoyang90.cn
這篇文章咱們開始講 laravel 框架中的門面 Facade,什麼是門面呢?官方文檔:php
Facades(讀音:/fəˈsäd/ )爲應用程序的服務容器中可用的類提供了一個「靜態」接口。Laravel 自帶了不少 facades ,幾乎能夠用來訪問到 Laravel 中全部的服務。Laravel facades 其實是服務容器中那些底層類的「靜態代理」,相比於傳統的靜態方法, facades 在提供了簡潔且豐富的語法同時,還帶來了更好的可測試性和擴展性。laravel
什麼意思呢?首先,咱們要知道 laravel 框架的核心就是個 Ioc 容器即 服務容器,功能相似於一個工廠模式,是個高級版的工廠。laravel 的其餘功能例如路由、緩存、日誌、數據庫其實都是相似於插件或者零件同樣,叫作 服務。Ioc 容器主要的做用就是生產各類零件,就是提供各個服務。在 laravel 中,若是咱們想要用某個服務,該怎麼辦呢?最簡單的辦法就是調用服務容器的 make 函數,或者利用依賴注入,或者就是今天要講的門面 Facade。門面相對於其餘方法來講,最大的特色就是簡潔,例如咱們常用的 Router,若是利用服務容器的 make:數據庫
App::make('router')->get('/', function () { return view('welcome'); });
若是利用門面:bootstrap
Route::get('/', function () { return view('welcome'); });
能夠看出代碼更加簡潔。其實,下面咱們就會介紹門面最後調用的函數也是服務容器的 make 函數。segmentfault
咱們以 Route 爲例,來說解一下門面 Facade 的原理與實現。咱們先來看 Route 的門面類:數組
class Route extends Facade { protected static function getFacadeAccessor() { return 'router'; } }
很簡單吧?其實每一個門面類也就是重定義一下 getFacadeAccessor 函數就好了,這個函數返回服務的惟一名稱:router。須要注意的是要確保這個名稱能夠用服務容器的 make 函數建立成功(App::make('router')),緣由咱們立刻就會講到。
那麼當咱們寫出 Route::get() 這樣的語句時,到底發生了什麼呢?奧祕就在基類 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); }
當運行 Route::get() 時,發現門面 Route 沒有靜態 get() 函數,PHP 就會調用這個魔術函數 __callStatic。咱們看到這個魔術函數作了兩件事:得到對象實例,利用對象調用 get() 函數。首先先看看如何得到對象實例的:app
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]; } return static::$resolvedInstance[$name] = static::$app[$name]; }
咱們看到基類 getFacadeRoot() 調用了 getFacadeAccessor(),也就是咱們的服務重載的函數,若是調用了基類的 getFacadeAccessor,就會拋出異常。在咱們的例子裏 getFacadeAccessor() 返回了 「router」,接下來 getFacadeRoot() 又調用了 resolveFacadeInstance()。在這個函數裏重點就是composer
return static::$resolvedInstance[$name] = static::$app[$name];
咱們看到,在這裏利用了 app 也就是服務容器建立了 「router」,建立成功後放入 resolvedInstance做爲緩存,以便之後快速加載。
好了,Facade 的原理到這裏就講完了,可是到這裏咱們有個疑惑,爲何代碼中寫 Route 就能夠調用 IlluminateSupportFacadesRoute 呢?這個就是別名的用途了,不少門面都有本身的別名,這樣咱們就沒必要在代碼裏面寫 use IlluminateSupportFacadesRoute,而是能夠直接用 Route 了。框架
爲何咱們能夠在 larval 中全局用 Route,而不須要使用 use IlluminateSupportFacadesRoute?其實奧祕在於一個 PHP 函數:class_alias,它能夠爲任何類建立別名。larval 在啓動的時候爲各個門面類調用了 class_alias 函數,所以沒必要直接用類名,直接用別名便可。在 config 文件夾的 app 文件裏面存放着門面與類名的映射:
'aliases' => [ 'App' => Illuminate\Support\Facades\App::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, 'Auth' => Illuminate\Support\Facades\Auth::class, ... ]
下面咱們來看看 laravel 是如何爲門面類建立別名的。
說到 larval 的啓動,咱們離不開 index.php:
require __DIR__.'/../bootstrap/autoload.php'; $app = require_once __DIR__.'/../bootstrap/app.php'; $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); ...
第一句就是咱們前面博客說的 composer 的自動加載,接下來第二句獲取 laravel 核心的 Ioc 容器,第三句「製造」出 Http 請求的內核,第四句是咱們這裏的關鍵,這句牽扯很大,laravel 裏面全部功能服務的註冊加載,乃至 Http 請求的構造與傳遞都是這一句的功勞。
$request = Illuminate\Http\Request::capture()
這句是 laravel 經過全局 _SERVER 數組構造一個 Http 請求的語句,接下來會調用 Http 的內核函數 handle:
public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { $this->reportException($e); $response = $this->renderException($request, $e); } catch (Throwable $e) { $this->reportException($e = new FatalThrowableError($e)); $response = $this->renderException($request, $e); } event(new Events\RequestHandled($request, $response)); return $response; }
在 handle 函數方法中 enableHttpMethodParameterOverride 函數是容許在表單中使用 delete、put 等類型的請求。咱們接着看 sendRequestThroughRouter:
protected function sendRequestThroughRouter($request) { $this->app->instance('request', $request); Facade::clearResolvedInstance('request'); $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); }
前兩句是在 larval 的 Ioc 容器設置 request 請求的對象實例,Facade 中清楚 request 的緩存實例。bootstrap:
public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ];
$bootstrappers 是 Http 內核裏專門用於啓動的組件,bootstrap 函數中調用 Ioc 容器的 bootstrapWith 函數來建立這些組件並利用組件進行啓動服務。app->bootstrapWith:
public function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]); } }
能夠看到 bootstrapWith 函數也就是利用 Ioc 容器建立各個啓動服務的實例後,回調啓動本身的函數 bootstrap,在這裏咱們只看咱們 Facade 的啓動組件
\Illuminate\Foundation\Bootstrap\RegisterFacades::class
RegisterFacades 的 bootstrap 函數:
class RegisterFacades { public function bootstrap(Application $app) { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register(); } }
能夠看出來,bootstrap 作了一下幾件事:
清除了 Facade 中的緩存
設置 Facade 的 Ioc 容器
得到咱們前面講的 config 文件夾裏面 app 文件 aliases 別名映射數組
使用 aliases 實例化初始化 AliasLoader
調用 AliasLoader->register()
public function register() { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } protected function prependToLoaderStack() { spl_autoload_register([$this, 'load'], true, true); }
咱們能夠看出,別名服務的啓動關鍵就是這個 spl_autoload_register,這個函數咱們應該很熟悉了,在自動加載中這個函數用於解析命名空間,在這裏用於解析別名的真正類名。
咱們首先來看看被註冊到 spl_autoload_register 的函數,load:
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); } }
這個函數的下面很好理解,就是 class_alias 利用別名映射數組將別名映射到真正的門面類中去,可是上面這個是什麼呢?實際上,這個是 laravel5.4 版本新出的功能叫作實時門面服務。
其實門面功能已經很簡單了,咱們只須要定義一個類繼承 Facade 便可,可是 laravel5.4 打算更近一步——自動生成門面子類,這就是實時門面。
實時門面怎麼用?看下面的例子:
namespace App\Services; class PaymentGateway { protected $tax; public function __construct(TaxCalculator $tax) { $this->tax = $tax; } }
這是一個自定義的類,若是咱們想要爲這個類定義一個門面,在 laravel5.4 咱們能夠這麼作:
use Facades\ { App\Services\PaymentGateway }; Route::get('/pay/{amount}', function ($amount) { PaymentGateway::pay($amount); });
那麼這麼作的原理是什麼呢?咱們接着看源碼:
protected static $facadeNamespace = 'Facades\\'; if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) { $this->loadFacade($alias); return true; }
若是命名空間是以 Facades\ 開頭的,那麼就會調用實時門面的功能,調用 loadFacade 函數:
protected function loadFacade($alias) { tap($this->ensureFacadeExists($alias), function ($path) { require $path; }); }
tap 是 laravel 的全局幫助函數,ensureFacadeExists 函數負責自動生成門面類,loadFacade 負責加載門面類:
protected function ensureFacadeExists($alias) { if (file_exists($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) { return $path; } file_put_contents($path, $this->formatFacadeStub( $alias, file_get_contents(__DIR__.'/stubs/facade.stub') )); return $path; }
能夠看出來,laravel 框架生成的門面類會放到 stroge/framework/cache/ 文件夾下,名字以 facade 開頭,以命名空間的哈希結尾。若是存在這個文件就會返回,不然就要利用 file_put_contents 生成這個文件,formatFacadeStub:
protected function formatFacadeStub($alias, $stub) { $replacements = [ str_replace('/', '\\', dirname(str_replace('\\', '/', $alias))), class_basename($alias), substr($alias, strlen(static::$facadeNamespace)), ]; return str_replace( ['DummyNamespace', 'DummyClass', 'DummyTarget'], $replacements, $stub ); }
簡單的說,對於 Facades\App\Services\PaymentGateway
,replacements
第一項是門面命名空間,將 Facades\App\Services\PaymentGateway
轉爲 Facades/App/Services/PaymentGateway
,取前面 Facades/App/Services/
,再轉爲命名空間 Facades\App\Services\
;第二項是門面類名,PaymentGateway
;第三項是門面類的服務對象,App\Services\PaymentGateway
,用這些來替換門面的模板文件:
<?php namespace DummyNamespace; use Illuminate\Support\Facades\Facade; /** * @see \DummyTarget */ class DummyClass extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'DummyTarget'; } }
替換後的文件是:
<?php namespace Facades\App\Services\; use Illuminate\Support\Facades\Facade; /** * @see \DummyTarget */ class PaymentGateway extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'App\Services\PaymentGateway'; } }
就是這麼簡單!!!
門面的原理就是這些,相對來講門面服務的原理比較簡單,和自動加載相互配合使得代碼更加簡潔,但願你們能夠更好的使用這些門面!