依存性注入と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()・singleton()によるインターフェースの結び付けは、サービスコンテナの基本的な使い方です。
Laravelのサービスプロバイダには、これ以外にも状況に応じて異なる実装を渡す仕組みなど、より高度な機能があります4。
これらの高度な活用は、本ガイドの範囲を超えるため扱いません。
まとめ
- コンストラクタの型宣言が具象クラスであれば、Laravelのサービスコンテナは何も設定せずに自動解決する
- 型宣言がインターフェースの場合は、
bind()で実装クラスとの対応をあらかじめ登録する必要がある bind()は解決のたびに新しいインスタンスを作り、singleton()は最初の1回だけ作って使い回す- 第6章で見た「実装を切り替えてもコードを変更しなくてよい」という利点は、この結び付けの登録によって実現される
次に読む
次章では、例外設計とバリデーションを扱います。 カスタム例外クラスと、Form Requestによるリクエストの形式チェックを見ていきます。
練習問題
次の状況で、コンテナへの登録は必要でしょうか。理由とともに答えてください
Notificationのコンストラクタが、具象クラスEmailChannelを型宣言しているNotificationのコンストラクタが、インターフェースNotificationChannelを型宣言している
解答例
- 不要:
EmailChannelは具象クラスなので、コンテナはコンストラクタを見てそのままnewできる - 必要:
NotificationChannelはインターフェースなので、どの実装クラスを渡すかbind()で登録しておかないと、コンテナは解決できない