LaravelのDIコンテナはどう使われているのか

スポンサーリンク

こんにちはー。ファガイです。
今日は知見的なものの共有をしようかなと思います。

この記事を見てるということはLaravelに興味があったり、実際に使っている方だと思いますがLaravelのコアで利用されているサービスコンテナ(DIコンテナ)がどのように作用しているのかを把握されてない方もいると思います。
そのあたりをこの記事では書いていこうと思います。(そう言いつつも、なんか色々網羅した気がします・・・)

はじめに

この記事、結構長いです。解説には必要不可欠な情報ばかり含まれてます。
それを理解して読んでいただければと思います。。。

対象者

  • Laravelを少し触ってきた人
  • DIコンテナがなんとなく分かる人
  • LaravelのDIの仕組みが魔法だと思ってる人

環境

  • Laravel5.3を想定。ただ、Laravel5.1でもほとんど同じソースなので気にせずに。

まずDIって何よ?

Dependency Injectionの略で、依存性の注入という意味です。
簡単にいえばクラスの内部でnewするのではなく、外部で用意してそれを注入するということです。

class A
{
    /**
     * @var Alice
     */
    protected $person;

    /**
     * class A constructor.
     */
    public function __construct()
    {
        $this->person = new Alice();
    }

    /**
     * 話す
     */
    public function say()
    {
        return $this->person->say();
    }
}

class Alice
{
    public function say()
    {
        return "アリスだよ~";
    }
}

$classA = new A();

上記のソースはクラスAを用意していますが、内部でAliceクラスをnewしてしまっています。
この状態では、クラスAはAliceクラスに依存しているといえます。

これを解決するためには、外から注入する必要があります。

class A
{
    /**
     * @var Alice
     */
    protected $person;

    /**
     * class A constructor.
     * @param Alice $person
     */
    public function __construct(Alice $person)
    {
        $this->person = $person;
    }

    /**
     * 話す
     */
    public function say()
    {
        return $this->person->say();
    }
}

class Alice
{
    public function say()
    {
        return "アリスだよ~";
    }
}

$alice = new Alice();
$classA = new A($alice);

これでAliceの生成処理を外に出すことができました。このように外から情報を注入することをDependency Injectionと呼びます。

ステップアップとして、class Aは今の状態だとAliceクラスしか許容することができません。
これでは依存性は解決したわけではないのでインターフェースに分離します。

class A
{
    /**
     * @var PersonInterface
     */
    protected $person;

    /**
     * class A constructor.
     * @param PersonInterface $person
     */
    public function __construct(PersonInterface $person)
    {
        $this->person = $person;
    }

    /**
     * 話す
     */
    public function say()
    {
        return $this->person->say();
    }
}

interface PersonInterface
{
    public function say();
}

class Alice implements PersonInterface
{
    public function say()
    {
        return "アリスだよ~";
    }
}

$alice = new Alice();
$classA = new A($alice);

このようにインターフェースに変更したことによって、クラスAはsayメソッドを実装していればどのようなクラスでも入力を受け付けることが出来るようになりました。

人によってはここまでやったことをDependency Injectionという人もいるかと思いますが、依存性を注入するという意味ではひとつ前の時点でDIといえるかなと思います。(元にDIコンテナではそれが多い気がする)

やったね!

LaravelはDIをどうやってるの?

Laravelにはサービスコンテナ(以前まではIoCコンテナ)という機能でDIコンテナを用意しています。DIコンテナというのはDIをするためのコンテナ、箱のようなものです。DIに関しての情報をコンテナの中に入れておいて、それをDIで注入します。

実際にLaravelはサービスコンテナをどう利用しているの?

実際に利用しているでも、このDIコンテナがどうやってDIやっているのか把握しづらいでしょう。
実は処理の最初からDIを利用しています。だってコアとなるApplicationクラス自体がサービスコンテナです。

では、Laravel5.3のpublic/index.phpファイルを見てみます。(わかりやすいようにコメントを入れてます)

<?php

// 1.オートロードファイルを読み込み
require __DIR__.'/../bootstrap/autoload.php';

