文章簡單介紹了 Facade 門面寸癌,總結(jié)了其工作原理,并介紹了 Facade 的使用方法弱贼。
介紹
Facade 為應用服務容器中綁定的類提供了一個“靜態(tài)”接口蒸苇。Laravel 內(nèi)置了很多 Facade,幾乎可以用來訪問 Laravel 中所有的服務哮洽。Laravel 的 Facade 作為服務容器中底層類的“靜態(tài)代理”填渠,相比于傳統(tǒng)靜態(tài)方法,F(xiàn)acade 提供了簡潔且豐富的語法同時,還帶來了更好的可測試性和擴展性氛什。Laravel 的所有 Facade 都定義在 “Illuminate\Support\Facades”命名空間下莺葫,我們也可以自己定義新的 Facade。
Facade 加載原理
為了更好的理解 Facade 的工作原理枪眉,我們從以下幾個方面進行介紹:
- Facade 配置文件
- 加載 RegisterFacades 類
- 注冊 Facade 服務
- 解析 Facade 服務
Facade配置文件
Facade 的配置文件保存在 config/app.php 中捺檬,主要用來保存所有的 Facade 別名,也即是把 Facade 和別名的對應關系存放在 config/app.php文件中的 aliases 數(shù)組里贸铜。aliases 數(shù)組的形式如下:
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
aliases 數(shù)組遵循(別名 => Facade 類)的數(shù)據(jù)格式堡纬,當接受一個 HTTP 請求時注冊 aliases 數(shù)組,但是并沒有實際的進行別名綁定蒿秦,只是通過 spl_autoload_register() 函數(shù)進行了注冊烤镐。只有在遇到尚未發(fā)現(xiàn)的別名時,才去真正綁定別名棍鳖,因此炮叶,別名綁定是“懶惰”加載,并不影響程序的性能渡处。
加載 RegisterFacades 類
Facade 服務的注冊工作由“Illuminate\Foundation\Bootstrap\RegisterFacades”類完成镜悉,RegisterFacades 類是在啟動應用程序的過程中加載的。因此医瘫,我們首先來看index.php文件
<?php
//public\index.php
/**
* 記錄框架的啟動時間
* microtime(get_as_float)函數(shù)返回當前Unix時間戳的微妙數(shù)
* get_as_float為true時返回浮點數(shù)侣肄,為false時返回字符串,默認為false
*/
define('LARAVEL_START', microtime(true));
/**
* Composer的自動加載文件
*/
require __DIR__.'/../vendor/autoload.php';
/**
* bootstrap文件里創(chuàng)建了一個Application實例
*/
$app = require_once __DIR__.'/../bootstrap/app.php';
/**
* 通過container服務容器獲得一個kernel類的實例醇份,(Illuminate\Contracts\Http\Kernel是一個接口稼锅,
* 真正生成的實例是App\Http\Kernel類)
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
/**
* laravel中所有功能服務的注冊加載
* 通過調(diào)用kernel的handle方法,返回一個response
* Illuminate\Http\Request::capture():通過全局$_SERVER數(shù)組構(gòu)造一個Http請求的語句
*/
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
//把response內(nèi)容發(fā)到瀏覽器
$response->send();
//執(zhí)行請求生命周期中的后續(xù)操作
$kernel->terminate($request, $response);
RegisterFacades 類的加載是在 $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ) 語句進行的僚纷。該語句主要用于 Laravel各個功能服務的注冊啟動缰贝。
$request = Illuminate\Http\Request::capture()
該語句是 Laravel 通過全局 $_SERVER 數(shù)組構(gòu)造一個 HTTP 請求的語句,接下來會調(diào)用 HTTP 的內(nèi)核函數(shù) handle()畔濒,RegisterFacades 類的加載即是在內(nèi)核函數(shù) handle() 中進行剩晴。
//Illuminate\Foundation\Http\Kernel.php
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
//允許在表單中使用delete、put等類型的請求
$request->enableHttpMethodParameterOverride();
//將請求發(fā)給中間件侵状、路由處理
$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);
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}
接下來赞弥,我們查看 sendRequestThroughRouter() 函數(shù)俏险。
//Illuminate\Foundation\Http\Kernel.php
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
//設置request請求的對象實例
$this->app->instance('request', $request);
//清楚'request'對應的服務對象實例
Facade::clearResolvedInstance('request');
/**
* 依次執(zhí)行$bootstrappers中每一個bootstrapper的bootstrap()函數(shù),做了幾件準備事情:
* 1. 配置加載 LoadConfiguration
* 2.日志配置 ConfigureLogging
* 3.異常處理 HandleException
* 4.注冊Facades RegisterFacades
* 5.注冊Providers RegisterProviders
* 6.啟動Providers BootProviders
*/
$this->bootstrap();
return (new Pipeline($this->app)) //請求的分發(fā)
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
函數(shù)首先在 Laravel 容器中設置了 request 請求的對象實例柬批,并且清楚了 Facade 中 request 請求對應的服務對象實例痰腮。接下來我們來看 bootstrap() 函數(shù)
//Illuminate\Foundation\Http\Kernel.php
/**
* Bootstrap the application for HTTP requests.
* 啟動引導(reauests驅(qū)動)
*
* @return void
*/
public function bootstrap()
{
//判斷引導是否已經(jīng)被啟動
if (! $this->app->hasBeenBootstrapped()) {
/**
* 使用Application類的bootstrapWith()函數(shù)啟動引導
* 參數(shù):bootstrapers數(shù)組晒旅。
*/
$this->app->bootstrapWith($this->bootstrappers());
}
}
/**
* The bootstrap classes for the application.
* 引導類,起引導作用的類
*
* @var array
*/
protected $bootstrappers = [
//載入服務器環(huán)境變量(.env文件)
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
//載入配置信息(config目錄)
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
//配置異常處理
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
//注冊Facades
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
//注冊Providers
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
//啟動Providers
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
bootstrap() 函數(shù)首先判斷引導類是否已經(jīng)啟動侣诺,如果沒有啟動抛人,則啟動引導類叹洲。引導類數(shù)組包含 RegisterFacades 類。啟動引導類是調(diào)用“Illuminate\Foundation\Application”類中的 bootstrapWith() 函數(shù)鲁纠。
//Illuminate\Foundation\Application
/**
* Run the given array of bootstrap classes.
*
* @param array $bootstrappers
* @return void
*/
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
/**
* 告知將要啟動該bootstrapper
* $this['events']:對應綁定的類為 Dispatcher類(Illuminate\Events\Dispatcher)
* [$this]:數(shù)組总棵,只有一個Application類元素
*/
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
//解析每個 $bootstrapper,并調(diào)用他們自身的 bootstrap() 函數(shù)
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}
bootstrapWith() 函數(shù)中的 $this->make($bootstrapper)->bootstrap($this) 語句即是解析出 $bootstrappers 數(shù)組中的每個類改含,并調(diào)用相應的 bootstrap() 函數(shù)情龄。因此,RegisterFacades 類也即是在此時調(diào)用捍壤。
注冊 Facade 服務
下面我們來看 RegisterFacades 類骤视。
//Illuminate\Foundation\Bootstrap\RegisterFacades.php
<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Contracts\Foundation\Application;
class RegisterFacades
{
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
//清除所有的門面對象服務實例
Facade::clearResolvedInstances();
//設置門面對象的Application實例
Facade::setFacadeApplication($app);
/**
* 默認的別名配置是從 app 配置文件下的 aliases 讀取的,
* PackageManifest 是 laravel 5.5 新增的 包自動發(fā)現(xiàn) 規(guī)則
*/
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
}
可以看出鹃觉,RegisterFacades 類只有一個 bootstrap() 函數(shù)专酗,該函數(shù)主要完成以下功能:
- 清楚所有 Facade 對象服務實例
- 設置門面對象的 Application 實例
- 通過 AliasLoader 類為所有的 Facade 注冊別名
接下來我們查看 AliasLoader 類如何注冊別名。
//llluminate\Foundation\AliasLoader.php
/**
* Get or create the singleton alias loader instance.
* 獲取或創(chuàng)建 AliasLoader 單例實例盗扇。
*
* @param array $aliases
* @return \Illuminate\Foundation\AliasLoader
*/
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;
}
/**
* Register the loader on the auto-loader stack.
* 將加載器注冊到自動加載中
*
* @return void
*/
public function register()
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
/**
* Prepend the load method to the auto-loader stack.
* 設置自動加載方法
*
* @return void
*/
protected function prependToLoaderStack()
{
// 把AliasLoader::load()放入自動加載函數(shù)隊列中笼裳,并置于隊列頭部
spl_autoload_register([$this, 'load'], true, true);
}
AliasLoader 類首先調(diào)用 getInstance() 函數(shù)獲得 AliasLoader 的單例實例,然后調(diào)用 register() 函數(shù)將 AliasLoader 類中的 load() 函數(shù)注冊到 SPL __autoload 函數(shù)隊列的頭部粱玲。
//llluminate\Foundation\AliasLoader.php
/**
* Load a class alias if it is registered.
*
* @param string $alias
* @return bool|null
*/
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);
}
}
load() 函數(shù)的上半部分是 laravel5.4 版本新出的功能,叫做實時門面服務拜轨。下半部分是 class_alias() 函數(shù)利用別名映射數(shù)組將別名映射到真正的門面類中去抽减。例如,我們使用別名類 Cache 時橄碾,程序會通過 AliasLoader 類的 load() 函數(shù)為“Illuminate\Support\Facades\Cache::class”類創(chuàng)建一個別名 Cache卵沉,所以我們在程序里使用別名 Cache 就是使用“Illuminate\Support\Facades\Cache”類。
解析 Facade 服務
以 Cache 為例法牲,我們講一下 Facade 的使用史汗。我們首先來看一下 Cache Facade 類。
<?php
//Illuminate\Support\Facades\Cache.php
class Cache extends Facade
{
/**
* Get the registered name of the component.
* 獲取組件注冊名稱
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cache';
}
}
該類內(nèi)部只有一個 getFacadeAccessor() 函數(shù)拒垃,該方法的功能是獲取已經(jīng)注冊組件的名稱停撞。其實每個門面類只是重寫了基類(Facade)的 getFacadeAccessor() 方法。
//Illuminate\Support\Facades\Facade.php
/**
* Handle dynamic, static calls to the object.
* 動態(tài)綁定悼瓮,將門面的靜態(tài)方法調(diào)用綁定到門面對應的服務對象實例來執(zhí)行
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
//返回當前門面對應的服務對象實例
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
//執(zhí)行服務對象實例相應的方法
return $instance->$method(...$args);
}
當運行 Cache::get() 函數(shù)時戈毒,門面類 Cache 類里并沒有 get() 函數(shù),此時 PHP 會自動調(diào)用魔術(shù)函數(shù) __callStatic()横堡。魔術(shù)函數(shù) __callStatic() 首先獲得 Facade 的服務對象實例埋市,然后調(diào)用對象的 get() 函數(shù)。下面我們來看如何獲得 Facade 的對象實例命贴。
//Illuminate\Support\Facades\Facade.php
/**
* Get the root object behind the facade.
* 返回當前門面對應服務對象的實例
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/**
* Get the registered name of the component.
* 獲取組件注冊名稱道宅,子類重寫該函數(shù)
*
* @return string
*
* @throws \RuntimeException
*/
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
/**
* Resolve the facade root instance from the container.
* 創(chuàng)建并返回 $name 對應的對象實例
*
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
//如果是對象食听,則直接返回
if (is_object($name)) {
return $name;
}
//$resolvedInstance數(shù)組中 $name 對應的對象實例是否存在,如果存在污茵,直接返回
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
/**
* 具體的創(chuàng)建工作由Application類對象進行樱报,所以$name需要在Application中進行過綁定
*/
return static::$resolvedInstance[$name] = static::$app[$name];
}
基類中的 getFacadeRoot() 函數(shù)首先調(diào)用了 getFacadeAccessor() 函數(shù),也即是 Cache 類重寫的 getFacadeAccessor() 函數(shù)省咨,如果調(diào)用的是基類的 getFacadeAccessor() 函數(shù)則報錯肃弟。然后調(diào)用 resolveFacadeInstance() 函數(shù)獲得“cache”對應的服務實例。resolveFacadeInstance() 函數(shù)主要是從 Laravel 服務容器 static::$app[$name] 中解析出相應的服務零蓉◇允埽“cache”服務是應用程序初始化時,在類“Illuminate\Foundation\Bootstrap\RegisterProviders”中被注冊到容器中的敌蜂。
因為在程序啟動時箩兽,將 Facade 的別名通過 RegisterFacades 類進行了加載注冊,所以可以直接使用別名 Cache 代替“Illuminate\Support\Facades\Cache”章喉。
其實汗贫,Cache::get('key') 的寫法等價于以下寫法。
$value = $app->make('cache')->get('key');
建立 Facade
建立自己的 Facade秸脱,需要做以下幾方面的工作:
- 創(chuàng)建自定義類(Facade 的實現(xiàn)類)
- 創(chuàng)建 Facade 類
- Facade 類別名設置
- 創(chuàng)建 ServiceProvider
- 加載 ServiceProvider
創(chuàng)建自定義類(Facade 的實現(xiàn)類)
我們現(xiàn)在 app\Facades 目錄下創(chuàng)建 PaymentGateway\Payment 類落包。
<?php
//app\Facades\\PaymentGateway\Payment.php
namespace App\Facades\PaymentGateway;
class Payment
{
public function get(){
return "Facade Test ...";
}
}
創(chuàng)建 Facade 類
創(chuàng)建自定義的 Facade 類,該類繼承基類 Facade摊唇,并重寫基類的 getFacadeAccessor() 函數(shù)咐蝇。
<?php
//app\Facades\\PaymentGateway\Facade\Payment.php
namespace App\Facades\PaymentGateway\Facade;
use Illuminate\Support\Facades\Facade;
class Payment extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'payment';
}
}
Facade 類別名設置
我們可以為自定義的 Facade 類起個別名,只需要將別名和類的對應關系添加到 config/app.php 配置文件中的 aliases 數(shù)組即可巷查。
//config/app.php
'aliases' => [
...
'Payment' => App\Facades\PaymentGateway\Facade\Payment::class,
...
],
創(chuàng)建 ServiceProvider
在 app\Providers 文件夾下創(chuàng)建 PaymentServiceProvider有序,提供“payment”服務。
<?php
//app\Providers\PaymentServiceProvider.php
namespace App\Providers;
use App\Facades\PaymentGateway\Payment;
use Illuminate\Support\ServiceProvider;
class PaymentServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
* $defer為true岛请,表示延遲加載此類
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton('payment', function ($app) {
return new Payment();
});
}
/**
* 設置了延遲啟動旭寿,需要重寫 providers 函數(shù)
* 該方法返回provider中注冊的服務容器綁定別名
*/
public function provides()
{
return ["payment"];
}
}
其中,$defer 設置為 true崇败,是為了提供延遲加載功能盅称,provides() 函數(shù)也是為了輔助延遲加載功能而重寫的。register() 函數(shù)中的服務名“payment”應該與 Facade 類中 getFacadeAccessor() 函數(shù)返回值保持一致后室。
加載 ServiceProvider
為了能加載我們創(chuàng)建的 PaymentServiceProvider微渠,需要在 config/app.php 配置文件中 providers 數(shù)組內(nèi)添加 PaymentServiceProvider。
//config/app.php
'providers' => [
...
App\Providers\PaymentServiceProvider::class,
...
],
最后咧擂,使用如下代碼即可以調(diào)用 Facade 類逞盆。
$value = Payment::get();
總結(jié)
之所以能夠簡單的使用 Cache::get() 操作,是因為程序啟動時自動進行了別名注冊松申,使用 Cache 等價于“Illuminate\Support\Facades\Cache”云芦。另外俯逾,程序在基類 Facade 中自動調(diào)用魔術(shù)函數(shù) __callStatic() 進行了動態(tài)綁定。
門面類列表
下面列出了每個 Facade 舅逸、對應的底層類以及服務容器綁定鍵桌肴。