7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravel ソースコードリーディング サービスコンテナ編 part.1 - bindとmake、buildとresolve -

Last updated at Posted at 2020-03-21

いい加減サービスコンテナをよく知らずに使うのはよくないな〜ということでLaravelコアのコードを読みつつ、記事を書いて投げてみました。同じようなLaravel初心者の方の参考になれば幸い...

ただ、僕もまだまだ未熟ですので、意図の読み違いなどは普通にあり得ると思います。間違いなどありましたらご指摘くだされば幸いです:pray:

基礎知識: サービスコンテナ

DIコンテナとも。Dependency Injection パターンと併用して、依存性を1箇所に集める。
Laravelアプリケーション内における様々なインスタンスの生成を担う。Laravelでは文字通り、これがないとアプリケーションが始まらない。Laravelのコアと言って良い。
FQCNは Illuminate\Foundation\Applicationクラスで、Laravel中ではよく$appというプロパティとして現れる。
Illuminate\Foundation\ApplicationクラスはIlluminate\Container\Containerクラスを継承しており、コンテナの結合処理などを担うのはこちらのクラスなので、最初はこっちを見ることになる。

用語

結合(bind)

キーと解決処理をペアでコンテナに登録すること。
結合にはbindsingletoninstanceなどのメソッドを使う。

呼び出し時に使用されるキーには大抵FQCNが使用されるが、極端な話文字列であればなんでも良い。ファサードの場合はわかりやすく短い文字列が使われやすい(DBファサードならdbとか)。

具象クラスの場合、Laravelは勝手にそのクラスを探し出しインスタンス化してくれるので、結合処理を書く必要性は特にない(はず)。言い換えると、後述の処理内容を見ればわかりますが、コンテナに結合されていないキーで問い合わせた場合、そのキーと一致するクラスを探し、インスタンスを生成しようとする処理が走ることになる。

結合処理は大抵、サービスプロバイダ内で行われる。

解決(resolve)

結合されたインスタンス化の方法を元に、インスタンスを生成すること。
明示的な解決には基本的にmakeメソッドを使用する。

実際の処理

doc以外のコメントは省略しています。

buildによる具象クラスの解決処理

順序的にはまずbuildメソッドの、実際に与えられたクラス名からインスタンスを生成する役割を理解しておくと良さそう。ReflectionClassReflectionParameterに関してはわからない方はPHPマニュアルを参照してください。

Container.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メソッドが呼ばれるということだけは、後述の処理の関係もあり押さえておいた方が良い。

Container.php
/**
     * 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の処理から。

Container.php
    /**
     * 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する。

Container.php
    /**
     * 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メソッドを使ってインスタンスを生成するためのクロージャを作成する。

Container.php
    /**
     * 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メソッドを呼んでその結果を返しているだけ。

Container.php
    /**
     * 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に処理を委譲し、インスタンスを返却する。

Container.php
    /**
     * 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であると判断し、そのまま返却する。

Container.php
    /**
     * 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メソッドに処理が移り、インスタンス化が行われる。

Container.php
    /**
     * 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メソッドで解決できるまで、この処理は再帰的に繰り返されることになる。

「ネストした依存」の解決の一例を示すものとしてわかりやすいのが下記のテストケース。
関係する抽象クラスの解決方法を用意し、コンストラクタインジェクションを使用しておけば、依存先のクラスも含めてすべて依存を解決した上でインスタンス化してくれることがわかる。

tests/Container/ContainerTest.php

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コアむずかしい。。。

7
1
0

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
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?