在開始之前,歡迎關注我自己的部落格:www.leoyang90.cn
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...
這篇文章我們開始講laravel框架中的門面Facade,什麼是門面呢?官方文件:
Facades(讀音:/fəˈsäd/ )為應用程式的服務容器中可用的類提供了一個「靜態」介面。Laravel 自帶了很多 facades ,幾乎可以用來訪問到 Laravel 中所有的服務。Laravel facades 實際上是服務容器中那些底層類的「靜態代理」,相比於傳統的靜態方法, facades 在提供了簡潔且豐富的語法同時,還帶來了更好的可測試性和擴充套件性。
什麼意思呢?首先,我們要知道laravel框架的核心就是個Ioc容器即服務容器,功能類似於一個工廠模式,是個高階版的工廠。laravel的其他功能例如路由、快取、日誌、資料庫其實都是類似於外掛或者零件一樣,叫做服務。Ioc容器主要的作用就是生產各種零件,就是提供各個服務。在laravel中,如果我們想要用某個服務,該怎麼辦呢?最簡單的辦法就是呼叫服務容器的make函式,或者利用依賴注入,或者就是今天要講的門面Facade。門面相對於其他方法來說,最大的特點就是簡潔,例如我們經常使用的Router,如果利用服務容器的make:
App::make('router')->get('/', function () {
return view('welcome');
});
如果利用門面:
Route::get('/', function () {
return view('welcome');
});
可以看出程式碼更加簡潔。其實,下面我們就會介紹門面最後呼叫的函式也是服務容器的make函式。
我們以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()函式。首先先看看如何獲得物件例項的:
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()。在這個函式裡重點就是
return static::$resolvedInstance[$name] = static::$app[$name];
我們看到,在這裡利用了\$app也就是服務容器建立了“router”,建立成功後放入$resolvedInstance作為快取,以便以後快速載入。
好了,Facade的原理到這裡就講完了,但是到這裡我們有個疑惑,為什麼程式碼中寫Route就可以呼叫Illuminate\Support\Facades\Route呢?這個就是別名的用途了,很多門面都有自己的別名,這樣我們就不必在程式碼裡面寫use Illuminate\Support\Facades\Route,而是可以直接用Route了。
為什麼我們可以在laravel中全域性用Route,而不需要使用use Illuminate\Support\Facades\Route?其實奧祕在於一個PHP函式:class_alias,它可以為任何類建立別名。laravel在啟動的時候為各個門面類呼叫了class_alias函式,因此不必直接用類名,直接用別名即可。在config資料夾的app檔案裡面存放著門面與類名的對映:
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
...
]
下面我們來看看laravel是如何為門面類建立別名的。
啟動別名Aliases服務
說到laravel的啟動,我們離不開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());
}
前兩句是在laravel的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函式:
```php
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,這個函式我們應該很熟悉了,在自動載入中這個函式用於解析名稱空間,在這裡用於解析別名的真正類名。
別名Aliases服務
我們首先來看看被註冊到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);
});
當然如果你願意,你還可以在alias陣列為門面新增一個別名對映"PaymentGateway" => "use Facades\App\Services\PaymentGateway",這樣就不用寫這麼長的名字了。
那麼這麼做的原理是什麼呢?我們接著看原始碼:
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';
}
}
就是這麼簡單!!!
門面的原理就是這些,相對來說門面服務的原理比較簡單,和自動載入相互配合使得程式碼更加簡潔,希望大家可以更好的使用這些門面!
本作品採用《CC 協議》,轉載必須註明作者和本文連結