Symfony 依存性注入:サービスコンテナ、オートワイヤリング、タグ(2026年)

Symfony の依存性注入を深く掘り下げ、サービスコンテナ、オートワイヤリング、#[Autowire] アトリビュート、インターフェースのバインディング、#[AutowireIterator] によるタグ付きサービス、コンパイラパス、遅延サービスを Symfony 7.4 と 8.0 で解説します。

Symfony の依存性注入におけるサービスコンテナとオートワイヤリングのアーキテクチャ図

Symfony の依存性注入(DI)は、あらゆる Symfony アプリケーションの土台です。サービスコンテナがオブジェクトを生成・設定し、互いに結び付けることで、各クラスは自ら依存を構築する代わりに、必要なものを宣言するだけで済みます。Symfony 7.4 と 8.0 では、オートワイヤリング、PHP アトリビュート、タグ付きサービスによって明示的な設定はほぼ不要になりました。それでいてコンテナはコンパイル時に完全に解決され、動作が予測可能なまま保たれます。この解決の仕組みを理解しているかどうかが、コンテナと戦う開発者と、コンテナに仕事を任せる開発者を分けます。

Symfony の依存性注入とは?

Symfony における依存性注入とは、オブジェクトが自ら依存を生成するのではなく、サービスコンテナがコンストラクタを通じてオブジェクトの依存を渡すパターンです。オートワイヤリングは型ヒントの付いた各引数を、対応するサービスへ自動的に解決するため、ほとんどのクラスは手動での設定を一切必要としません。

Symfony のサービスコンテナが依存を解決する仕組み

サービスコンテナは、アプリケーション内のあらゆるサービスの構築方法を把握した、コンパイル済みの PHP クラスです。コンテナのコンパイルフェーズで、Symfony はクラス定義を走査し、コンストラクタのシグネチャを読み取り、キャッシュされたファクトリを生成します。実行時にサービスを要求すると、遅延生成された共有インスタンスが返されます。結線がコンパイル時に行われるため、依存の設定ミスは本番環境でリクエストが届いたときではなく、キャッシュがビルドされる時点で失敗します。

サービスとは、単に何らかの処理を担い、コンテナによって管理されるクラスにすぎません。デフォルトの config/services.yaml は、src/ 配下のすべてをオートワイヤリングとオートコンフィギュレーションを有効にしたサービスとして登録します。だからこそ、素朴なクラスが追加設定なしで動作するのです。

yaml
# config/services.yaml
services:
    _defaults:
        autowire: true       # inject arguments by type-hint
        autoconfigure: true  # apply tags based on implemented interfaces

    App\:
        resource: '../src/'
        exclude:
            - '../src/Entity/'
            - '../src/Kernel.php'

これらのデフォルトが整っていれば、クラスはコンストラクタを通じて協調オブジェクトを要求し、コンテナがそれらを供給します。ファクトリも new も、手書きの引数リストも不要です。

src/Notification/SlackNotifier.phpphp
namespace App\Notification;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class SlackNotifier
{
    // The container reads each type-hint and injects a matching service
    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly LoggerInterface $logger,
    ) {}

    public function send(string $channel, string $message): void
    {
        $this->httpClient->request('POST', 'https://slack.example/api/post', [
            'json' => ['channel' => $channel, 'text' => $message],
        ]);
        $this->logger->info('Slack message dispatched', ['channel' => $channel]);
    }
}

コンテナは SlackNotifier を一度だけインスタンス化し、共有の HTTP クライアントとロガーを注入したうえで、要求されるすべての場所でそのインスタンスを再利用します。このデフォルトで共有という挙動こそが、コンテナを高速かつメモリ効率の良いものにしています。全体のモデルは公式の Symfony サービスコンテナガイド に記載されており、PHP エコシステム全体で共有される PSR-11 コンテナインターフェース 標準の上に構築されています。

