Skip to content

サービスコンテナ

はじめに

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はAuthCacheConfigDBLog、および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メソッドを使用して、そのタグを持つすべてのコンテナバインディングを簡単に注入できます。

$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;
    }
}

コンテキストバインディングを使用して、この依存関係を解決するには、解決された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),
                ];
          });

便宜上、FirewallFilterインスタンスを必要とするときにコンテナによって解決されるクラス名の配列を提供することもできます。

$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]);
$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\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のインスタンスがスローされます。

ユーザーノート