RealTime Facades の仕組み

こんにちはエンジニアの @localdisk です。みなさんはLaravel 5.4 で追加された RealTime Facades をご存知でしょうか? あまり話題にはなっていないし、私自身も使ったことはないのですが、先日 Real Time Facades のことをふと思い出しついでにどのように動いているのか調べてみました

f:id:localdisk:20170829122417p:plain

RealTime Facades?

RealTime Facades とは、Facade として作成されていないクラスを、あたかもFacadeの如く使用できる機能です。もちろん Illuminate\Support\Facades\Facade クラスの継承を必要としません。例えば下記 Twitter クラスの名前空間の最初に Facades を追加するだけで Twitter クラスは Facade として機能します。

<?php

namespace App\Services;

class Twitter
{
  public function tweet($message)
  {
    var_dump($message)
  }
}
<?php
// 名前空間の最初に Facades をつけるだけ
\Facades\App\Services\Twitter::tweet('hello');

このコードが何故動くのか? 少し気になったので調べてみました。結論としては黒魔術というよりは原始的な仕組みを利用した呪術に近いのではないか、と私は思っています。

spl_autoload_register

Composer 登場以前の私たちは spl_autoload_register を利用して、積み重なる require から逃れようと四苦八苦していました。一定年齢以上の PHPer 、あるいはレガシーシステムを運用している PHPer の皆様におかれましては鮮明に思い出せるのではないでしょうか。

Comoser がそれを隠蔽し秩序をもたらしたことで、それらの(忌まわしい)記憶は頭の隅に追いやられていたかもしれませんが、少し間だけ思い出してください。

spl_autoload_register は未定義のクラスのロードを試みる __autoload の実装を特定のメソッド、あるいは関数に実行させるよう登録することができます。

Illuminate\Foundation\AliasLoaderprependToLoaderStack メソッドで spl_autoload_register が使われており AliasLoaderload メソッドが登録されています。このメソッドが RealTime Facades の肝です。

RealTime Facades の呪術

RealTime Facades を読み解くために Illuminate\Foundation\AliasLoader クラスの関わりのある部分を抜粋しました。 load メソッドの最初の if に注目してください。 未定義のクラスが $facadeNamespace で始まっている場合(そう Facades で始まる場合です) loadFacade メソッドをコールします。

そして loadFacade メソッドは ensureFacadeExists の結果を require しています。

ensureFacadeExists は特定のパス・命名規約でクラスを生成しそのパスを返却しています。つまり Facade クラスを生成して require してるだけなのです。読み解いていくとその仕組みは単純といえます。

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

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

  protected function loadFacade($alias)
  {
      require $this->ensureFacadeExists($alias);
  }

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

まとめ

RealTime Facades の仕組みを読み解いてみました。なかなか面白いでしょう? 私は読み解くまでこの機能がどのように実装されているか思いつきませんでした。このようにフレームワークやライブラリのコードを読み解くことはその理解を深め、自分になかった新しい考えを吸収するとてもよい方法だと思います。

みなさんもぜひ試してみてください。

私はもう少し RealTime Facades の使い所を考えてみたいと思います。もしかしたら見つからないかもしれませんが…。*1

*1:Mockが作りやすくなるという恩恵はあるが Facade にせず DI すればいいのでは…という気がしなくもない