Laravelのリアルタイムファサードの仕組みをソースコードを読みながら説明します

Laravel PHP フレームワーク プログラミング

Laravelのリアルタイムファサードがなかなか面白いと思ったので実装部分についてソースコードと共に解説します。

リアルタイムファサードがなぜ「リアルタイム」と呼ばれるのか、この記事を読む事で理解できるかと思います。

Laravelのファサード、特にリアルタイムファサードを使うにあたってはデメリット等ありますが、この記事はあえてそこには触れません。

この記事を読んで仕組みを理解したうえで、使うべきかどうか考えてみましょう。

リアルタイムファサードとは何か

Laravelのリアルタイムファサードに関して、公式ドキュメントでは以下の説明をしています。

リアルタイムファサードを使用すれば、アプリケーション中のどんなクラスもファサードのように使用できます。活用法を示すため、新しいテストの手法を取ってみます。たとえば、Podcastモデルがpublishメソッドを持っているとしましょう。しかし、ポッドキャストを公開(publish)するには、Publisherインスタンスを注入する必要があるとします。

ファサード 7.x Laravel

リアルタイムファサードとは「どんなクラスもファサードのように使用できる」仕組み。とのことです。

例えばHTTPクライアントライブラリのGuzzleもファサード化できます。

以下はHomeControllerというコントローラーの中でGuzzleのClientをファサード化して使う例です。

<?php

namespace App\Http\Controllers;

// 通常は「GuzzleHttp\Client」とする所を、
// 頭に「Facades\」をつけて、
// 「Facades\GuzzleHttp\Client」として読み込む。
use Facades\GuzzleHttp\Client;

class HomeController extends Controller
{
    public function index()
    {
        // 通常は以下のように、Guzzleクライアントのインスタンスを生成する
        // $client = new Client();
        // $res = $client->request('GET', 'http://google.com');

        // リアルタイムファサードにすることで、
        // Guzzleのインスタンスを生成せずにメソッドを静的関数のように呼び出すことができる。
        $res = Client::request('GET', 'http://google.com');
        dd($res);
    }
}

ーーー 実行結果
GuzzleHttp\Psr7\Response {#313 ▼
  -reasonPhrase: "OK"
  -statusCode: 200
  -headers: array:12 [▶]
  -headerNames: array:12 [▶]
  -protocol: "1.1"
  -stream: GuzzleHttp\Psr7\Stream {#311 ▶}
}

このようにクラスをロードする際にuse文で「Facades\」というプレフィックスをつけるだけで、そのクラスがファサードになるというなんとも不思議な機能となっています。

Facadeが持っているモック化の機能も使えるようになります。

use文でFacadeをプレフィックスにつけると、そのクラスはFacadeになる。

Laravelであらかじめ用意されているファサード

リアルタイムファサードの仕組みを見る前に、Laravelの通常のファサードをおさらいします。

Laravelであらかじめ用意されているファサードは、Laravelが用意しているサブシステム(クラス)のメソッドを静的関数のように呼び出すことができるものです。

ファサード自体はアプリケーションとサブシステムをつなぐだけのシンプルなものです。

例を見ていきます。

cacheファサードの例

LaravelのCacheファサードを例にとると、以下のようなイメージになります。

Cacheのファサードの中身は以下のようになっています。

<?php

namespace Illuminate\Support\Facades;

class Cache extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'cache';
    }
}

Facadeクラスを継承し、getFacadeAccessor()のみ実装されたシンプルなクラスがLaravelのファサードの正体です。

getFacadeAccessor()メソッドはファサードがアクセスするクラスの実体へのパスか、サービスコンテナに登録されたエイリアス名を返すように実装します。

Cacheファサードの場合はエイリアス名「cache」を返しています。

「cache」というエイリアス名はIlluminate/Foundation/Application.phpで以下のようにエイリアス登録されています。

~前略~
    public function registerCoreContainerAliases()
    {
        foreach ([
~略~
            'cache'  => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
~攻略~

getFacadeAccessor()の戻り値は親クラスのFacade内で参照され、ファサードの静的メソッドにアクセスされた時にCacheManagerのメソッドが呼ばれる仕組みなっています。

このあたりはリアルタイムファサードではなくファサードそのものの理解が必要になりますが、長くなってしまうのでこの記事では詳細は扱いません。

ファサードパターン

アプリケーションから直接CacheManagerクラスを使わずCacheファサードを使うことで、Cacheの機能の実装を切り替え(CacheManagerから何か別のクラスに差し替える)が簡単になります。

このような方法で機能へのインターフェースを提供する設計パターンを「ファサードパターン」と呼びます。

自分で作るファサード

Laravelで用意されているFacadeクラスを継承する事で、自分で簡単にファサードを作る事ができます。

PHPのHTTPクライアントライブラリをファサードを介して使えるようにする例を考えてみます。

実装イメージは以下のようになります。

これでアプリケーションからはGuzzleを使っている事をほとんど意識せずにHTTPクライアントの機能が使えるようになります。

Laravelで定義済のCacheファサードと基本的には同じです。

ファサードは下記のように実装します。

use Illuminate\Support\Facades\Facade;

/**
 * @see \GuzzleHttp\Client
 */
class Client extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'GuzzleHttp\Client';
    }
}

Laravelで定義済みのCacheファサードではgetFacadeAccessor()で「cache」というエイリアス名を返していましたが、今回はGuzzleHttpのnamespaceを表す「GuzzleHttp\Client」を直接返しています。

エイリアス名を使うかどうかの違いはありますが、基本的には定義済ファサードも自作ファサードも同じです。

LaravelのファサードのgetFacadeAccessor()はエイリアス名を返しても良いし、実態クラスのネームスペースを返しても良い。

リアルタイムファサードの仕組み

通常のファサードは上記で説明してきたように、Facadeクラスを継承したクラスを用意する必要があります。

リアルタイムファサードはこの「Facadeクラスを継承したクラス」を作っていないのに「use Facades\GuzzleHttp\Client;」という一文を宣言しただけでファサードが使えるようになっています。
いったいどういう事でしょうか。

答えは「Facadeクラスを継承したクラスが自動生成されている」です。

もう一度リアルタイムファサードを使った例を見てみます。

<?php

namespace App\Http\Controllers;

use Facades\GuzzleHttp\Client;

class HomeController extends Controller
{
    public function index()
    {
        $res = Client::request('GET', 'http://google.com');
        dd($res);
    }
}

この処理を一度動かすと、storageディレクトリに以下のような形でファイルが作られます。

Laravelアプリケーションディレクトリ
┗ storage/
  ┗ framework/
    ┗ cache/
      ┗ facade-8f4eb59ea777457dbec43c1ed1324c3f744b1e6c.php

「8f4eb59ea777457dbec43c1ed1324c3f744b1e6c.php」は以下のような内容になっています。

<?php

namespace Facades\GuzzleHttp;

use Illuminate\Support\Facades\Facade;

/**
 * @see \GuzzleHttp\Client
 */
class Client extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'GuzzleHttp\Client';
    }
}

