Laravel框架門面Facade源碼分析

前言

在開始以前,歡迎關注我本身的博客: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

Facade 的原理

咱們以 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 了。框架

別名 Aliases

爲何咱們能夠在 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 是如何爲門面類建立別名的。

啓動別名Aliases服務

說到 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 作了一下幾件事:

  1. 清除了 Facade 中的緩存

  2. 設置 Facade 的 Ioc 容器

  3. 得到咱們前面講的 config 文件夾裏面 app 文件 aliases 別名映射數組

  4. 使用 aliases 實例化初始化 AliasLoader

  5. 調用 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);
});

那麼這麼作的原理是什麼呢?咱們接着看源碼:

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\PaymentGatewayreplacements 第一項是門面命名空間,將 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';
    }
}

就是這麼簡單!!!

結語

門面的原理就是這些,相對來講門面服務的原理比較簡單,和自動加載相互配合使得代碼更加簡潔,但願你們能夠更好的使用這些門面!

相關文章
相關標籤/搜索