// 2.Applicationクラスのインスタンスを生成。各種カーネルクラスの登録作業。
$app = require_once __DIR__.'/../bootstrap/app.php';

// 3.HTTPカーネルをサービスコンテナから作成
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

// 4.HTTPカーネルにRequestを渡してReponseを貰う
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

// 5.Responseヘッダーのセットと、レスポンス内容をechoする
$response->send();

// 6.Middlewareのterminateを実行と各種terminate時のコールバックを実行
$kernel->terminate($request, $response);

この処理の中で見るべきは、2,3,4あたりです。
2の中身はこんな感じになっています。

<?php

// Applicationクラスのインスタンスを作成
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

// カーネルのインターフェース名を実際のクラス名に紐付けてます
// ひも付けているだけで実際に作られるのはmakeされる時。
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class, // こっちがインタフェース
    App\Http\Kernel::class // 実クラス
);
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;

Applicationクラスのインスタンスを生成している処理をみてみます。

public function __construct($basePath = null)
{
    // 1. 自分自身($this)を自分のDIコンテナに登録します。
    $this->registerBaseBindings();

    // 2. 必ず必要になるServiceProviderを登録しています。(Event,Routing)
    $this->registerBaseServiceProviders();

    // 3. DIコンテナにLaravel本体のコア機能の情報を登録しています。
    //    ただし、ここでは名前のひも付けを登録しているだけで実際のコンテナ登録はServiceProviderが対応します。
    $this->registerCoreContainerAliases();

    if ($basePath) {
        // 4. pathを取得できるようにpathの登録を一括で行います。
        //    これもDIコンテナにインスタンス(実際は文字列)を登録しています。
        $this->setBasePath($basePath);
    }
}

このように、最初の処理でもDIコンテナをばりばり使っています。
というか、DIコンテナしか使ってない気がします。

この辺で少しだけ解説しておくとすれば2番目と3番目です。

2番目は特定のServiceProviderのregisterを実行しています。

$this->register(new EventServiceProvider($this));
$this->register(new RoutingServiceProvider($this));

ここで例としてEventServiceProviderを上げておきたいと思います。

$this->app->singleton('events', function ($app) {
    return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
        return $app->make('Illuminate\Contracts\Queue\Factory');
    });
});

ここではsingletonメソッドだけに注目します。
singletonは以下を実行します。

$this->bind($abstract, $concrete, true);

このbindに関しては過去に説明していますが、こちらにも。
こっちは5.3のソースになりますけどね。

public function bind($abstract, $concrete = null, $shared = false)
{
    // 必要ない文字列を削ったりして返しています。
    $abstract = $this->normalize($abstract);
    $concrete = $this->normalize($concrete);

    // 配列になっていたらaliasに登録する
    if (is_array($abstract)) {
        list($abstract, $alias) = $this->extractAlias($abstract);
        $this->alias($abstract, $alias);
    }

    // すでに$abstractのキーでインスタンスがあったり
    // エイリアスがある場合はそれをunsetします
    // ここで言うと、eventsという名前で用意がすでにされていた場合はunsetします
    $this->dropStaleInstances($abstract);

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

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

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

    // すでに依存解決されていた場合
    if ($this->resolved($abstract)) {
        // 再度インスタンスを登録する処理が走ります。
        // なぜこれを呼ぶかといえば、依存解決されていた場合は
        // 以前のインスタンスが返されてしまうので
        // 登録した内容を発火させてインスタンスを入れておく必要があるからです
        $this->rebound($abstract);
    }
}

3番目Aliasの登録はコメントにも書いてあるとおり、あくまでも名前のひも付けをやっているだけです。

public function registerCoreContainerAliases()
{
    // 内容が多いので減らしています
    $aliases = [
        'app'    => ['Illuminate\Foundation\Application', 'Illuminate\Contracts\Container\Container', 'Illuminate\Contracts\Foundation\Application'],
        'events' => ['Illuminate\Events\Dispatcher', 'Illuminate\Contracts\Events\Dispatcher'],
        'router' => ['Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'],
    ];

    foreach ($aliases as $key => $aliases) {
        foreach ($aliases as $alias) {
            $this->alias($key, $alias);
        }
    }
}

