📦

【Laravel】サービスコンテナ・サービスプロバイダ・依存性注入の仕組みを整理した

2024/01/11に公開

スクラップにダラダラ書いていたものを、整理した記事です。

#0 書いた背景

サービスコンテナやサービスプロバイダ、ファサードなどフレームワークの仕組みを特に意識することなく開発はでき、それがフレームワークの良さとも感じる、ただ、そろそろちゃんと理解しようと思ったとき色々な概念や単語が交錯して障壁を感じた。

これから書く内容は

「サービスコンテナはオブジェクト間の依存解決をやってくれますよ」
「サービスコンテナを使うにはこんなプロセスを経ますよ」
「サービスプロバイダとサービスコンテナの関係はこんなですよ」
「サービスコンテナの主なメソッドにはこんなのがありますよ」

という話を並べているであるが、ソースコードまで見ると以下のように出てくる概念や単語が多いので、整理しながらサービスコンテナの機能を追っていくというのが趣旨である。

  • サービスコンテナ
  • 依存性注入
  • 依存関係の解決
  • サービスプロバイダ
  • シングルトン(singleton)
  • バインド(bind)
  • 登録(register)
  • 解決(resolve)
  • 抽象(abstrat)/具体(concrete)

よって結論とかは特にない。

前提として現在最新のLaravel10.x系で話を進める。

#1 Laravelアプリケーションのライフサイクル

https://readouble.com/laravel/10.x/ja/lifecycle.html

上記ではLaravelアプリケーションがHTTPリクエストを受けてレスポンスを返すまでの、ざっくりとした流れ(ライフサイクル)が説明されている。

Laravelアプリケーションが最初にやることとしてはアプリケーション/サービスコンテナのインスタンスを生成することであると書かれている。

では、サービスコンテナとは何なのか。

#2 サービスコンテナの概要と役割

サービスコンテナとはLaravelに備わっている機能のことで、以下のような機能がある。

  1. アプリケーション内のサービスをまとめて管理する
  2. 依存性注入を扱いやすくする


    1に出てきたサービスとは「認証情報を扱う」「ログを記録する」などの機能の単位を指す。

サービスコンテナは読んで字の如くサービスを格納する入れ物のようなものである。
先ほどのサービスをサービスコンテナ内にバインドというプロセスを経て登録しておき、サービスコンテナに詰めたサービスを使用する際には解決(resolve)というプロセスをサービスコンテナに要求する。解決のプロセスを経てによって予め登録したサービスを新しくインスタンス化して受け取ることができる。


2はアプリケーション内のプログラムにある依存の解決を自動でやってくれる。自動で解決できない場合もあるが、そのときは1に書いた方法でバインドのプロセスで依存解決の方法を明示してあげることができる。


ここで1、2と別々に書いてしまったが、「1で解決(resolve)を行う際に2の依存解決を自動でやってくれる」という関係があり、独立した機能というよりはニコイチの関係である。

#3 サービスコンテナまわりの用語整理

登録(register)

サービスコンテナにサービスを登録する行為のことを指す。

Laravel内ではサービスプロバイダ内にregisterという名前メソッドが使われているが、やっていることとしては次に説明するバインドを行なっているのでそちらで説明。

バインド(bind)

ニュアンスとしては先ほどの登録(register)とあまり変わらず、サービスコンテナにサービスを登録することを指す。

「サービスを登録する行為のこと」と申し上げたが、具体的にはbindメソッドで「抽象(ラベルのようなもの)」と「具体(クラス名やクロージャ)」のペアをサービスコンテナに登録している。

ちなみにReadableの説明では、「binding」に「結合」という訳語があてられている。

解決(resolve)

指定したラベルに対応するクラスのインスタンスを、サービスコンテナに予め登録しているものから取り出すことを指す。

ただ、インスタンスを返す際にはサービスコンテナが依存関係の解決も行なっているので、この適切な依存解決プロセス含めて「解決」と呼ぶ方が正しいかもしれない。
(依存解決しなかったらそれはただの「インスタンス化」になるので)

make

サービスコンテナからインスタンスを取り出すためのメソッド名。

    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

つまり、resolveと同じであり、現にmakeメソッドは上記のようにresolveメソッドに処理を丸投げしている。

※ちなみにLaravel5.3まではフレームワークのソースにresolveメソッドは存在せず、makeメソッドに今のresolveの処理が書かれていた。

シングルトン(singleton)

シングルトンという言葉が一般的に指し示すのは「実行時に最初の一度だけインスタンスを生成(new)し、以後は同一のインスタンスを使い回すデザインパターン」のこと。

