メインコンテンツまでスキップ

依存性注入と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

  1. OrderControllerを作るためにOrderPlacementServiceが必要だと気づく
  2. OrderPlacementServiceを作るためにStockCheckerが必要だと気づく
  3. StockCheckerにはコンストラクタ引数がないため、そのままnew StockChecker()する
  4. StockCheckerを渡してnew OrderPlacementService($stockChecker)する
  5. OrderPlacementServiceを渡してnew OrderController($orderPlacementService)する

具象クラス同士の依存であれば、何も設定しなくてもこの解決が自動的に行われます。

インターフェースを渡すには結び付けが必要

第6章では、OrderServiceLoggerというインターフェースに依存する例を見ました。

class OrderService
{
public function __construct(
private readonly Logger $logger,
) {
}
}

Loggerはインターフェースであり、実体を持ちません。 コンテナがOrderServiceを作ろうとしてLoggerにたどり着いても、FileLoggerCloudWatchLoggerのどちらをnewすればよいか、コンテナには判断できません。

そこで、インターフェースと実装クラスの対応を、あらかじめコンテナに教えておきます。

✅ Good: インターフェースと実装クラスを結び付ける
// 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

✅ Good: 1つのインスタンスを共有する
$this->app->singleton(Logger::class, FileLogger::class);

bind()singleton()のどちらも、「インターフェースが要求されたときに、どの実装クラスを渡すか」を登録する点は同じです。 違いは、呼び出すたびに新しいインスタンスを作るか、最初の1回だけ作って使い回すかです。

本章で扱う範囲

ここまでで見たbind()singleton()によるインターフェースの結び付けは、サービスコンテナの基本的な使い方です。 Laravelのサービスプロバイダには、これ以外にも状況に応じて異なる実装を渡す仕組みなど、より高度な機能があります4。 これらの高度な活用は、本ガイドの範囲を超えるため扱いません。

まとめ

  • コンストラクタの型宣言が具象クラスであれば、Laravelのサービスコンテナは何も設定せずに自動解決する
  • 型宣言がインターフェースの場合は、bind()で実装クラスとの対応をあらかじめ登録する必要がある
  • bind()は解決のたびに新しいインスタンスを作り、singleton()は最初の1回だけ作って使い回す
  • 第6章で見た「実装を切り替えてもコードを変更しなくてよい」という利点は、この結び付けの登録によって実現される

次に読む

次章では、例外設計とバリデーションを扱います。 カスタム例外クラスと、Form Requestによるリクエストの形式チェックを見ていきます。

練習問題

次の状況で、コンテナへの登録は必要でしょうか。理由とともに答えてください
  1. Notificationのコンストラクタが、具象クラスEmailChannelを型宣言している
  2. Notificationのコンストラクタが、インターフェースNotificationChannelを型宣言している

解答例

  1. 不要: EmailChannelは具象クラスなので、コンテナはコンストラクタを見てそのままnewできる
  2. 必要: NotificationChannelはインターフェースなので、どの実装クラスを渡すかbind()で登録しておかないと、コンテナは解決できない

Footnotes

  1. 出典: Laravel公式ドキュメント — Service Container。型宣言による自動解決が、多くのオブジェクトを解決する実践的な方法だと説明されています。

  2. 出典: Laravel公式ドキュメント — Service Container: Binding Interfaces to Implementations

  3. 出典: Laravel公式ドキュメント — Service Container: Registering a Singleton

  4. 出典: Laravel公式ドキュメント — Service Container: Contextual Bindingwhen()->needs()->give()によるコンテキストに応じた結び付け(contextual binding)や、複数のプロバイダへの分割などが解説されています。