aliasメソッドはaliasesに文字列を登録しているだけです。
例えばIlluminate\Events\Dispatcherという名前かIlluminate\Contracts\Events\Dispatcherがmakeメソッドやインジェクションで呼ばれた時に、eventsという名前でコンテナに登録されていないかを確認しに行きます。
(もしなかった場合はエラーになります。何か間違えてlogがないよみたいなエラーに躓いた方もいると思います。それはここで登録しているから処理されたけどlogという名前でコンテナにはまだ登録がないよってことでエラーになってます)

DIを実現させる処理

戻って4番目の$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);を見ます。
これは先程のbootstrap/app.phpの部分でsingletonとして登録してましたよね。
このmakeの処理がDIの要となっているメソッドです。DIを動作させるにはこの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を返します
    // 例えば、Illuminate\Events\Dispatcherが渡された時、eventsという文字列が返されます。
    $abstract = $this->getAlias($this->normalize($abstract));

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

    // 依存関係の先の内容を取得します。
    // メソッドの返り値は基本的にクロージャーですが、バインディングが登録されてない場合等はそのままabstractが返ります。
    // 先ほどのeventsの場合は、bindメソッドで登録をしているクロージャーが返ってきます。
    $concrete = $this->getConcrete($abstract);

    // build出来るかどうか。concreteがクロージャーだった場合や、abstractとconcreteが同じ場合。
    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メソッドを見ていきましょう。
先ほどのeventsであれば、最初のクロージャで終わります。
もし、クラス名でそのクラスがコンストラクタにクラス名を指定していた場合は下に続いていきます。

public function build($concrete, array $parameters = [])
{
    // クロージャーだったらそのまま呼び出して戻す
    if ($concrete instanceof Closure) {
        return $concrete($this, $parameters);
    }

    // これ以降は少しむずかしいかもです。
    $reflector = new ReflectionClass($concrete);

    // インスタンス化が可能かどうか確認
    // (abstractクラスやインターフェースとかだとインスタンス化出来ないので)
    if (! $reflector->isInstantiable()) {
        if (! empty($this->buildStack)) {
            $previous = implode(', ', $this->buildStack);

            $message = "Target [$concrete] is not instantiable while building [$previous].";
        } else {
            $message = "Target [$concrete] is not instantiable.";
        }

        throw new BindingResolutionException($message);
    }

    // スタックにクラス名を入れる
    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    // コンストラクタが存在しない場合はスタックからpopしてnewして返す
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    // コンストラクタのパラメータ情報を取得
    $dependencies = $constructor->getParameters();

    // makeするとき等にパラメータを渡していた場合は、そのパラメータの名前で引数があれば入れて返します。
    $parameters = $this->keyParametersByArgument(
        $dependencies, $parameters
    );

    // コンストラクタインジェクションします
    // コンストラクタの引数を1つずつ取り出して、
    // 1. makeするときにパラメータを渡していた場合はそれが一番優先されて入ります
    // 2. クラス(インターフェース含む)じゃなかった場合はデフォルト値が入れれたら入れる、
    //   入れれなかったらエラー
    // 3. クラス名でmakeメソッドにまた投げます
    $instances = $this->getDependencies(
        $dependencies, $parameters
    );

    // インジェクションの設定が終わったのでpopします
    array_pop($this->buildStack);

    // インスタンスを生成して返します
    return $reflector->newInstanceArgs($instances);
}

あれ、buildStackに入れてる理由ってなんなんだっけ?って思うかもしれません。
これはコンテキストバインディングを利用する際に呼び出し元のクラス名がわからないとコンテキストバインディングでバインディング出来ないからですね。

Reflectionにあまり慣れてない人もいると思うので、こちらを。

<?php

class A{}
interface B{}
class C {
    public function __construct(A $a, B $b, $c) {}
}

$reflection = new ReflectionClass('C');
$constructor = $reflection->getConstructor();
$parameters = $constructor->getParameters();
foreach($parameters as $parameter)
{
    var_dump($parameter->getClass());
}
object(ReflectionClass)#5 (1) {
  ["name"]=>
  string(1) "A"
}
object(ReflectionClass)#5 (1) {
  ["name"]=>
  string(1) "B"
}
NULL

