#はじめに
Laravelのサービスコンテナは、クラスの依存関係を管理し、依存性注入を行う強力なツールです。依存性注入とは、クラスの依存関係をコンストラクタや場合によっては「セッター」メソッドを通じて「注入」することを指します。
簡単な例を見てみましょう:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* 新しいコントローラーインスタンスを作成します。
*/
public function __construct(
protected UserRepository $users,
) {}
/**
* 指定されたユーザーのプロフィールを表示します。
*/
public function show(string $id): View
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
この例では、UserController はデータソースからユーザーを取得する必要があります。そこで、ユーザーを取得できるサービスを注入します。この場合、UserRepository はおそらく Eloquent を使ってデータベースからユーザー情報を取得しています。しかし、リポジトリが注入されているため、別の実装に簡単に差し替えられます。また、アプリケーションのテスト時に UserRepository のモックやダミー実装を簡単に作成できます。
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 メソッドは、コンテナに一度だけ解決されるクラスやインターフェイスをバインドします。シングルトンバインディングが解決されると、その後の呼び出しでは同じオブジェクトインスタンスが返されます:
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のリクエストやジョブのライフサイクル内で一度だけ解決されるクラスやインターフェイスをコンテナにバインドします。このメソッドは singleton に似ていますが、scoped で登録されたインスタンスは、Laravelアプリケーションが新しい「ライフサイクル」を開始するたびに破棄されます。例えば、Laravel Octane のワーカーが新しいリクエストを処理するときや、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));
});
#インスタンスのバインディング
既存のオブジェクトインスタンスを instance メソッドでコンテナにバインドすることもできます。指定したインスタンスは、その後の呼び出しで常に返されます:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
#インターフェイスから実装へのバインディング
サービスコンテナの非常に強力な機能は、インターフェイスを特定の実装にバインドできることです。例えば、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;
/**
* 新しいクラスインスタンスを作成します。
*/
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');
});
#プリミティブのバインディング
クラスがいくつかの注入されたクラスを受け取る一方で、整数などのプリミティブ値も注入する必要がある場合があります。コンテキストバインディングを使うと、クラスが必要とする任意の値を簡単に注入できます:
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
{
/**
* フィルターのインスタンス。
*
* @var array
*/
protected $filters;
/**
* 新しいクラスインスタンスを作成します。
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}
コンテキストバインディングを使うと、give メソッドにクロージャを渡して、解決済みの Filter インスタンスの配列を返すことで、この依存関係を解決できます。
$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;
/**
* 新しいクラスインスタンスを作成します。
*/
public function __construct(
protected Container $container
) {}
#自動注入
また重要なことに、コンテナで解決されるクラスのコンストラクタで依存関係を型指定できます。これは コントローラー、イベントリスナー、ミドルウェア などに適用されます。さらに、キュージョブ の handle メソッドでも依存注入が可能です。実際には、ほとんどのオブジェクトはこの方法でコンテナから解決されるべきです。
例えば、コントローラーのコンストラクタでアプリケーションが定義したリポジトリを型指定すると、そのリポジトリが自動的に解決されて注入されます。
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use App\Models\User;
class UserController extends Controller
{
/**
* 新しいコントローラーインスタンスを作成します。
*/
public function __construct(
protected UserRepository $users,
) {}
/**
* 指定されたIDのユーザーを表示します。
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
return $user;
}
}
#メソッド呼び出しと注入
オブジェクトのメソッドを呼び出す際に、コンテナがそのメソッドの依存関係を自動注入するようにしたい場合があります。例えば、以下のクラスがあるとします。
<?php
namespace App;
use App\Repositories\UserRepository;
class UserReport
{
/**
* 新しいユーザーレポートを生成します。
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}
コンテナを使って generate メソッドを次のように呼び出せます。
use App\UserReport;
use Illuminate\Support\Facades\App;
$report = App::call([new UserReport, 'generate']);
call メソッドは任意の PHP コールバックを受け取ります。
サービスコンテナの call メソッドは、クロージャを呼び出す際にその依存関係を自動的に注入するためにも使用できます:
use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
$result = App::call(function (UserRepository $repository) {
// ...
});
#コンテナイベント
サービスコンテナはオブジェクトを解決するたびにイベントを発火します。resolving メソッドでこのイベントをリッスンできます。
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// コンテナが "Transistor" 型のオブジェクトを解決したときに呼ばれます...
});
$this->app->resolving(function (mixed $object, Application $app) {
// コンテナが任意の型のオブジェクトを解決したときに呼ばれます...
});
ご覧の通り、解決されたオブジェクトがコールバックに渡されるため、利用者に渡す前に追加のプロパティを設定できます。
#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 の例外がスローされます。