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