LaravelのIoCコンテナ、サービスコンテナに関してまとめ-その1

スポンサーリンク

こんにちは。ファガイです。
本日は、LaravelのIoCコンテナ(サービスコンテナ)に関してまとめます。まとめてると言いつつも、今回は殆どバインディングの話だけです。

使用技術

  • Laravel5

概要

Laravel5で名前が変わったIoCコンテナですが、名前が変わってもやってる内容は基本的に変わりません。(ここでは、認知度が高いIoCコンテナに統一します。)
主に、Laravelのコアの存在なので、少しでも理解しておくと良いです。

IoCコンテナが主にやるのは依存の解決です。これはオブジェクト指向開発をやる上で非常に重要ですし、IoCコンテナを使うことで色々な処理がシンプルになります。
今回、bindメソッドの中身を掘り下げて紹介しています。想像以上に時間がかかりました。

LaravelのApplicationクラス、Containerクラス

この2つのクラスは、Laravelのコアクラスです。vendor/laravel/framework/src/Illuminate/Container/Container.php(Illuminate\Container\Container)vendor/laravel/framework/src/Illuminate/Foundation/Application.php(Illuminate\Foundation\Application)ですね。
このContainer.phpがIoCコンテナの根幹となる部分です。ApplicationクラスがContainerクラスを継承しています。この2つのクラスはLaravelを理解する上で非常に重要です。読めると視界が晴れていきます。

IoCコンテナの例

一旦は、依存性に関してはあまり理解できなくても良いです。(依存性の解決に関しては他の人が色々書いてるのでそちらを見ていただけると・・・!)
どのような動きをするのか、サンプルとして出してみます。

とりあえずバインディングの基本(Facadeからの呼び出し)

\App::bind('FooBar', function($app)
{
    return "テスト";
});
echo app('FooBar');

これだと、

テスト

と出力されます。
上記のソースを見ていただけると分かる通り、別名を付けられます。第1引数にはApplicationクラスのインスタンスが渡されています。

公式ドキュメントだと、$this->app->bindで呼び出していますが同じものです。ヘルパーからも呼べます。(app())

上記は文字列を返していますが、通常であればクラスのインスタンスを返すのが基本で、

\App::bind('Illuminate\Contracts\Auth\Register', 'App\Services\Register');

上記の様に登録しておけば、クラスのコンストラクタやメソッドでIlluminate\Contracts\Auth\Register $registerのように引数に書くことでApp\Services\Registerクラスのインスタンスが$registerに入ります。(コンストラクタインジェクションとか、メソッドインジェクションと言います。Reflectionが上手くやってくれます)

バインディング

バインディングに使用するメソッドに関して紹介します。

bindメソッド