解決時に新しくインスタンスを生成せずに既存のインスタンスを受け取りたい場合は、登録の段階でsingletonメソッドを使って登録しておく。

先ほど出てきたバインドでは、サービスコンテナが解決を要求された際にクラスのインスタンスを新しく生成(new)して返すが、シングルトンとして登録されている場合はキャッシュされたインスタンスが返される。

abstract(抽象)とconcrete(具体)

ソースコードを見ると$absract$concreteという変数がしばしば使われている。

    public function bind($abstract, $concrete = null, $shared = false)

まず、$concreteの方が分かりやすくクラス名やクロージャなどの後々にインスタンス化したいオブジェクトを扱う。

$abstractという表現はやや分かりづらいが、要は解決したい$concreteを特定するためのラベルのようなものである。

依存性注入と依存関係解決

class UserController extends Controller
{
      public function create($param) {
          $repository = new UserRepository;
          $repository->save($param);
          ...
    }
}

まず「依存」という単語が登場したが、プログラミングにおける依存という単語は「あるプログラムが別のプログラムに依存している」という状態を表す。

class SomeClass {
    private $service;

    public function __construct(UserService $service) {
        $this->service = $service;
    }
}
class SomeClass {
    public function doSomething(UserService $service) {
    }
}

ここで依存性注入(Dependency Injection)とは、上記のような依存したオブジェクトを外部から渡す設計パターンである。DIパターンとも呼ばれる。
(依存性という訳語がややこしい。。。)

例えば先ほどの前者のようにクラスのコンストラクタからや、後者のようにメソッドの引数として注入する方法があるが、いずれにしてもタイプヒンティングによって注入するオブジェクトの型を指定する。

もう1つ、依存解決という言葉も目にするが、Laravelでは上記のタイプヒンティングしたオブジェクトのインスタンスをサービスコンテナが用意してくれる。

このサービスコンテナがやってくれているように、「あるプログラムが依存するプログラムを全て揃うまで取り寄せるプロセス」を「依存解決(依存関係の解決)」と呼ぶ。

【補足メモ】LaravelがDIをどう実現させているか

class SomeClass {
    private $service;

    public function __construct(UserService $service) {
        $this->service = $service;
    }
}

上記は先ほどのコンストラクタインジェクションの例であるが、UserServiceクラスのインスタンスを外部から注入することでSomeClass内で利用することができるようになっている。

ここで、以下のような素朴な疑問が生まれた。

「インスタンス化を行なっているのはLaravelのサービスコンテナによるものであるが、PHPのコンストラクタの機能自体には依存解決する機能はないよね?サービスコンテナを使って依存解決してるのは分かるけど、どこがそれを担ってくれて、それはどうやってトリガーされているの?」

と。

結局DIを実現させるためにはContainerクラスのresolveメソッドを通す必要があり、その中のbuildメソッド内でPHP組み込みのreflectionClassの機能を使ってコンストラクタの情報を参照し、依存性注入を実現している。

resolveメソッドとbuildメソッドについては後ほど追っていく。

https://blog.fagai.net/2016/09/17/laravel-dependency-injection/

#4 サービスプロバイダとは

一言で言うと「サービスの登録や設定を行うための仕組み」となる。


Laravelの全てのコアサービスや独自のアプリケーション(Composer経由でインストールしたものなど)はサービスプロバイダ経由で登録される。

具体的にはregisterメソッド内でbind、もしくはsingletonメソッドを呼び出して特定のサービスやクラスをサービスコンテナに登録している。


以下、実際のソースコードを追ってみる。

config/app.php
    'providers' => ServiceProvider::defaultProviders()->merge([
        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
    ])->toArray(),

サービスプロバイダは/config/app.phpのprovidersに列挙されている。

Laravel8以前はコア機能に関するサービスプロバイダも列挙されていたが、9.x以降はvendor内の\Illuminate\Support\DefaultProvidersに移された。

一例として\Illuminate\Auth\AuthServiceProviderを覗いてみる。

\Illuminate\Auth\AuthServiceProvider
class AuthServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->registerAuthenticator();
        $this->registerUserResolver();
        $this->registerAccessGate();
        $this->registerRequirePassword();
        $this->registerRequestRebindHandler();
        $this->registerEventRebindHandler();
    }

    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', fn ($app) => new AuthManager($app));

        $this->app->singleton('auth.driver', fn ($app) => $app['auth']->guard());
    }

... 省略
}

registerメソッドの中ではサービスコンテナに何かを結合することだけを行わなければなりません。イベントリスナやルート、その他のどんな機能もregisterメソッドの中では決して行ってはいけません。これを守らないと、サービスプロバイダがまだロードしていないサービスを意図せず使ってしまう羽目になるでしょう。
https://readouble.com/laravel/10.x/ja/providers.html

