文章簡單介紹了 Facade 門面,總結(jié)了其工作原理,并介紹了 Facade 的使用方法。
介紹
Facade 為應(yīng)用服務(wù)容器中綁定的類提供了一個“靜態(tài)”接口。Laravel 內(nèi)置了很多 Facade,幾乎可以用來訪問 Laravel 中所有的服務(wù)。Laravel 的 Facade 作為服務(wù)容器中底層類的“靜態(tài)代理”,相比于傳統(tǒng)靜態(tài)方法,F(xiàn)acade 提供了簡潔且豐富的語法同時,還帶來了更好的可測試性和擴展性。Laravel 的所有 Facade 都定義在 “Illuminate\Support\Facades”命名空間下,我們也可以自己定義新的 Facade。
Facade 加載原理
為了更好的理解 Facade 的工作原理,我們從以下幾個方面進行介紹:
- Facade 配置文件
- 加載 RegisterFacades 類
- 注冊 Facade 服務(wù)
- 解析 Facade 服務(wù)
Facade配置文件
Facade 的配置文件保存在 config/app.php 中,主要用來保存所有的 Facade 別名,也即是把 Facade 和別名的對應(yīng)關(guān)系存放在 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 服務(wù)的注冊工作由“Illuminate\Foundation\Bootstrap\RegisterFacades”類完成,RegisterFacades 類是在啟動應(yīng)用程序的過程中加載的。因此,我們首先來看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服務(wù)容器獲得一個kernel類的實例,(Illuminate\Contracts\Http\Kernel是一個接口,
* 真正生成的實例是App\Http\Kernel類)
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
/**
* laravel中所有功能服務(wù)的注冊加載
* 通過調(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各個功能服務(wù)的注冊啟動。
$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)
{
//設(shè)置request請求的對象實例
$this->app->instance('request', $request);
//清楚'request'對應(yīng)的服務(wù)對象實例
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 容器中設(shè)置了 request 請求的對象實例,并且清楚了 Facade 中 request 請求對應(yīng)的服務(wù)對象實例。接下來我們來看 bootstrap() 函數(shù)
//Illuminate\Foundation\Http\Kernel.php
/**
* Bootstrap the application for HTTP requests.
* 啟動引導(dǎo)(reauests驅(qū)動)
*
* @return void
*/
public function bootstrap()
{
//判斷引導(dǎo)是否已經(jīng)被啟動
if (! $this->app->hasBeenBootstrapped()) {
/**
* 使用Application類的bootstrapWith()函數(shù)啟動引導(dǎo)
* 參數(shù):bootstrapers數(shù)組。
*/
$this->app->bootstrapWith($this->bootstrappers());
}
}
/**
* The bootstrap classes for the application.
* 引導(dǎo)類,起引導(dǎo)作用的類
*
* @var array
*/
protected $bootstrappers = [
//載入服務(wù)器環(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ù)首先判斷引導(dǎo)類是否已經(jīng)啟動,如果沒有啟動,則啟動引導(dǎo)類。引導(dǎo)類數(shù)組包含 RegisterFacades 類。啟動引導(dǎo)類是調(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']:對應(yīng)綁定的類為 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)用相應(yīng)的 bootstrap() 函數(shù)。因此,RegisterFacades 類也即是在此時調(diào)用。
注冊 Facade 服務(wù)
下面我們來看 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)
{
//清除所有的門面對象服務(wù)實例
Facade::clearResolvedInstances();
//設(shè)置門面對象的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 對象服務(wù)實例
- 設(shè)置門面對象的 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.
* 設(shè)置自動加載方法
*
* @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 版本新出的功能,叫做實時門面服務(wù)。下半部分是 class_alias() 函數(shù)利用別名映射數(shù)組將別名映射到真正的門面類中去。例如,我們使用別名類 Cache 時,程序會通過 AliasLoader 類的 load() 函數(shù)為“Illuminate\Support\Facades\Cache::class”類創(chuàng)建一個別名 Cache,所以我們在程序里使用別名 Cache 就是使用“Illuminate\Support\Facades\Cache”類。
解析 Facade 服務(wù)
以 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)用綁定到門面對應(yīng)的服務(wù)對象實例來執(zhí)行
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
//返回當前門面對應(yīng)的服務(wù)對象實例
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
//執(zhí)行服務(wù)對象實例相應(yīng)的方法
return $instance->$method(...$args);
}
當運行 Cache::get() 函數(shù)時,門面類 Cache 類里并沒有 get() 函數(shù),此時 PHP 會自動調(diào)用魔術(shù)函數(shù) __callStatic()。魔術(shù)函數(shù) __callStatic() 首先獲得 Facade 的服務(wù)對象實例,然后調(diào)用對象的 get() 函數(shù)。下面我們來看如何獲得 Facade 的對象實例。
//Illuminate\Support\Facades\Facade.php
/**
* Get the root object behind the facade.
* 返回當前門面對應(yīng)服務(wù)對象的實例
*
* @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 對應(yīng)的對象實例
*
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
//如果是對象,則直接返回
if (is_object($name)) {
return $name;
}
//$resolvedInstance數(shù)組中 $name 對應(yīng)的對象實例是否存在,如果存在,直接返回
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”對應(yīng)的服務(wù)實例。resolveFacadeInstance() 函數(shù)主要是從 Laravel 服務(wù)容器 static::$app[$name] 中解析出相應(yīng)的服務(wù)?!癱ache”服務(wù)是應(yīng)用程序初始化時,在類“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 類別名設(shè)置
- 創(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 類別名設(shè)置
我們可以為自定義的 Facade 類起個別名,只需要將別名和類的對應(yīng)關(guān)系添加到 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”服務(wù)。
<?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();
});
}
/**
* 設(shè)置了延遲啟動,需要重寫 providers 函數(shù)
* 該方法返回provider中注冊的服務(wù)容器綁定別名
*/
public function provides()
{
return ["payment"];
}
}
其中,$defer 設(shè)置為 true,是為了提供延遲加載功能,provides() 函數(shù)也是為了輔助延遲加載功能而重寫的。register() 函數(shù)中的服務(wù)名“payment”應(yīng)該與 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 、對應(yīng)的底層類以及服務(wù)容器綁定鍵。