リクエストごとに新しいインスタンスが必要な場合、たとえばデータを蓄積するステートフルなビルダーのようなケースでは、共有を無効にできます。定義に shared: false を設定すると、それは注入されるたびに新しいオブジェクトを返すファクトリになります。ほとんどのサービスはステートレスな協調オブジェクトであるため実際にはまれですが、このデフォルトが存在することとその上書き方法を知っておくことは、2 つの利用側が同じオブジェクトを予期せず変更していたことにバグの原因がたどり着いたときに役立ちます。

Symfony のオートワイヤリング:型ヒントと Autowire アトリビュート

Symfony のオートワイヤリングは、コンストラクタ引数をその型宣言によって解決します。引数にちょうど 1 つのサービスへ対応するクラスやインターフェースの型ヒントが付いていれば、コンテナは設定なしでそれを注入します。オートワイヤリングが機能しなくなるのは、サービスではない値、つまりスカラー値、環境変数、パラメータの場合だけです。#[Autowire] アトリビュートは、その隙間を引数上で直接埋め、結線をそれを使うコードのすぐそばに保ちます。

src/Service/PaymentGateway.phpphp
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class PaymentGateway
{
    public function __construct(
        // Inject a container parameter with the %...% syntax
        #[Autowire('%kernel.environment%')]
        private readonly string $environment,

        // Inject an environment variable, resolved at runtime
        #[Autowire(env: 'STRIPE_SECRET')]
        private readonly string $apiKey,

        // Bind a specific service when the type alone is ambiguous
        #[Autowire(service: 'monolog.logger.payment')]
        private readonly LoggerInterface $logger,
    ) {}
}

各引数は、パラメータ、環境変数、名前付きサービスのいずれかという、自身の取得元を宣言します。このアトリビュートは Expression Language の値を扱う expression: 引数も受け付けます。これは、依存が静的な定義ではなく実行時の計算から得られる場合に便利です。このメタデータをコンストラクタ上に置いておくことで、読み手は別の YAML ファイルを開かずとも、すべての値がどこに由来するかを正確に把握できます。アトリビュートのリファレンスは Symfony オートワイヤリングのドキュメント にあります。

具象クラスではなくインターフェースに型ヒントを付ける

S3Storage のような具象クラスをオートワイヤリングすると、すべての利用側がその実装に結合し、テストで差し替えるのが苦痛になります。代わりにインターフェースへ型ヒントを付け、具体的なサービスの決定は #[AsAlias]#[Autowire(service: ...)] に委ねましょう。コンテナ全体を注入してサービスを手作業で取り出すやり方は、コンパイル時のチェックを台無しにし、オートワイヤリングが取り除くために存在するアンチパターンそのものです。

複数の実装があるときにインターフェースをバインディングする

インターフェースに対するオートワイヤリングは、2 つのクラスがそれを実装するまでは問題なく機能します。その時点でコンテナはどちらを注入すべきか推測できず、コンパイル時に曖昧さのエラーを投げます。これを解決するアトリビュートは 2 つあります。#[AsAlias] は 1 つの実装をそのインターフェースのデフォルトとして指定し、#[Autowire(service: ...)] は個々の引数に対して特定の実装を固定します。

src/Storage/StorageInterface.phpphp
namespace App\Storage;

interface StorageInterface
{
    public function put(string $key, string $contents): void;
}
src/Storage/S3Storage.phpphp
namespace App\Storage;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

// Becomes the default service returned for StorageInterface
#[AsAlias(StorageInterface::class)]
final class S3Storage implements StorageInterface
{
    public function put(string $key, string $contents): void { /* upload to S3 */ }
}
src/Service/BackupService.phpphp
namespace App\Service;

use App\Storage\LocalStorage;
use App\Storage\StorageInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class BackupService
{
    public function __construct(
        // Resolves to the #[AsAlias] default (S3Storage)
        private readonly StorageInterface $primary,

        // Pins the concrete implementation explicitly by service id
        #[Autowire(service: LocalStorage::class)]
        private readonly StorageInterface $fallback,
    ) {}
}

#[Target] アトリビュートは第 3 の選択肢を提供します。これは実装をそのオートワイヤリングのエイリアス名で参照し、引数の変数名を解決から切り離します。これらの使い分けは意図に帰着します。コードベース全体で 1 つの実装が本当にデフォルトである場合には #[AsAlias] を使い、単一の利用側だけがデフォルト以外のバリアントを必要とする場合には #[Autowire(service: ...)]#[Target] を使います。このパターンは本番アプリケーションで絶えず登場し、Symfony 依存性注入の面接対策モジュール でも定番のテーマです。