上記の内容は「自分で作るファサード」の項目で作った「GuzzleHttp\Client」のファサードと全く同じ内容です。

リアルタイムファサードとは、「アプリケーションの実行時にリアルタイムにファサードを生成する」という意味であったと理解する事が出来ます。

リアルタイムファサードはどのように作られるのか

リアルタイムファサードのファイルはPHPのautoloaderによって作られる仕組みとなっています。

autoloaderについて分からない場合は下記などを参照してください。

PHP: クラスのオートローディング – Manual
【図解】phpのautoloaderとは?使用例と概要を説明。

Laravelの起動処理の中でautoloaderに登録されているAliasLoader::load()を追ってみましょう。

ファイル中のコメントは少し書き換えています。

以下のような処理をしている事をまずはイメージしておくと読みやすいかと思います。

  • オートローダーに「Facades\」で始まるnamespaceが渡されると、リアルタイムファサード専用の処理が動く。
  • 渡されたnamespaceとファサードのテンプレートを元に、リアルタイムファサードクラスを作る。
  • 作ったファイルのパスを返す。
<?php

namespace Illuminate\Foundation;

class AliasLoader
{
~略~
    /**
     * The namespace for all real-time facades.
     *
     * @var string
     */
    protected static $facadeNamespace = 'Facades\\';

~略~

    /**
     * Laravel起動時にオートローダーに登録される処理。
     */
    public function load($alias)
    {
        // $facadeNamespaceは「'Facades\\'」という値になっています。
        // 渡された$aliasが「Facades\」で始まっていたら「loadFacade()」を呼び出します。
        // 「loadFacade()」リアルタイムファサード用のオートローディング処理になります。
        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);
        }
    }

    /**
     * 指定されたエイリアスのリアルタイムファサードを読み込みます。
     */
    protected function loadFacade($alias)
    {
        require $this->ensureFacadeExists($alias);
    }

    /**
     * リアルタイムファサードクラスを作ってパスを返します。
     * 既にファイルがあったら作らずにパスを返します。
     */
    protected function ensureFacadeExists($alias)
    {
        // エイリアス名(namespace名)のsha1ハッシュ値を生成してファイル名として使っています。
        // storage_path()はLaravelアプリケーションstorageディレクトリ以降のパス名を生成します。
        // file_exists()ですでにファイルが存在するか確認し、
        // すでにファイルがあればここで生成した$pathを返して終わります。
        if (file_exists($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) {
            return $path;
        }

        // 「facade.stub」はファサードクラスのテンプレートのテキストファイルです。
        // このテンプレートを元に、リアルタイムファサードクラスを作ります。
        file_put_contents($path, $this->formatFacadeStub(
            $alias,
            file_get_contents(__DIR__.'/stubs/facade.stub')
        ));

        return $path;
    }

    /**
     * facade.stubを元にリアルタイムファサードクラスを生成します。
     * facade.stub内の文字列を以下のように置き換えます。
     * 「DummyNamespace」→ ファサードにしたいクラスのnamespace
     * 「DummyClass」→ ファサードにしたいクラスのクラス名
     * 「DummyTarget」→ getFacadeAccessor()で返すクラス名
     */
    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
        );
    }

~略~

    /**
     * オートローダーの登録処理。
     * load()メソッドをオートローダーとして登録している。
     */
    protected function prependToLoaderStack()
    {
        spl_autoload_register([$this, 'load'], true, true);
    }
~略~
}

リアルタイムファサードクラスの元になっているfacade.stubは以下のようになっています。「DummyNamespace」「DummyTarget」「DummyClass」という文字が置換対象になります。

<?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のオートローダーの理解が必要になるので、少し難しいかもしれません。

自分でファサードクラスを沢山作ってみたり、ソースコードを一部変えてみるなど、手を動かしながら理解を深めるという方法もあります。

リアルタイムファサードの仕組みが気になった方の参考になればと思います。

コメント/ピンバック

タイトルとURLをコピーしました