いい加減サービスコンテナをよく知らずに使うのはよくないな〜ということでLaravelコアのコードを読みつつ、記事を書いて投げてみました。同じようなLaravel初心者の方の参考になれば幸い...
ただ、僕もまだまだ未熟ですので、意図の読み違いなどは普通にあり得ると思います。間違いなどありましたらご指摘くだされば幸いです
基礎知識: サービスコンテナ
DIコンテナとも。Dependency Injection パターンと併用して、依存性を1箇所に集める。
Laravelアプリケーション内における様々なインスタンスの生成を担う。Laravelでは文字通り、これがないとアプリケーションが始まらない。Laravelのコアと言って良い。
FQCNは Illuminate\Foundation\Application
クラスで、Laravel中ではよく$app
というプロパティとして現れる。
Illuminate\Foundation\Application
クラスはIlluminate\Container\Container
クラスを継承しており、コンテナの結合処理などを担うのはこちらのクラスなので、最初はこっちを見ることになる。
用語
結合(bind)
キーと解決処理をペアでコンテナに登録すること。
結合にはbind
、singleton
、instance
などのメソッドを使う。
呼び出し時に使用されるキーには大抵FQCNが使用されるが、極端な話文字列であればなんでも良い。ファサードの場合はわかりやすく短い文字列が使われやすい(DBファサードならdb
とか)。
具象クラスの場合、Laravelは勝手にそのクラスを探し出しインスタンス化してくれるので、結合処理を書く必要性は特にない(はず)。言い換えると、後述の処理内容を見ればわかりますが、コンテナに結合されていないキーで問い合わせた場合、そのキーと一致するクラスを探し、インスタンスを生成しようとする処理が走ることになる。
結合処理は大抵、サービスプロバイダ内で行われる。
解決(resolve)
結合されたインスタンス化の方法を元に、インスタンスを生成すること。
明示的な解決には基本的にmake
メソッドを使用する。
実際の処理
doc以外のコメントは省略しています。
build
による具象クラスの解決処理
順序的にはまずbuild
メソッドの、実際に与えられたクラス名からインスタンスを生成する役割を理解しておくと良さそう。ReflectionClass
とReflectionParameter
に関してはわからない方はPHPマニュアルを参照してください。
/**
* Instantiate a concrete instance of the given type.
*
* @param string $concrete
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
// $concreteがクロージャなら、実行してその結果を返す。
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
重要なのは、リフレクションを使った処理以降の部分。後述するbind
の処理と合わせて理解して欲しい部分ですが、build
メソッドの$concrete
には、具象クラスのFQCNか、解決方法となるクロージャが渡されることが想定されている。
よって、まずインスタンス化できない( = abstract class or interface)場合、notInstantiable
メソッドでBindingResolutionException
を投げる。
具象クラスのFQCNが渡されている(インスタンス化が可能である)場合、$buildStack
の末尾に$concrete
をコピーしたのち、$concrete
のコンストラクタを取得する。
ここでコンストラクタが取得できない場合(=定義されていない場合)、Laravelでは他のクラスに依存していないことを意味するようで(Laravelコアではコンストラクタインジェクションを使う前提なのですね)、そのまま$concrete
をインスタンス化して返却する。
コンストラクタが取得できた場合、他のクラスへの依存関係をもっていると判断し、コンストラクタの引数を配列として取得、それをresolveDependencies
メソッドの引数に与え実行、その結果依存関係をもつクラスのインスタンス群である$instances
が返される。
resolveDependencies
メソッドの実装はコンテキストによる結合とかも絡んできて長くなるので今回はばっさり省略したいところですが、その処理の途中で、引数のタイプヒントがクラスだった場合、そのインスタンス化を行うresolveClass
メソッドの中でmake
メソッドが呼ばれるということだけは、後述の処理の関係もあり押さえておいた方が良い。
/**
* Resolve a class based dependency from the container.
*
* @param \ReflectionParameter $parameter
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
} catch (BindingResolutionException $e) {
if ($parameter->isOptional()) {
return $parameter->getDefaultValue();
}
throw $e;
}
}
最後に、その$instances
を引数として与えた上で、ReflectionClass::newInstanceArgs
メソッドを実行。結果として、$concrete
に指定されたクラスのインスタンスが依存性を解決した上で生成、返却され、build
メソッドの処理が終了する。
bind
, make
による結合と解決
bind
による結合
結合の基本となるbind
の処理から。
/**
* Register a binding with the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
docを見るとわかる通り、$concrete
には、クロージャか文字列が渡されることが想定されている。クロージャなら解決方法となるインスタンスの生成処理、文字列ならFQCNだと思って良い。
bind
の処理の順序としては、まずdropStaleInstances
メソッドで同じキーで登録されているインスタンスとエイリアスをunset
する。
/**
* Drop all of the stale instances and aliases.
*
* @param string $abstract
* @return void
*/
protected function dropStaleInstances($abstract)
{
unset($this->instances[$abstract], $this->aliases[$abstract]);
}
もし$concrete
に何も引数が渡されなかった場合、デフォルトのnull
が使用されることになり、$concrete
には$abstract
の文字列がそのままコピーされる。
(この$abstract
に抽象クラスのFQCNを渡しておきながら$concrete
に何の解決方法も渡さなかった場合、 この処理が走った上でbuild
に処理が移るため、例外が投げられてしまう。)
次に、$concrete
がクラス名の場合、getClosure
メソッドを使ってインスタンスを生成するためのクロージャを作成する。
/**
* Get the Closure to be used when building a type.
*
* @param string $abstract
* @param string $concrete
* @return \Closure
*/
protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = []) use ($abstract, $concrete) {
if ($abstract == $concrete) {
return $container->build($concrete);
}
return $container->resolve(
$concrete, $parameters, $raiseEvents = false
);
};
}
その後、compact
で配列を作成し、$bindings
プロパティに格納する。
compact
はPHPの標準関数であり、変数名とその値から連想配列を作成する。
makeによる解決
解決処理に使われるmake
メソッドは、resolve
メソッドを呼んでその結果を返しているだけ。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
resolveによる抽象クラスの解決処理
前述の通り、抽象クラスの場合はbuild
メソッドによるインスタンス化はできない。
そこで解決方法をクロージャとして用意し、resolve
メソッドの内部でそれを取り出し、改めてbuild
に処理を委譲し、インスタンスを返却する。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
やりすぎなぐらいなんか色々やってますが、ここで重要と思われるのは真ん中辺り以降の処理。
まず$parameters
を$with
プロパティに一旦コピーした後、getConcrete
メソッドで解決方法となるクロージャを取得する。結合が存在しない場合は$abstract
が具象クラスのFQCNであると判断し、そのまま返却する。
/**
* Get the concrete type for a given abstract.
*
* @param string $abstract
* @return mixed $concrete
*/
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// 結合されたクロージャを取り出す。
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
// bindされていない場合、そのまま $abstract を返す。
return $abstract;
}
そして、isBuildable
メソッドを呼び、インスタンス化をbuild
メソッド、make
メソッドのどちらに任せるかを決定する。$concrete
と$abstract
が同じ文字列であるか、$concrete
がクロージャである場合、build
メソッドに処理が移り、インスタンス化が行われる。
/**
* Determine if the given concrete is buildable.
*
* @param mixed $concrete
* @param string $abstract
* @return bool
*/
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
そしてそのどちらでもなかった場合、make
メソッドに処理が移り、その中で再度resolve
メソッドが呼ばれる。つまり、「ネストした依存」をすべてbuild
メソッドで解決できるまで、この処理は再帰的に繰り返されることになる。
「ネストした依存」の解決の一例を示すものとしてわかりやすいのが下記のテストケース。
関係する抽象クラスの解決方法を用意し、コンストラクタインジェクションを使用しておけば、依存先のクラスも含めてすべて依存を解決した上でインスタンス化してくれることがわかる。
use PHPUnit\Framework\Testcase;
class ContainerTest extends TestCase
{
// ...
public function testNestedDependencyResolution()
{
$container = new Container;
$container->bind(IContainerContractStub::class, ContainerImplementationStub::class);
$class = $container->make(ContainerNestedDependentStub::class);
$this->assertInstanceOf(ContainerDependentStub::class, $class->inner);
$this->assertInstanceOf(ContainerImplementationStub::class, $class->inner->impl);
}
// ...
}
interface IContainerContractStub
{
//
}
class ContainerImplementationStub implements IContainerContractStub
{
//
}
class ContainerImplementationStubTwo implements IContainerContractStub
{
//
}
class ContainerDependentStub
{
public $impl;
public function __construct(IContainerContractStub $impl)
{
$this->impl = $impl;
}
}
class ContainerNestedDependentStub
{
public $inner;
public function __construct(ContainerDependentStub $inner)
{
$this->inner = $inner;
}
}
一旦ここまで。
タグとエイリアス、拡張、コンテキストによる結合などは次回以降(があれば)触れたいと思います。
Laravelコアむずかしい。。。