Symfonyの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Symfony のサービスタグと AutowireIterator による収集

タグは、関連するサービスをまとめて別のサービスが一度に消費できるようにするための、Symfony の仕組みです。これは、フレームワーク自体の随所に現れるストラテジー、責任の連鎖(chain of responsibility)、プラグインといったパターンを支えています。フォームタイプの拡張、Twig 拡張、イベントサブスクライバーはいずれもタグ駆動です。手作業でタグ付けする代わりに、インターフェースに付けた #[AutoconfigureTag] は、それを実装するすべてのクラスに自動でタグを付けるようコンテナへ指示します。

src/Export/ExporterInterface.phpphp
namespace App\Export;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

// Every class implementing this interface is tagged automatically
#[AutoconfigureTag('app.exporter')]
interface ExporterInterface
{
    public function supports(string $format): bool;
    public function export(array $rows): string;
}

個々の実装は、#[AsTaggedItem] によってコレクション内での自身の位置を制御します。これは優先度(値が大きいほど先に実行される)と、キー付きアクセスのための任意の文字列インデックスを設定します。

src/Export/CsvExporter.phpphp
namespace App\Export;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;

// Higher priority services appear earlier in the injected iterable
#[AsTaggedItem(priority: 10)]
final class CsvExporter implements ExporterInterface
{
    public function supports(string $format): bool
    {
        return $format === 'csv';
    }

    public function export(array $rows): string
    {
        return implode("\n", array_map(fn ($r) => implode(',', $r), $rows));
    }
}

消費側のサービスは、#[AutowireIterator] を使って、タグ付けされたすべてのエクスポーターを順序付きの iterable として収集します。そしてそのコレクションを走査し、要求されたフォーマットをサポートする最初のストラテジーへ処理を委譲します。

src/Export/ExportManager.phpphp
namespace App\Export;

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

final class ExportManager
{
    // Collects all services tagged 'app.exporter', ordered by priority
    public function __construct(
        #[AutowireIterator('app.exporter')]
        private readonly iterable $exporters,
    ) {}

    public function export(string $format, array $rows): string
    {
        foreach ($this->exporters as $exporter) {
            if ($exporter->supports($format)) {
                return $exporter->export($rows);
            }
        }

        throw new \InvalidArgumentException("No exporter for format: $format");
    }
}

コレクションが大きい、あるいは構築コストが高い場合には、#[AutowireLocator] が遅延版の選択肢になります。これは PSR-11 コンテナを注入し、タグ付きサービスは実際にキーで取得されたときにのみインスタンス化されます。この、先行して反復する方式と遅延して参照する方式との違いは、接続や重い状態を保持するサービスにとって重要です。

オートコンフィギュレーション、コンパイラパス、遅延サービス

オートコンフィギュレーションは、#[AutoconfigureTag]#[AsTaggedItem] といったアトリビュートを読み取り、コンパイル時に適用する仕組みです。EventSubscriberInterface を実装するだけで、設定ゼロでイベントサブスクライバーが登録されるのはこのためです。アトリビュートでは表現できない変換のために、コンパイラパスがコンテナのビルドへ直接フックし、キャッシュが書き出される前に定義を書き換えます。GitHub 上の DependencyInjection コンポーネントのソースを見れば、フレームワーク自身がこうしたパスを内部で何十も使っている様子が分かります。

パフォーマンスに敏感なグラフは、遅延サービスの恩恵を受けます。引数に #[Autowire(lazy: true)] を付けると、生成されたプロキシライブラリではなく PHP 8.4 のネイティブ遅延オブジェクトに支えられて、依存が最初に呼び出されるまでインスタンス化が先送りされます。

src/Service/ReportBuilder.phpphp
namespace App\Service;