public function bind($abstract, $concrete = null, $shared = false)
$abstract - 呼び出すときの名前(aliasesに指定するための配列も可能)
$concrete - 対象のクラス名かクロージャ(nullの場合は、呼び出すときの名前がそのまま入るので、$abstractが対象のクラス名になる時もあります
$shared   - インスタンスを共有するか(singletonにするかどうかということです)
返却値:無し

先程から使用されているメソッドです。
以降で説明するメソッドは、このbindメソッドを利用して実装されているのでこれが分かってしまえば他のメソッドも理解できます。

中身を見てみましょう。  
abstractは何となくわかるものの、concreteは名前がよく分からなくなるので、とりあえずクロージャーか対象のクラス名が入ってることを覚えておいてください。

bindメソッドのソース

/**
 * Register a binding with the container.
 *
 * @param  string|array  $abstract
 * @param  \Closure|string|null  $concrete
 * @param  bool  $shared
 * @return void
 */
public function bind($abstract, $concrete = null, $shared = false)
{
    // 配列になっていたらaliasに登録する
    if (is_array($abstract))
    {
        list($abstract, $alias) = $this->extractAlias($abstract);

        $this->alias($abstract, $alias);
    }

    // すでに$abstractのキーでインスタンスがあったり、エイリアスがある場合はそれをunsetします
    $this->dropStaleInstances($abstract);

    if (is_null($concrete))
    {
        $concrete = $abstract;
    }

    // クロージャーではない(つまり文字列)であればクロージャーにする。
    // $abstractと$concreteが別名であれば、コンテナのmakeメソッド、同じであればbuildメソッドを呼び出すクロージャーを返します。
    if ( ! $concrete instanceof Closure)
    {
        $concrete = $this->getClosure($abstract, $concrete);
    }

    // bindingsに登録する
    $this->bindings[$abstract] = compact('concrete', 'shared');

    // すでに依存解決(make)済みだった場合、通る
    if ($this->resolved($abstract))
    {
        // 再度インスタンスを登録します。rebindingをしているものもあるので、それも適用させます。
        $this->rebound($abstract);
    }
}

makeメソッドの解説

クロージャーで利用しているmakeメソッドの中身。Applicationクラスのmakeメソッドは端折ってます。
makeメソッドは依存関係解決の際にはほとんど呼ばれてます。

/**
 * Resolve the given type from the container.
 *
 * @param  string  $abstract
 * @param  array   $parameters
 * @return mixed
 */
public function make($abstract, $parameters = [])
{
    // aliasesに$abstractのキーがあれば、その値(binding名)、無かったらそのまま$abstractを返します
    $abstract = $this->getAlias($abstract);

    // すでにインスタンスが存在する場合はそれを返す(singletonやbindSharedで2回目以降等)
    if (isset($this->instances[$abstract]))
    {
        return $this->instances[$abstract];
    }

    // 依存関係の先のクラス名を取得します。
    // (直接メンバ変数のbindingから取得しない理由は、コンテキストバインディング等への対策です)
    $concrete = $this->getConcrete($abstract);

    // build出来るかどうか。(クロージャーや、$concreteと$abstractが同じなら)
    if ($this->isBuildable($concrete, $abstract))
    {
        // 求めているオブジェクトを返却します。クロージャーの場合はクロージャーを発火させた後の値が入ります。
        $object = $this->build($concrete, $parameters);
    }
    else
    {
        // 違う場合は再度makeします。(多分ネストされてる時?)
        $object = $this->make($concrete, $parameters);
    }

    // extenderが存在すれば、それを適用させます。
    foreach ($this->getExtenders($abstract) as $extender)
    {
        $object = $extender($object, $this);
    }

    // singleton等、共有する必要がある場合はinstancesに登録しておきます。
    if ($this->isShared($abstract))
    {
        $this->instances[$abstract] = $object;
    }

    // resolvingを発火させます。
    $this->fireResolvingCallbacks($abstract, $object);

    $this->resolved[$abstract] = true;

    return $object;
}

buildメソッドの解説

クロージャーで利用しているbuildメソッドの中身。
Reflection周りを理解できてない場合はXdebug等で追わないと厳しいです。

buildStackが存在する理由は主にコンストラクタインジェクションの部分でしょう。
コンストラクタインジェクションを解決する際に、makeメソッドが呼ばれるのでそのスタックとしてbuildStackが用意されています。

/**
 * Instantiate a concrete instance of the given type.
 *
 * @param  string  $concrete
 * @param  array   $parameters
 * @return mixed
 *
 * @throws BindingResolutionException
 */
public function build($concrete, $parameters = [])
{
    // クロージャーだったらそのまま呼び出して戻す
    if ($concrete instanceof Closure)
    {
        return $concrete($this, $parameters);
    }

    // リフレクションのインスタンスを作る
    $reflector = new ReflectionClass($concrete);

    // インスタンス化が可能かどうか確認(abstractクラスとかだとインスタンス化出来ないので)
    if ( ! $reflector->isInstantiable())
    {
        $message = "Target [$concrete] is not instantiable.";

        throw new BindingResolutionException($message);
    }

    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    // コンストラクタが存在しない場合はそのままnewしてインスタンスを返す
    if (is_null($constructor))
    {
        array_pop($this->buildStack);

        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    // $parametersで指定がされていた場合、引数の変数名 => 値の形で配列で$parametersが返ってきます。
    // 指定がない場合は空の配列が返ります。
    $parameters = $this->keyParametersByArgument(
        $dependencies, $parameters
    );

    // ここで引数の依存関係を解決し、解決したクラスをReflectionClassで返します。コンストラクタインジェクションの機能はここで動いています。
    $instances = $this->getDependencies(
        $dependencies, $parameters
    );

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

bindIfメソッド

/**
 * Register a binding if it hasn't already been registered.
 *
 * @param  string  $abstract
 * @param  \Closure|string|null  $concrete
 * @param  bool  $shared
 * @return void
 */
public function bindIf($abstract, $concrete = null, $shared = false)

bindIfメソッドはすでにbindingされていたらbindせず、bindingされていなければbindします。
引数はbindメソッドと同じです。

singletonメソッド

/**
 * Register a shared binding in the container.
 *
 * @param  string  $abstract
 * @param  \Closure|string|null  $concrete
 * @return void
 */
public function singleton($abstract, $concrete = null)

singletonメソッドは名前の通りです。インスタンスを共有します。
bindShareメソッドも存在しますが、bindShareメソッドはバージョンによる変化を見る限り、下位互換です。Laravel5で使用するのであればsingletonを利用しましょう。

instanceメソッド

/**
 * Register an existing instance as shared in the container.
 *
 * @param  string  $abstract
 * @param  mixed   $instance
 * @return void
 */
public function instance($abstract, $instance)

instanceメソッドは、すでにインスタンス化されているオブジェクトをbindingする際に利用します。
すでにインスタンス化されているオブジェクトを利用するため、基本的にsingletonとして動作します。

コンテキストバインディングの利用

Laravel5からは、コンテキストバインディングが実装されています。
Laravelエキスパート養成読本から良い記述サンプルがあるので、これを引用させていただきます。

App::when('Tama')   // Tamaクラスが生成されるとき
->needs('Animal')   // AnimalインタフェースのDI要求がきたら
->give('Cat');      // CatクラスをDIします。

これはApp::makeでは使用せず、主にインジェクションに利用されます。
クラスごとに、注入されるクラスを振り分けることが出来ます。

まとめ

IoCコンテナに関して、調べていきましたが非常に興味深い内容でした。
特に現状見たものだとコンストラクタインジェクションの仕組みが上手く出来てると感じたので、もしかしたら記事にするかもしれません。

ではではー。

コメント

タイトルとURLをコピーしました