Laravel の特徴として良く挙げられるファサードクラスの仕組みをコードで見てみました。
Laravel のファサードクラス
Laravel では、Input::get()
やRoute::get()
など、クラスメソッドでフレームワークの機能を利用する場面が多くあります。
これは一見すると、InputやRouteクラスで提供されているクラスメソッドを実行しているだけに見えますが、これらのクラスにメソッドの実装があるわけではなく、実際はIoCコンテナに格納されているインスタンスのメソッドを実行しています。
例えば、InputであればIlluminate\Http\Request
クラス、RouteであればIlluminate\Routing\Router
のインスタンスメソッドが実行されます。
これらのインスタンスは、IoCコンテナにて管理されており、ファサードクラスのクラスメソッドが実行されると、IoCコンテナから定められたインスタンスを取得して、そのインスタンスメソッドを実行する仕組みになっています。
ファサードクラスの利用により、コードの記述がシンプルになります(ここは意見が分かれるところですが)。また、ファサードクラスでのメソッド呼び出しでは、自身では処理を行わずに、IoCコンテナに格納されたインスタンスなどに委譲します。つまり、ファサードクラスが呼び出すインスタンスを差し替えることで実際に処理を行うクラスを変えることができます。
Laravel のファサードクラスでは、shouldReceive()
メソッドにて、テスト時に実行するインスタンスをモックオブジェクトと差し替える機能が標準で用意されています。
ファサードクラス実行の流れ
ではRoute::get()
を例にして、ファサードクラスがどのように実行されるか見て行きましょう。
- 1) ファサードクラスの解決
- 2) ファサードクラスのクラスメソッド実行
- 3) IoCコンテナからインスタンス取得
- 4) インスタンスメソッドの実行
1. ファサードクラスの解決
まずRouteクラスの解決を行います。実は、Routeクラスは、フレームワークでは定義されていません。
では、なぜRouteクラスのクラスメソッドが実行できるのかというと、class_alias()
関数にて、存在するクラスの別名として、Routeを定義するためです。
この処理はIlluminate\Foundation\AliasLoader
にて行います。
Routeクラスへのアクセスがあると、クラス定義を探すためにオートローダが起動します。オートローダには、Illuminate\Foundation\AliasLoader
とComposerのものが登録されているのですが、はじめにIlluminate\Foundation\AliasLoader
が実行されます。
Illuminate\Foundation\AliasLoader
でオートロードを行う箇所が以下です。
$this->aliases
に対象のクラス名(ここではRoute)があれば、class_alias()
で、クラス別名を設定します。
public function load($alias) { if (isset($this->aliases[$alias])) { return class_alias($this->aliases[$alias], $alias); } }
$this->aliases
は、app/config/app.php
で定義しているaliases
キーの内容がセットされています。
Routeは、下記のように設定されているため、Illuminate\Support\Facades\Route
への別名として設定されます。
'Route' => 'IlluminateSupportFacadesRoute',
次にIlluminate\Support\Facades\Route
について、オートロードによる読み込みが行われるので、Route
はIlluminate\Support\Facades\Route
の別名として利用できるようになります。
2. Routeクラスのクラスメソッドを実行
次に、Routeクラスのget()
メソッドを実行します。
Routeクラス(Illuminate\Support\Facades\Route
)を見ると、get()
というメソッドは存在しません。
Illuminate\Support\Facades\Route
の基底クラスであるIlluminate\Support\Facade\Facade
には、__callStatic()
メソッドが定義されているため、このメソッドが呼ばれます。
__callStatic()
メソッドが下記です。
public static function __callStatic($method, $args) { $instance = static::resolveFacadeInstance(static::getFacadeAccessor()); 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); } } }
3. IoC コンテナからインスタンス取得
__callStatic()
メソッドの先頭では、resolveFacadeInstance()
メソッドを実行して、IoC コンテナから処理対象のインスタンスを取得します。
まず、static::getFacadeAccessor()
を実行して、どのインスタンスを取得するかを決定します。このメソッドは各ファサードクラスで定義されており、Routeクラスの場合は下記のようになっています。ここでは、router
という文字列を返しています。
このようにファサードクラスではgetFacadeAccessor()
の戻り値で、実行するインスタンスを指定します。
protected static function getFacadeAccessor() { return 'router'; }
このrouter
を引数にresolveFacadeInstance()
メソッドを実行して、インスタンスを取得します。
resolveFacadeInstance()
メソッドの実装は以下です。
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]; }
引数で与えられた$name
(ここではrouter
)がオブジェクトであれば、そのまま返します。もし、すでにファサードクラスで解決済ならそのインスタンスを返します。そうでなければ、IoCコンテナからインスタンスを取得して返します。
IoCコンテナからインスタンスを取得した場合は、Fasadeクラスのクラス定数$resolvedInstance
にキャッシュされる仕組みになっているので、常に同じインスタンスが利用されます。
もし実行インスタンスを変えたい場合はswap()
やclearResolvedInstance()
を使うと良いでしょう。
4. インスタンスメソッドの実行
- で取得したインスタンスについて、指定されたインスタンスメソッド(この場合
get()
)を実行します。
さいごに
フレームワークのソースから Laravel のファサードクラスの仕組みを見てきました。
ファサードクラスの作り方として、ServiceProvider の構築が良く手順に含まれていますが、実はファサードクラスを作る上ではこれは必須ではありません。ファサードクラスが IoC コンテナからインスタンスを取得するため、そのインスタンスを事前にコンテナに設定するために ServiceProvider を利用することが多いだけです。
ファサードクラスを自作する際もこうした動きを知っていると作りやすいですね。
なお、Laravel のファサードクラスは、GoF のファサードパターンとは異なり、IoCコンテナにあるインスタンスを透過的に呼び出す仕組みとなっており、便利なサービスロケータと言った方がイメージしやすいかもしれません。( ちなみに、Laravel のファサードと GoF のファサードパターンは違うものだそうです。)