依存性注入とLaravelのコンテナ — 自動解決とインターフェースの結び付け
第10章では、OrderControllerのコンストラクタにOrderPlacementServiceを渡しました。
このとき、new OrderPlacementService(...)のようなコードはどこにも書いていません。
Laravelがコンストラクタの型宣言を見て、必要なインスタンスを自動的に生成しているためです。
この章では、この自動解決の仕組みと、第6章で見たインターフェースへの依存をLaravelでどう実現するかを見ていきます。
コンストラクタでの自動解決
第10章のOrderPlacementServiceは、StockCheckerという具象クラスに依存していました。
class OrderPlacementService
{
public function __construct(
private readonly StockChecker $stockChecker,
) {
}
}
OrderControllerのコンストラクタでOrderPlacementServiceを型宣言すると、Laravelのサービスコンテナが次のように動きます1。
OrderControllerを作るためにOrderPlacementServiceが必要だと気づくOrderPlacementServiceを作るためにStockCheckerが必要だと気づくStockCheckerにはコンストラクタ引数がないため、そのままnew StockChecker()するStockCheckerを渡してnew OrderPlacementService($stockChecker)するOrderPlacementServiceを渡してnew OrderController($orderPlacementService)する
具象クラス同士の依存であれば、何も設定しなくてもこの解決が自動的に行われます。
インターフェースを渡すには結び付けが必要
第6章では、OrderServiceがLoggerというインターフェースに依存する例を見ました。
class OrderService
{
public function __construct(
private readonly Logger $logger,
) {
}
}
Loggerはインターフェースであり、実体を持ちません。
コンテナがOrderServiceを作ろうとしてLoggerにたどり着いても、FileLoggerとCloudWatchLoggerのどちらをnewすればよいか、コンテナには判断できません。
そこで、インターフェースと実装クラスの対応を、あらかじめコンテナに教えておきます。
// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(Logger::class, FileLogger::class);
}
}
register()メソッドの中でbind()を呼び、「Loggerが要求されたらFileLoggerを渡す」という対応をコンテナに登録します2。
これで、OrderServiceのコンストラクタでLoggerを型宣言するだけで、コンテナがFileLoggerのインスタンスを自動的に渡してくれます。
ログの出力先をCloudWatchに変えたくなったときは、OrderServiceのコードを一切変更せず、bind()の2番目の引数をCloudWatchLogger::classに差し替えるだけで済みます。
第6章で見た「OrderServiceのコードを変更せずに実装を切り替えられる」という利点は、このbind()の登録によって実現されています。
singletonでインスタンスを共有する
bind()は、解決のたびに新しいインスタンスを作ります。
アプリケーション全体で同じインスタンスを使い回したい場合は、singleton()を使います3。
$this->app->singleton(Logger::class, FileLogger::class);
bind()とsingleton()のどちらも、「インターフェースが要求されたときに、どの実装クラスを渡すか」を登録する点は同じです。
違いは、呼び出すたびに新しいインスタンスを作るか、最初の1回だけ作って使い回すかです。
注入先ごとに違う実装を渡す
bind()で登録した対応は、どのクラスから要求されても同じ実装を返します。
「ふだんのログはファイルに書き、注文処理だけCloudWatchへ流したい」のように注入先で実装を変えたい場合は、contextual bindingを使います4。
// 既定はファイル
$this->app->bind(Logger::class, FileLogger::class);
// OrderService が要求する Logger だけ CloudWatch へ
$this->app->when(OrderService::class)
->needs(Logger::class)
->give(fn () => new CloudWatchLogger());
when()に注入先のクラス、needs()に要求される型、give()に渡す実装を書きます。
when()には配列を渡せるので、同じ実装を使うクラスをまとめて指定できます。
give()のクロージャが返したものが、その注入先に渡ります。
bind()の登録はそのまま残り、指定した注入先でだけ上書きされます。
使いどころは限られます。
効くのは、どの実装を使うかが注入先のクラスで決まる場合だけです。
注入先が同じまま、設定ファイルの値やリクエストの内容によって実装を変えたいなら、contextual bindingでは表せません。give()のクロージャに分岐を書くこともできますが、その分岐はコンテナの登録に埋もれて追いにくくなります。
その場合は実装を選ぶファクトリを書き、そのファクトリをbind()で登録します。
本章で扱う範囲
ここまでで見たbind()・singleton()・contextual bindingが、サービスコンテナの中心的な使い方です。
これ以外に、同じ役割を持つ複数の実装をまとめて解決するtag()とtagged()、解決済みのインスタンスに後から手を加えるextend()があります4。
どちらも登録の書き方が変わるだけで、「インターフェースを要求すれば実装が渡る」という仕組みは変わりません。
まとめ
- コンストラクタの型宣言が具象クラスであれば、Laravelのサービスコンテナは何も設定せずに自動解決する
- 型宣言がインターフェースの場合は、
bind()で実装クラスとの対応をあらかじめ登録する必要がある bind()は解決のたびに新しいインスタンスを作り、singleton()は最初の1回だけ作って使い回す- 注入先によって実装を変えたいときは
when()->needs()->give()で登録する。実行時の値で切り替えるならファクトリを書く - 第6章で見た「実装を切り替えてもコードを変更しなくてよい」という利点は、この結び付けの登録によって実現される
次に読む
次章では、例外設計とバリデーションを扱います。 カスタム例外クラスと、Form Requestによるリクエストの形式チェックを見ていきます。
練習問題
次の状況で、コンテナへの登録は必要でしょうか。理由とともに答えてください
Notificationのコンストラクタが、具象クラスEmailChannelを型宣言しているNotificationのコンストラクタが、インターフェースNotificationChannelを型宣言している
解答例
- 不要:
EmailChannelは具象クラスなので、コンテナはコンストラクタを見てそのままnewできる - 必要:
NotificationChannelはインターフェースなので、どの実装クラスを渡すかbind()で登録しておかないと、コンテナは解決できない