Laravel 框架門面 Facade 原始碼分析

leoyang發表於2017-05-11

在開始之前,歡迎關注我自己的部落格: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做了一下幾件事:

  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);
    });

  當然如果你願意,你還可以在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 協議》,轉載必須註明作者和本文連結

相關文章