この場合はregisterメソッド内で同クラス内の複数のメソッドを呼び出しているが、いずれにしてもサービスコンテナのbindメソッドかsingletonメソッドが呼び出されているだけであり、上記のルールは守られている。

Illuminate\Foundation\Application
    public function registerCoreContainerAliases()
    {
        foreach ([
            'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
            'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
            'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
// ...多すぎるので省略
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

ちなみにアプリケーションのインスタンス生成時に上記のようなエイリアスの登録を行うが、上記は名前を紐づけているだけであり、インスタンスの登録はサービスプロバイダが担っている。

#5 アプリケーションの起動〜サービスプロバイダ登録の流れ

ついでなのでアプリ立ち上げ〜サービスコンテナ生成、サービスプロバイダの登録までおおまかに追っていきたい。

今回はartisanコマンドからの起動前提に書く。

bootstrap

bootstrap/app.php
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

HTTPリクエストの場合でも、Consoleの場合でも/bootstrap/app.phpが呼び出され、Illuminate\Foundation\Applicationクラスのインスタンスが生成される。

サービスコンテナのインスタンス生成

Illuminate\Foundation\Application
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }

        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
    }

コンストラクタは上記のようになっており、自身($this)をサービスコンテナに登録したり、必ず使うサービスプロバイダを登録したり、コア機能のエイリアスの登録を行なっている。

それぞれ追ったらキリがないので、ここは省略。

Kernelの解決

bootstrap/app.php
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

$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
);

Applicationクラスのインスタンスが生成された後、HttpカーネルとConsoleカーネルがシングルトンとして結合される。

artisan
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

artisanファイルに戻る。

consoleの場合はartisanの処理中でApplicationクラスのインスタンスからmakeメソッドを呼び出し、Illuminate\Contracts\Console\Kernel::classが引数として渡されることによってApp\Console\Kernel::classのインスタンスが解決される。

artisan
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput,
    new Symfony\Component\Console\Output\ConsoleOutput
);

その後Kernelクラスののhandleメソッドがコールされており、ここでartisanコマンド実行時の引数が渡される。

ソースについてはApp\Console\Kernel::classIlluminate\Foundation\Console\Kernelが継承されているのでそちらを参照。


    protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ];

...省略

    public function handle($input, $output = null)
    {
        $this->commandStartedAt = Carbon::now();

        try {
            if (in_array($input->getFirstArgument(), ['env:encrypt', 'env:decrypt'], true)) {
                $this->bootstrapWithoutBootingProviders();
            }

            $this->bootstrap();

            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            $this->reportException($e);

            $this->renderException($output, $e);

            return 1;
        }
    }

Illuminate\Foundation\Console\Kernelのhandleメソッドでは$bootstrappersがロードされており、\Illuminate\Foundation\Bootstrap\RegisterProviders::classも含まれている。

class RegisterProviders
{
    public function bootstrap(Application $app)
    {
        $app->registerConfiguredProviders();
    }
}

RegisterProvidersクラスのbootstrapメソッドでは、ApplicationインスタンスのregisterdConfiguredProvidersメソッドが実行されており、

    public function registerConfiguredProviders()
    {
        $providers = Collection::make($this->make('config')->get('app.providers'))
                        ->partition(fn ($provider) => str_starts_with($provider, 'Illuminate\\'));

        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());
    }

該当メソッドの中身を見ると上記のようになっている。

ここで/config/app.phpのサービスプロバイダリストが読み込まれ、サービスプロバイダの登録が行われる。

#6 サービスコンテナのメソッド

最後にサービスコンテナ内の各メソッドを追っていくことで、どんなことが行われているのかざっくりイメージを掴みたい。

bindメソッド

    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);

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

        if (! $concrete instanceof Closure) {
            if (! is_string($concrete)) {
                throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
            }

            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }
  • やっていることとしては、パラメータに抽象(abstract)と具体(concrete)を取り、それらのペアをApplicationクラス変数の$bindingsに追加することで登録を行う。
  • 第3引数にtrueを渡して実行すると、それらはシングルトンオブジェクトとして登録する。

singletonメソッド

    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }
  • singletonメソッドの実行時にやっていることとしては、bindメソッドの第3引数にtrueを渡して呼び出しているだけである。
  • 普通のバインドとシングルトンでのバインドの解決方法は次項のresolveメソッドを参照。

resolveメソッド

自分の方でコメントを加えて余計にややこしくなっているかもしれないが、やっていることとしては

  • 抽象に対する具体オブジェクトのインスタンス化
    • ※シングルトンオブジェクトを既にインスタンス化している場合は、既存のものを返す。
  • インスタンス化したオブジェクトが特定のオブジェクトに依存している場合はそれらの依存も解決する。

