サービスコンテナ¶
はじめに¶
Laravelのサービスコンテナは、クラスの依存関係を管理し、依存性の注入(ディペンデンシーインジェクション)を実行するための強力なツールです。依存性注入とは、あるクラスの依存関係がコンストラクタや、場合によっては「セッター」メソッドを介して「注入」されることを意味する、少し洒落た表現です。
簡単な例を見てみましょう:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
use Illuminate\View\View;
class PodcastController extends Controller
{
/**
* 新しいコントローラインスタンスの作成
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* 指定されたポッドキャストの情報を表示
*/
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));
});
Note
インターフェースに依存しないクラスをコンテナにバインドする必要はありません。コンテナはリフレクションを使用してこれらのオブジェクトを自動的に解決できるため、コンテナにこれらのオブジェクトの構築方法を指示する必要はありません。
シングルトンのバインディング¶
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');
});
コンテキスト属性¶
コンテキストバインディングは、多くの場合、ドライバの実装や設定値の注入に使用されるため、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
、およびTag
属性を提供します。
<?php
namespace App\Http\Controllers;
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\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\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,
#[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
{
/**
* 新しい属性インスタンスを作成します。
*/
public function __construct(public string $key, public mixed $default = null)
{
}
/**
* 設定値を解決します。
*
* @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
メソッドを使用して、そのタグを持つすべてのコンテナバインディングを簡単に注入できます。
アプリケーションの設定ファイルのいずれかから値を注入する必要がある場合は、giveConfig
メソッドを使用できます。
型付き可変引数のバインディング¶
時には、クラスが可変引数のコンストラクタ引数を使用して型付きオブジェクトの配列を受け取る場合があります。
<?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;
}
}
コンテキストバインディングを使用して、この依存関係を解決するには、解決された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
メソッドを使用して、その依存関係に対してタグ付けされたすべてのコンテナバインディングを簡単に注入できます。
タグ付け¶
特定の「カテゴリ」に属するすべてのバインディングを解決する必要がある場合があります。たとえば、多くの異なる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
メソッドは、解決したいクラスまたはインターフェースの名前を受け取ります。
クラスの依存関係の一部がコンテナによって解決できない場合、makeWith
メソッドに連想配列として渡すことで注入できます。たとえば、Transistor
サービスに必要な$id
コンストラクタ引数を手動で渡すことができます。
bound
メソッドは、クラスやインターフェースがコンテナに明示的にバインドされているかどうかを判断するために使用できます:
コードの場所がサービスプロバイダの外であり、$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\Services\AppleMusic;
class PodcastController extends Controller
{
/**
* 新しいコントローラインスタンスを作成します。
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* 指定されたポッドキャストに関する情報を表示します。
*/
public function show(string $id): Podcast
{
return $this->apple->findPodcast($id);
}
}
メソッドの呼び出しと注入¶
オブジェクトインスタンスのメソッドを呼び出したいが、そのメソッドの依存関係をコンテナに自動的に注入してもらいたい場合があります。例えば、次のクラスがあるとします:
<?php
namespace App;
use App\Services\AppleMusic;
class PodcastStats
{
/**
* 新しいポッドキャスト統計レポートを生成します。
*/
public function generate(AppleMusic $apple): array
{
return [
// ...
];
}
}
generate
メソッドは、コンテナを介して次のように呼び出すことができます:
use App\PodcastStats;
use Illuminate\Support\Facades\App;
$stats = App::call([new PodcastStats, 'generate']);
call
メソッドは、任意のPHP callableを受け入れます。コンテナの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) {
// "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
のインスタンスがスローされます。