LoginSignup
49

More than 5 years have passed since last update.

Laravelの便利機能, FacadeとAliasについて調べてみた

Last updated at Posted at 2017-07-26

調査環境

この記事は以下のバージョンの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を作るには

  1. サービスクラスを作る
  2. AppProviderクラスかどこかで1.のサービスクラスのインスタンスをDIコンテナに登録する
  3. 対応する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);

参考

class-alias
spl-autoload-register

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49