となる。

    protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
// aliases => [
//    "Illuminate\Foundation\Application" => "app"
//      "Illuminate\Contracts\Container\Container" => "app"
//      "Illuminate\Contracts\Foundation\Application" => "app"
//      "Psr\Container\ContainerInterface" => "app"
//      "Illuminate\Auth\AuthManager" => "auth"
//...多いので省略
// ]
//
// 上記のaliasesに$abstractキーが存在すれば、そのaliasを返す。なければ$abstractをそのまま返す。
        $abstract = $this->getAlias($abstract);

// イベントハンドラ(raisEvents)が有効な場合に、解決のプロセスを行う前に実行が必要な関数を処理する。
// グローバルなものと、抽象ごとに設定されたものがある。
        if ($raiseEvents) {
            $this->fireBeforeResolvingCallbacks($abstract, $parameters);
        }

// サービスコンテナにコンテクストバインディングが存在すれば返す。
// 通常のバインディングと違って特定のコンテキクストのみで解決されるように定義されている。
        $concrete = $this->getContextualConcrete($abstract);

// ビルド(オブジェクトをインスタンス化したものを返却するプロセス)が必要であるかを判定
        $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

// インスタンス化されたものが存在してビルドの必要もない場合は、既存のインスタンスを返す。
// シングルトンオブジェクトの要求である場合はここで終了。
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

// オブジェクトの解決時に必要なパラメータを格納しておく。
        $this->with[] = $parameters;

// コンテクストバインディングが解決されなかった場合に、通常のバインディングを解決。
// インスタンスを返す。
        if (is_null($concrete)) {
            $concrete = $this->getConcrete($abstract);
        }

// 具体がビルド可能であるかを判定($concrete===$abstracctであるか※、$concreteがクロージャーである場合※)
// ビルド可能である場合はビルドを行う。
// 具体的には解決先オブジェクトのコンストラクタを参照し、引数にタイプ品ティングがある場合はコンストラクタインジェクションを済ませた状態でインスタンスを生成する。

        $object = $this->isBuildable($concrete, $abstract)
            ? $this->build($concrete)
            : $this->make($concrete);

        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

// オブジェクトがシングルトンとして登録されている、かつ動的なコンテキストがない場合は$instancesにセットすることでキャッシュしておく。
// 再度要求された場合に新たにオブジェクトを生成せずに、キャッシュしたものを返す。
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

// イベントハンドラ(raisEvents)が有効な場合に、実行が必要な関数を処理する。
        if ($raiseEvents) {
            $this->fireResolvingCallbacks($abstract, $object);
        }

// オブジェクトの解決が完了したことを示すためにフラグを立てる。
        $this->resolved[$abstract] = true;

// 配列の末尾を取り除くことで、先ほどオブジェクトの解決のためにセットしておいたパラメータを取り除く。
        array_pop($this->with);

        return $object;
    }

makeメソッド

    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

makeメソッドはresolveメソッドを呼び出して、makeメソッドが受け取った引数をそのまま渡しているだけである。
ちなみに5.3まではresolveメソッドは存在せず、makeメソッドに現在のresolveメソッドと同じ処理が書かれていたが、5.4以降で現在の仕様になっている模様。

$app->make()は残しつつ、ただmakeというメソッド名が分かりづらかったので、resolveというより的確な名前のメソッドに処理を移したのでないかと考えられる。

buildメソッド

依存関係を解決しつつ、インスタンスを返すメソッド。
コンストラクタインジェクションはこのメソッド(特に$this->resolveDependenciesあたり)で行われている。

    public function build($concrete)
    {
// クロージャであれば先ほどの$this->withに格納したパラメータを渡してそのまま実行する。
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

// ReflectionClassを使って解決するオブジェクトの情報を取得。
// 取得できない(そのクラスが存在しない)場合は例外をスロー
        try {
            $reflector = new ReflectionClass($concrete);
        } catch (ReflectionException $e) {
            throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
        }

// オブジェクトがインスタンス化できない場合(※)はnotinstantiableメソッド経由で例外を投げる。
// 問題なければビルドスタックに突っ込む。
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }
        $this->buildStack[] = $concrete;

// コンストラクタの情報を取得
        $constructor = $reflector->getConstructor();

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

            return new $concrete;
        }

// reflectionClassの機能を使ってコンストラクタのパタメータ情報を依存関係として取得
// この情報にはパタメータの名前や型が含まれている。
        $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);
    }

※インスタンス化できないオブジェクトの例としては、

参考にさせて頂いた記事

以上。

Discussion