このソースを見ていただけるとわかるかもしれませんが、クラスAとインターフェースBと実際にインジェクション予定のクラスCがあったとします。
クラスCをリフレクションするために、ReflectionClassをnewするときに'C'を渡しています。これはCクラスのオブジェクトでも良いですし、クラスへの完全修飾名でも良いです。
そうすると、ReflectionClassのインスタンスが生成されます。コンストラクタを取得するとコンストラクタに関する情報が取得でき、そのパラメータ情報をvar_dumpで出力しています。
getClassメソッドを実行してみると、Aクラスが渡される予定であること、Bインターフェースを実装したクラスが渡される予定であることと、$cはそもそも変数を指定しただけなのでNULLになります。
これをgetDependenciesメソッドの中でやってます。getClassでとりあえずクラス(インターフェース含む)が取得できたらmakeに通して、クラスの場合は同じように通って行き、インターフェースの場合はエイリアスが指定されているかもしれないのでここで解決される可能性がありますね。

このようにどんどんクラスが解決されていくことがわかるかと思います。これこそがコンストラクタインジェクションです。
ループで回って行って、最終的にはコンストラクタが存在しないでnewで終えるか、コアクラス等で登録されたバインディングが最後に来るのかなと思います。

結局言いたいのは基本的にDIを経由してクラスが生成されること

コントローラとか、DIを魔法みたいにつかえたりしますが、結局のところmakeやbuildしているんですよ。
どこでやってんだよって話ですが、vendor/laravel/framework/src/illuminate/Routing/Router.phpのdispatchToRouteメソッドで、指定したルーティングを処理しています。

/**
 * Dispatch the request to a route and return the response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return mixed
 */
public function dispatchToRoute(Request $request)
{
    $route = $this->findRoute($request);

    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    $this->events->fire(new Events\RouteMatched($route, $request));

    // この中でコントローラーの取得や、メソッドの実行を行って、レスポンスを返しています。
    // コントローラーのクラスはmakeメソッドで取得して
    // メソッドはReflectionしてメソッドインジェクションを実装しています。
    $response = $this->runRouteWithinStack($route, $request);

    return $this->prepareResponse($request, $response);
}

中身どうなってんのよってことで見ましょう。

protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
            ->send($request)
            ->through($middleware)
            ->then(function ($request) use ($route) {
                return $this->prepareResponse(
                    // ここのrunメソッドがコントローラーを作ってる処理です
                    $request, $route->run($request)
                );
            });
}
public function run(Request $request)
{
    $this->container = $this->container ?: new Container;

    try {
        if ($this->isControllerAction()) {
            // コントローラーを実行してます
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

ここでコントローラーをrunしてー

protected function runController()
{
    return (new ControllerDispatcher($this->container))->dispatch(
        $this, $this->getController(), $this->getControllerMethod()
    );
}

@を分離してコントローラーのクラス名だけ取得してmake

protected function getController()
{
    list($class) = explode('@', $this->action['uses']);

    if (! $this->controller) {
        $this->controller = $this->container->make($class);
    }

    return $this->controller;
}

getControllerMethodは@のあとの部分の名前を取得するだけー

ControllerDispatcherのdispatchメソッドで呼び出す予定のメソッドのメソッドインジェクションと
実際に実行をしますー

public function dispatch(Route $route, $controller, $method)
{
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller, $method
    );

    if (method_exists($controller, 'callAction')) {
        return $controller->callAction($method, $parameters);
    }

    return call_user_func_array([$controller, $method], $parameters);
}

といったところですね。駆け足ですがこんな感じです。

まとめ

LaravelのDIの中身の処理は自動的に何かやってくれるのかと思いきや、実際は非常にちまちましたことを一生懸命やっていることがわかります。
これが色々と作用することでフレームワークとして動作しているのですー。すごいですー。

中身を読もうとしないでマニュアルやブログ記事だけに頼ってる人も、この機会に内部ソースを読んでみてはどうでしょうか。

ではではー。

コメント

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