use App\Pdf\PdfRenderer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class ReportBuilder
{
    public function __construct(
        // PdfRenderer is only instantiated on first actual use
        #[Autowire(lazy: true)]
        private readonly PdfRenderer $renderer,
    ) {}

    public function buildMonthly(): string
    {
        // If no method touches $this->renderer, it is never constructed
        return $this->renderer->render('monthly-report');
    }
}

ネイティブ遅延オブジェクトは PHP 8.4 で導入されたもので、Symfony 8 と PHP 8.4 の遅延オブジェクト に関するガイドで詳しく取り上げています。その見返りは、無駄のないコンテナです。PDF レンダラー、サードパーティ SDK のクライアント、レポート生成器といった重い協調オブジェクトは、それを必要とするコードパスが実際に実行されるまで、まったくコストがかかりません。

Symfony DI の面接質問と回答

Symfony の DI に関するよくある面接質問では、オートワイヤリングとオートコンフィギュレーションの違いが問われます。オートワイヤリングは個々のサービスのコンストラクタ引数を型によって解決し、オートコンフィギュレーションはサービスが実装するインターフェースに基づいて、タグ、メソッド呼び出し、設定を適用します。両者は独立したスイッチです。サービスはオートコンフィギュレーションなしでオートワイヤリングされることも、その逆もあり得ます。

もう 1 つよくある質問は、なぜコンテナが public なサービスと private なサービスを区別するのか、というものです。サービスはデフォルトで private であり、$container->get() でコンテナから直接取得することはできず、注入によってのみ利用できます。private なサービスはコンパイル時にインライン化され、最適化によって取り除かれることがあるため、より高速です。一方 public なサービスは取得可能でなければならないため、最適化はそれらをそのまま残します。コンテナから手動でサービスを取得する行為がアンチパターンとみなされるのは、まさにこの最適化を台無しにし、依存を隠してしまうからです。

循環依存は典型的な落とし穴であり、よくある追加質問でもあります。サービス A がコンストラクタで B を必要とし、B が A を必要とする場合、コンテナはどちらも構築できず、コンパイル時に循環参照を報告します。標準的な解決策は、共有される振る舞いを 3 つ目のサービスへ抽出して循環を断ち切る、#[Autowire(lazy: true)] によって片側を遅延注入に切り替えて依存が最初の呼び出し時にのみ解決されるようにする、あるいはセッター注入を使ってオブジェクトが協調オブジェクトの結線前に存在するようにする、というものです。循環を黙らせる方法だけでなく、なぜ循環が設計上の匂いなのかを認識していることこそ、面接官が実際に聞き取ろうとしている点です。

面接官はコンテナ解決のタイミングについても掘り下げます。Symfony はコンテナを一度コンパイルしてキャッシュするため、解決できない依存はリクエスト時ではなくキャッシュのウォームアップ時に表面化します。この保証はコンパイル時モデルの直接的な帰結であり、大規模な Symfony アプリケーションが負荷の下でも予測可能であり続ける理由の 1 つです。こうした質問のより幅広いセットについては、Symfony テクノロジートラック がコンポーネントと難易度ごとに整理しています。

まとめ

  • コンテナに仕事を任せましょう。src/ 配下にクラスを登録し、コンストラクタで依存に型ヒントを付け、手動の YAML 結線は一切省きます。
  • #[Autowire] に頼るのは、サービスではない値、パラメータ、環境変数、特定のサービス ID の場合だけにとどめます。
  • インターフェースの曖昧さは、デフォルトには #[AsAlias]、明示的な指定には #[Autowire(service: ...)]#[Target] で解決します。
  • ストラテジーやプラグインのパターンは、#[AutoconfigureTag]#[AsTaggedItem] の優先度、#[AutowireIterator] で駆動し、先行反復よりも遅延参照が望ましい場合は #[AutowireLocator] に切り替えます。
  • コストの高い協調オブジェクトは #[Autowire(lazy: true)] で遅延させます。これは PHP 8.4 のネイティブ遅延オブジェクトを利用します。
  • サービスは取得するのではなく private のまま注入し、コンパイラがグラフをインライン化して最適化できるようにします。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#symfony
#dependency injection
#service container
#autowiring
#php
#di

共有

関連記事