調査環境
この記事は以下のバージョンのLaravelを対象に調査を行いました.
laravel 5.0
古めのバージョンなので, 最新の実装とは異なっている可能性があります.
(事実異なっていました)
その点をご注意ください.
簡単な例から入るFacadeの調査
ある日, キミはLaravelを使った開発中にログ出力を行いたくなりました.
そして, マニュアルを読んだりグーグル様の啓示を受けたりした結果, 以下のような記述にたどり着きました.
<?php
use Log;
...
class SampleController extends Controller
{
public function getHoge()
{
Log::info('info message');
....
見ての通り, \Logクラスをuseし, その静的メソッドを呼び出すだけで, ログ出力を行うことができます.
実に, 簡単ですね.
これで, ログ出力については心配することなく開発を続けていけるでしょう.
しかし, 謎が一つ残ります.
この\Logクラスとはいったい何者なのでしょうか?
Facadeについて
いきなり結論なのですが, \Logとは\Illuminate\Support\Facades\Logのことでした.
(なぜ後者の長い名前のクラスに対してLogという簡潔なクラス名でアクセスできるかというのは, Aliasの項で説明します)
しかし, このクラスの実装を見てみると
protected static function getFacadeAccessor()
{
return 'log';
}
というメソッドが一つ定義されているだけで, 処理の本体は親クラスの\Illuminate\Support\Facades\Facadeに書かれているようです.
次に親クラスを見てみると, Mockなどいろいろ気になる名前が付いたメソッド群の中に,
__callStaticメソッドが定義されていることに気が付きます.
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
switch (count($args))
{
case 0:
return $instance->$method();
case 1:
return $instance->$method($args[0]);
case 2:
return $instance->$method($args[0], $args[1]);
case 3:
return $instance->$method($args[0], $args[1], $args[2]);
case 4:
return $instance->$method($args[0], $args[1], $args[2], $args[3]);
default:
return call_user_func_array(array($instance, $method), $args);
}
}
__callStatic
メソッドは, __call
の静的メソッド版で, 未定義の静的メソッドが呼び出された際に
呼び出されるメソッドです.
実装を見てみると, getFacadeRoot
メソッドで実インスタンスを取得して, そのインスタンスの対応する
メソッドを実行しようとしているようです.
getFacadeRoot
メソッドの実装も見てみると
/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
/**
* Resolve the facade root instance from the container.
*
* @param string $name
* @return mixed
*/
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];
}
どうやら, FacadeクラスのgetFacadeAccessorメソッドから返される名前をキーにアプリケーションの
DIコンテナの中から登録されたインスタンスを探しているようです.
(さりげなくgetFacadeAccessorでオブジェクトを返すようにしておけば, コンテナ経由しないでそのオブジェクトが返ってくるようになっているようです)
改めて, LogFacadeの実装を見てみると...
protected static function getFacadeAccessor()
{
return 'log';
}
まさに今話題に出た, getFacadeAccessorメソッドが定義されており, コンテナにlog
という名前で
登録されたインスタンスを実態として扱うようになってますね.
ここまでの話を念頭に, オリジナルのサービスクラスとそれに対するFacadeを作るには
- サービスクラスを作る
- AppProviderクラスかどこかで1.のサービスクラスのインスタンスをDIコンテナに登録する
- 対応するFacadeクラスを作成し, getFacadeAccessorメソッドで2.の登録名を返すようにする
とすれば, Facadeの静的メソッド経由で実インスタンスのメソッドを呼び出すことができるようになります.
が、このFacadeを実際に使おうと思ったら
use \MyApp\Facades\MyServiceFacade
という長ったらしい記述をしないといけません.
これをLogFacade
を使う時のように
use MyService;
とシンプルに記述するにはどうすればよいか, は次のAliasの項で説明します.
Aliasについて
laravelを実際に使っている人ならば, 設定ファイルであるapp.php
に以下のような記述があることを
目にしたことがあると思われます.
'aliases' => [
'App' => 'Illuminate\Support\Facades\App',
'Artisan' => 'Illuminate\Support\Facades\Artisan',
...
'Log' => 'Illuminate\Support\Facades\Log',
'Mail' => 'Illuminate\Support\Facades\Mail',
...
いかにもそれっぽい記述ですね.
では, この設定を使っている部分を探してみましょう.
> find ./vendor/laravel/framework/src/Illuminate -name '*.php' -type f | xargs grep aliases
./vendor/laravel/framework/src/Illuminate/Container/Container.php: * The registered type aliases.
...
./vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php: * The array of class aliases.
./vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php: protected $aliases;
./vendor/laravel/framework/src/Illuminate/Foundation/AliasLoader.php: * @param array $aliases
...
grepをかけてみると, いくつかのファイルでaliasesという名前をつかっているようですが,
その中でもAliasLoader.php
というファイルがいかにもあやしげなので, これについてより詳しく見てみます.
ざっと見たところ, 肝になっているのは次のメソッドのようです
/**
* Prepend the load method to the auto-loader stack.
*
* @return void
*/
protected function prependToLoaderStack()
{
spl_autoload_register(array($this, 'load'), true, true);
}
spl_autoload_register
関数は, マニュアルによると
spl_autoload_register
(PHP 5 >= 5.1.2, PHP 7)
spl_autoload_register — 指定した関数を __autoload() の実装として登録するbool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )
指定した関数を、spl が提供する __autoload キューに登録します。 キューがまだアクティブになっていない場合は、まずアクティブにします。
もしあなたのコード中に __autoload() 関数が存在するのなら、 それを明示的に __autoload キューに登録しなければなりません。 なぜなら、spl_autoload_register() は、 spl_autoload() あるいは spl_autoload_call() によって __autoload() 関数のエンジンキャッシュを効率的に置き換えるからです。
複数の autoload 関数が必要となる場合でも spl_autoload_register() は対応できます。この関数は autoload 関数のキューを作成し、 定義された順にそれを実行していきます。一方 __autoload() は、一度しか定義できません。
とあり, クラスが見つからなかった時に呼び出されるautoload関数を登録するための関数のようです.
関数そのものではなく, 関数のQueueを登録することで, 各ライブラリが他のライブラリやアプリケーションの実装を気にすることなく
自分の都合だけを考えたautoload関数を用意できるようになっているのですね.
そして, ここでautoload関数として登録されているAutoLoader#loadメソッドは以下の通り.
/**
* Load a class alias if it is registered.
*
* @param string $alias
* @return void
*/
public function load($alias)
{
if (isset($this->aliases[$alias]))
{
return class_alias($this->aliases[$alias], $alias);
}
}
class_alias
という, これまた見覚えのない関数が出てきたので, またマニュアルで調べてみると
class_alias
(PHP 5 >= 5.3.0, PHP 7)
class_alias — クラスのエイリアスを作成するbool class_alias ( string $original , string $alias [, bool $autoload = TRUE ] )
alias という名前のエイリアスを、 ユーザー定義のクラス original に対して作成します。 エイリアスは、元のクラスとまったく同一のものとなります。
まさに, 今回調べていた機能そのものが出てきました.
Classに対するAliasの機能は, フレームワーク側が実装したのではなく, すでにPHPの機能として提供されていたんですね.
この機能は, 日頃アプリケーションを使う上では使うことはなさそうですが(古いコードに対するテストコードを書きたいときは便利かな?)
自前でフレームワーク的なものを作るうえではいろいろと使い道がありそうです.
ここでポイントになるのは, loadメソッドをautoload関数として登録する際に, spl_autoload_registerの
第三引数にtrueを渡して, Queueの先頭に登録している点で,
これによって, (おそらくcomposerのautoload関数によって)既に登録されているであろう通常のautoload関数より先に実行されるようになっていることでしょう.
試していないのですが, おそらくすでに存在するがコードを書き換えることができないクラス(ライブラリのクラスとか)と
同じ名前のAliasを登録することで, クラスの実装を乗っ取るような曲芸もできるのではないでしょうか.
(お勧めできませんが)
さて, alias機能の実現方法が明らかになったので, あとは呼び出し元を逆順にたどって終わりにしようかと思います.
上記, prependToLoaderStack
メソッドは同クラスのregister
メソッドから呼び出されており
/**
* Register the loader on the auto-loader stack.
*
* @return void
*/
public function register()
{
if ( ! $this->registered)
{
$this->prependToLoaderStack();
$this->registered = true;
}
}
AutoLoader#register
は\Illuminate\Foundation\Bootstrap\RegisterFacades#bootstrap
メソッドで呼び出され,
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance($app['config']['app.aliases'])->register();
}
RegisterFacades#bootstrap
は\Illuminate\Foundation\Http\Kernel#bootstrap
メソッドの中で呼び出され
/**
* The bootstrap classes for the application.
*
* @var array
*/
protected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];
/**
* Bootstrap the application for HTTP requests.
*
* @return void
*/
public function bootstrap()
{
if ( ! $this->app->hasBeenBootstrapped())
{
$this->app->bootstrapWith($this->bootstrappers());
}
}
そして, このKernel#bootstrap
メソッドは, /public/index.php
の中で呼び出されている
Kernel#handle
メソッドの中で呼び出されているようです.
$kernel = $app->make('Illuminate\Contracts\Http\Kernel');
$response = $kernel->handle(
» $request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);