サービスコンテナ
イントロダクション
Laravelのサービスコンテナは、クラスの依存関係を管理し、依存性の注入(DI)を実行するための強力なツールです。依存性の注入とは、要するに、クラスの依存関係がコンストラクタ、あるいは場合によっては「セッター」メソッドを介してクラスに「注入」されることを意味する気の利いたフレーズです。
簡単な例を見てみましょう
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Services\AppleMusic; 6use Illuminate\View\View; 7 8class PodcastController extends Controller 9{10 /**11 * Create a new controller instance.12 */13 public function __construct(14 protected AppleMusic $apple,15 ) {}16 17 /**18 * Show information about the given podcast.19 */20 public function show(string $id): View21 {22 return view('podcasts.show', [23 'podcast' => $this->apple->findPodcast($id)24 ]);25 }26}
この例では、PodcastControllerはApple Musicなどのデータソースからポッドキャストを取得する必要があります。そこで、ポッドキャストを取得できるサービスを注入します。サービスが注入されるため、アプリケーションをテストする際に、AppleMusicサービスの「モック」やダミー実装を簡単に作成できます。
Laravelサービスコンテナを深く理解することは、強力で大規模なアプリケーションを構築するためだけでなく、Laravelコア自体に貢献するためにも不可欠です。
ゼロコンフィギュレーションでの依存解決
クラスに依存関係がないか、他の具象クラス(インターフェイスではない)にのみ依存している場合、コンテナにそのクラスを解決する方法を指示する必要はありません。例えば、routes/web.phpファイルに以下のコードを記述できます。
1<?php 2 3class Service 4{ 5 // ... 6} 7 8Route::get('/', function (Service $service) { 9 die($service::class);10});
この例では、アプリケーションの/ルートにアクセスすると、自動的にServiceクラスが解決され、ルートのハンドラに注入されます。これは画期的なことです。つまり、肥大化した設定ファイルを気にすることなく、依存性の注入を活用してアプリケーションを開発できるのです。
幸いなことに、Laravelアプリケーションを構築する際に記述するクラスの多くは、コントローラ、イベントリスナ、ミドルウェアなどを含め、コンテナを介して依存関係を自動的に受け取ります。さらに、キュー投入されたジョブのhandleメソッドで依存関係をタイプヒントで指定できます。自動的で設定不要な依存性の注入の力を一度味わうと、それなしでの開発は考えられなくなります。
コンテナを利用する場面
ゼロコンフィギュレーションでの依存解決のおかげで、コンテナと手動でやり取りすることなく、ルート、コントローラ、イベントリスナなどで依存関係をタイプヒントで指定することがよくあります。例えば、現在のリクエストに簡単にアクセスできるように、ルート定義でIlluminate\Http\Requestオブジェクトをタイプヒントで指定することがあります。このコードを書くためにコンテナとやり取りする必要はまったくありませんが、裏ではこれらの依存関係の注入を管理しています。
1use Illuminate\Http\Request;2 3Route::get('/', function (Request $request) {4 // ...5});
多くの場合、自動的な依存性の注入とファサードのおかげで、コンテナから何かを手動で結合したり解決したりすることなく、Laravelアプリケーションを構築できます。では、どのような場合に手動でコンテナとやり取りする必要があるのでしょうか?2つの状況を検証してみましょう。
第一に、インターフェイスを実装するクラスを書き、そのインターフェイスをルートやクラスのコンストラクタでタイプヒントで指定したい場合は、コンテナにそのインターフェイスを解決する方法を指示する必要があります。第二に、他のLaravel開発者と共有する予定のLaravelパッケージを作成する場合、パッケージのサービスをコンテナに結合する必要があるかもしれません。
結合
結合の基礎
単純な結合
ほとんどすべてのサービスコンテナの結合は、サービスプロバイダ内で登録されるため、これらの例のほとんどは、そのコンテキストでコンテナを使用する方法を示しています。
サービスプロバイダ内では、$this->appプロパティを介して常にコンテナにアクセスできます。bindメソッドを使用して結合を登録し、登録したいクラスまたはインターフェイス名と、そのクラスのインスタンスを返すクロージャを渡します。
1use App\Services\Transistor;2use App\Services\PodcastParser;3use Illuminate\Contracts\Foundation\Application;4 5$this->app->bind(Transistor::class, function (Application $app) {6 return new Transistor($app->make(PodcastParser::class));7});
リゾルバへの引数としてコンテナ自体を受け取ることに注意してください。その後、コンテナを使用して、構築しているオブジェクトのサブ依存関係を解決できます。
前述のように、通常はサービスプロバイダ内でコンテナとやり取りしますが、サービスプロバイダの外でコンテナとやり取りしたい場合は、App ファサードを介して行うことができます。
1use App\Services\Transistor;2use Illuminate\Contracts\Foundation\Application;3use Illuminate\Support\Facades\App;4 5App::bind(Transistor::class, function (Application $app) {6 // ...7});
bindIfメソッドを使用すると、指定した型に対して結合がまだ登録されていない場合にのみ、コンテナの結合を登録できます。
1$this->app->bindIf(Transistor::class, function (Application $app) {2 return new Transistor($app->make(PodcastParser::class));3});
便宜上、登録したいクラスまたはインターフェイス名を別の引数として提供するのを省略し、代わりにbindメソッドに提供するクロージャの戻り値の型からLaravelに型を推論させることができます。
1App::bind(function (Application $app): Transistor {2 return new Transistor($app->make(PodcastParser::class));3});
クラスがどのインターフェイスにも依存していない場合、コンテナにクラスを結合する必要はありません。コンテナはリフレクションを使用してこれらのオブジェクトを自動的に解決できるため、これらのオブジェクトを構築する方法を指示する必要はありません。
シングルトンの結合
singletonメソッドは、一度だけ解決されるべきクラスまたはインターフェイスをコンテナに結合します。シングルトン結合が解決されると、その後のコンテナへの呼び出しでは同じオブジェクトインスタンスが返されます。
1use App\Services\Transistor;2use App\Services\PodcastParser;3use Illuminate\Contracts\Foundation\Application;4 5$this->app->singleton(Transistor::class, function (Application $app) {6 return new Transistor($app->make(PodcastParser::class));7});
singletonIfメソッドを使用すると、指定した型に対してシングルトン結合がまだ登録されていない場合にのみ、コンテナのシングルトン結合を登録できます。
1$this->app->singletonIf(Transistor::class, function (Application $app) {2 return new Transistor($app->make(PodcastParser::class));3});
スコープ付きシングルトンの結合
scopedメソッドは、特定のLaravelリクエスト/ジョブのライフサイクル内で一度だけ解決されるべきクラスまたはインターフェイスをコンテナに結合します。このメソッドはsingletonメソッドに似ていますが、scopedメソッドを使用して登録されたインスタンスは、Laravel Octaneワーカーが新しいリクエストを処理するときや、Laravel キューワーカーが新しいジョブを処理するときなど、Laravelアプリケーションが新しい「ライフサイクル」を開始するたびに破棄されます。
1use App\Services\Transistor;2use App\Services\PodcastParser;3use Illuminate\Contracts\Foundation\Application;4 5$this->app->scoped(Transistor::class, function (Application $app) {6 return new Transistor($app->make(PodcastParser::class));7});
scopedIfメソッドを使用すると、指定した型に対してスコープ付きコンテナ結合がまだ登録されていない場合にのみ、コンテナのスコープ付き結合を登録できます。
1$this->app->scopedIf(Transistor::class, function (Application $app) {2 return new Transistor($app->make(PodcastParser::class));3});
インスタンスの結合
instanceメソッドを使用して、既存のオブジェクトインスタンスをコンテナに結合することもできます。指定されたインスタンスは、その後のコンテナへの呼び出しで常に返されます。
1use App\Services\Transistor;2use App\Services\PodcastParser;3 4$service = new Transistor(new PodcastParser);5 6$this->app->instance(Transistor::class, $service);
インターフェイスと実装の結合
サービスコンテナの非常に強力な機能の1つは、インターフェイスを特定の実装に結合する機能です。例えば、EventPusherインターフェイスとRedisEventPusher実装があるとします。このインターフェイスのRedisEventPusher実装をコーディングしたら、次のようにサービスコンテナに登録できます。
1use App\Contracts\EventPusher;2use App\Services\RedisEventPusher;3 4$this->app->bind(EventPusher::class, RedisEventPusher::class);
この文は、クラスがEventPusherの実装を必要とするときに、コンテナがRedisEventPusherを注入すべきであることをコンテナに伝えます。これで、コンテナによって解決されるクラスのコンストラクタでEventPusherインターフェイスをタイプヒントで指定できます。コントローラ、イベントリスナ、ミドルウェア、およびLaravelアプリケーション内の他のさまざまなタイプのクラスは、常にコンテナを使用して解決されることを忘れないでください。
1use App\Contracts\EventPusher;2 3/**4 * Create a new class instance.5 */6public function __construct(7 protected EventPusher $pusher,8) {}
コンテキストによる結合
同じインターフェイスを利用する2つのクラスがあり、各クラスに異なる実装を注入したい場合があります。例えば、2つのコントローラがIlluminate\Contracts\Filesystem\Filesystem 契約の異なる実装に依存している場合があります。Laravelは、この動作を定義するためのシンプルで流暢なインターフェイスを提供します。
1use App\Http\Controllers\PhotoController; 2use App\Http\Controllers\UploadController; 3use App\Http\Controllers\VideoController; 4use Illuminate\Contracts\Filesystem\Filesystem; 5use Illuminate\Support\Facades\Storage; 6 7$this->app->when(PhotoController::class) 8 ->needs(Filesystem::class) 9 ->give(function () {10 return Storage::disk('local');11 });12 13$this->app->when([VideoController::class, UploadController::class])14 ->needs(Filesystem::class)15 ->give(function () {16 return Storage::disk('s3');17 });
コンテキスト属性
コンテキストによる結合は、ドライバや設定値の実装を注入するためによく使用されるため、Laravelはサービスプロバイダでコンテキストによる結合を手動で定義することなく、これらのタイプの値を注入できるさまざまなコンテキスト結合属性を提供しています。
例えば、Storage属性を使用して、特定のストレージディスクを注入できます。
1<?php 2 3namespace App\Http\Controllers; 4 5use Illuminate\Container\Attributes\Storage; 6use Illuminate\Contracts\Filesystem\Filesystem; 7 8class PhotoController extends Controller 9{10 public function __construct(11 #[Storage('local')] protected Filesystem $filesystem12 )13 {14 // ...15 }16}
Storage属性に加えて、LaravelはAuth、Cache、Config、DB、Log、RouteParameter、およびTag属性を提供しています。
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Photo; 6use Illuminate\Container\Attributes\Auth; 7use Illuminate\Container\Attributes\Cache; 8use Illuminate\Container\Attributes\Config; 9use Illuminate\Container\Attributes\DB;10use Illuminate\Container\Attributes\Log;11use Illuminate\Container\Attributes\RouteParameter;12use Illuminate\Container\Attributes\Tag;13use Illuminate\Contracts\Auth\Guard;14use Illuminate\Contracts\Cache\Repository;15use Illuminate\Database\Connection;16use Psr\Log\LoggerInterface;17 18class PhotoController extends Controller19{20 public function __construct(21 #[Auth('web')] protected Guard $auth,22 #[Cache('redis')] protected Repository $cache,23 #[Config('app.timezone')] protected string $timezone,24 #[DB('mysql')] protected Connection $connection,25 #[Log('daily')] protected LoggerInterface $log,26 #[RouteParameter('photo')] protected Photo $photo,27 #[Tag('reports')] protected iterable $reports,28 )29 {30 // ...31 }32}
さらに、Laravelは現在認証されているユーザーを特定のルートまたはクラスに注入するためのCurrentUser属性を提供しています。
1use App\Models\User;2use Illuminate\Container\Attributes\CurrentUser;3 4Route::get('/user', function (#[CurrentUser] User $user) {5 return $user;6})->middleware('auth');
カスタム属性の定義
Illuminate\Contracts\Container\ContextualAttribute契約を実装することで、独自のコンテキスト属性を作成できます。コンテナは属性のresolveメソッドを呼び出します。このメソッドは、属性を利用するクラスに注入されるべき値を解決する必要があります。以下の例では、Laravelの組み込みConfig属性を再実装します。
1<?php 2 3namespace App\Attributes; 4 5use Illuminate\Contracts\Container\ContextualAttribute; 6 7#[Attribute(Attribute::TARGET_PARAMETER)] 8class Config implements ContextualAttribute 9{10 /**11 * Create a new attribute instance.12 */13 public function __construct(public string $key, public mixed $default = null)14 {15 }16 17 /**18 * Resolve the configuration value.19 *20 * @param self $attribute21 * @param \Illuminate\Contracts\Container\Container $container22 * @return mixed23 */24 public static function resolve(self $attribute, Container $container)25 {26 return $container->make('config')->get($attribute->key, $attribute->default);27 }28}
プリミティブ値の結合
クラスがいくつかの注入されたクラスを受け取るが、整数などの注入されたプリミティブ値も必要とする場合があります。コンテキストによる結合を簡単に使用して、クラスが必要とする任意の値を注入できます。
1use App\Http\Controllers\UserController;2 3$this->app->when(UserController::class)4 ->needs('$variableName')5 ->give($value);
クラスがタグ付けされたインスタンスの配列に依存することがあります。giveTaggedメソッドを使用すると、そのタグを持つすべてのコンテナ結合を簡単に注入できます。
1$this->app->when(ReportAggregator::class)2 ->needs('$reports')3 ->giveTagged('reports');
アプリケーションの設定ファイルの1つから値を注入する必要がある場合は、giveConfigメソッドを使用できます。
1$this->app->when(ReportAggregator::class)2 ->needs('$timezone')3 ->giveConfig('app.timezone');
タイプヒント付き可変長引数の結合
時折、可変長コンストラクタ引数を使用して型付けされたオブジェクトの配列を受け取るクラスがあるかもしれません。
1<?php 2 3use App\Models\Filter; 4use App\Services\Logger; 5 6class Firewall 7{ 8 /** 9 * The filter instances.10 *11 * @var array12 */13 protected $filters;14 15 /**16 * Create a new class instance.17 */18 public function __construct(19 protected Logger $logger,20 Filter ...$filters,21 ) {22 $this->filters = $filters;23 }24}
コンテキストによる結合を使用して、解決されたFilterインスタンスの配列を返すクロージャをgiveメソッドに提供することで、この依存関係を解決できます。
1$this->app->when(Firewall::class)2 ->needs(Filter::class)3 ->give(function (Application $app) {4 return [5 $app->make(NullFilter::class),6 $app->make(ProfanityFilter::class),7 $app->make(TooLongFilter::class),8 ];9 });
便宜上、FirewallがFilterインスタンスを必要とするときにコンテナによって解決されるクラス名の配列を提供するだけでもかまいません。
1$this->app->when(Firewall::class)2 ->needs(Filter::class)3 ->give([4 NullFilter::class,5 ProfanityFilter::class,6 TooLongFilter::class,7 ]);
可変長タグ依存関係
クラスが特定のクラスとしてタイプヒントされた可変長依存関係(Report ...$reports)を持つことがあります。needsメソッドとgiveTaggedメソッドを使用すると、指定された依存関係に対して、そのタグを持つすべてのコンテナ結合を簡単に注入できます。
1$this->app->when(ReportAggregator::class)2 ->needs(Report::class)3 ->giveTagged('reports');
タグ付け
時折、特定の「カテゴリ」のすべての結合を解決する必要がある場合があります。例えば、多くの異なるReportインターフェイス実装の配列を受け取るレポートアナライザを構築しているとします。Report実装を登録した後、tagメソッドを使用してタグを割り当てることができます。
1$this->app->bind(CpuReport::class, function () {2 // ...3});4 5$this->app->bind(MemoryReport::class, function () {6 // ...7});8 9$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
サービスがタグ付けされると、コンテナのtaggedメソッドを介してそれらすべてを簡単に解決できます。
1$this->app->bind(ReportAnalyzer::class, function (Application $app) {2 return new ReportAnalyzer($app->tagged('reports'));3});
結合の拡張
extendメソッドを使用すると、解決されたサービスを変更できます。例えば、サービスが解決されたときに、サービスを装飾または設定するための追加のコードを実行できます。extendメソッドは2つの引数、拡張するサービスクラスと、変更されたサービスを返す必要があるクロージャを受け入れます。クロージャは、解決されるサービスとコンテナインスタンスを受け取ります。
1$this->app->extend(Service::class, function (Service $service, Application $app) {2 return new DecoratedService($service);3});
依存解決
makeメソッド
makeメソッドを使用して、コンテナからクラスインスタンスを解決できます。makeメソッドは、解決したいクラスまたはインターフェイスの名前を受け入れます。
1use App\Services\Transistor;2 3$transistor = $this->app->make(Transistor::class);
クラスの依存関係の一部がコンテナを介して解決できない場合は、makeWithメソッドに連想配列として渡すことでそれらを注入できます。例えば、Transistorサービスが必要とする$idコンストラクタ引数を手動で渡すことができます。
1use App\Services\Transistor;2 3$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
boundメソッドを使用して、クラスまたはインターフェイスがコンテナに明示的に結合されているかどうかを判断できます。
1if ($this->app->bound(Transistor::class)) {2 // ...3}
サービスプロバイダの外で、$app変数にアクセスできないコードの場所にいる場合は、App ファサードまたはapp ヘルパを使用して、コンテナからクラスインスタンスを解決できます。
1use App\Services\Transistor;2use Illuminate\Support\Facades\App;3 4$transistor = App::make(Transistor::class);5 6$transistor = app(Transistor::class);
コンテナによって解決されるクラスにLaravelコンテナインスタンス自体を注入したい場合は、クラスのコンストラクタでIlluminate\Container\Containerクラスをタイプヒントで指定できます。
1use Illuminate\Container\Container;2 3/**4 * Create a new class instance.5 */6public function __construct(7 protected Container $container,8) {}
自動インジェクション
あるいは、そして重要なことですが、コントローラ、イベントリスナ、ミドルウェアなどを含む、コンテナによって解決されるクラスのコンストラクタで依存関係をタイプヒントで指定できます。さらに、キュー投入されたジョブのhandleメソッドで依存関係をタイプヒントで指定できます。実際には、これがほとんどのオブジェクトがコンテナによって解決されるべき方法です。
例えば、コントローラのコンストラクタでアプリケーションによって定義されたサービスをタイプヒントで指定できます。サービスは自動的に解決され、クラスに注入されます。
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Services\AppleMusic; 6 7class PodcastController extends Controller 8{ 9 /**10 * Create a new controller instance.11 */12 public function __construct(13 protected AppleMusic $apple,14 ) {}15 16 /**17 * Show information about the given podcast.18 */19 public function show(string $id): Podcast20 {21 return $this->apple->findPodcast($id);22 }23}
メソッドの呼び出しとインジェクション
コンテナがメソッドの依存関係を自動的に注入できるようにしながら、オブジェクトインスタンスのメソッドを呼び出したい場合があります。例えば、次のクラスがあるとします。
1<?php 2 3namespace App; 4 5use App\Services\AppleMusic; 6 7class PodcastStats 8{ 9 /**10 * Generate a new podcast stats report.11 */12 public function generate(AppleMusic $apple): array13 {14 return [15 // ...16 ];17 }18}
次のようにコンテナを介してgenerateメソッドを呼び出すことができます。
1use App\PodcastStats;2use Illuminate\Support\Facades\App;3 4$stats = App::call([new PodcastStats, 'generate']);
callメソッドは、任意のPHP callableを受け入れます。コンテナのcallメソッドは、依存関係を自動的に注入しながらクロージャを呼び出すためにも使用できます。
1use App\Services\AppleMusic;2use Illuminate\Support\Facades\App;3 4$result = App::call(function (AppleMusic $apple) {5 // ...6});
コンテナイベント
サービスコンテナは、オブジェクトを解決するたびにイベントを発行します。resolvingメソッドを使用してこのイベントをリッスンできます。
1use App\Services\Transistor; 2use Illuminate\Contracts\Foundation\Application; 3 4$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) { 5 // Called when container resolves objects of type "Transistor"... 6}); 7 8$this->app->resolving(function (mixed $object, Application $app) { 9 // Called when container resolves object of any type...10});
ご覧のとおり、解決されるオブジェクトはコールバックに渡され、コンシューマに渡される前に追加のプロパティをオブジェクトに設定できます。
再結合
rebindingメソッドを使用すると、サービスがコンテナに再結合されるとき、つまり、最初の結合後に再度登録または上書きされるときにリッスンできます。これは、特定の結合が更新されるたびに依存関係を更新したり、動作を変更したりする必要がある場合に便利です。
1use App\Contracts\PodcastPublisher; 2use App\Services\SpotifyPublisher; 3use App\Services\TransistorPublisher; 4use Illuminate\Contracts\Foundation\Application; 5 6$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class); 7 8$this->app->rebinding( 9 PodcastPublisher::class,10 function (Application $app, PodcastPublisher $newInstance) {11 //12 },13);14 15// New binding will trigger rebinding closure...16$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);
PSR-11
Laravelのサービスコンテナは、PSR-11インターフェイスを実装しています。したがって、PSR-11コンテナインターフェイスをタイプヒントで指定して、Laravelコンテナのインスタンスを取得できます。
1use App\Services\Transistor;2use Psr\Container\ContainerInterface;3 4Route::get('/', function (ContainerInterface $container) {5 $service = $container->get(Transistor::class);6 7 // ...8});
指定された識別子を解決できない場合、例外がスローされます。識別子が一度も結合されていない場合、例外はPsr\Container\NotFoundExceptionInterfaceのインスタンスになります。識別子が結合されたが解決できなかった場合、Psr\Container\ContainerExceptionInterfaceのインスタンスがスローされます。