メインコンテンツへスキップ
複数のパッケージや利用アプリケーションから、同じ集約サービスへ機能を追加できるようにしたい場合、コンテナの tag() と tagged() を使えます。集約する側がすべての実装クラスを知る必要はありません。 このページはLaravel 13を対象とし、Framework v13.38.0 の実装と公式テストを確認しています。バインディングの基本はサービスコンテナ、プロバイダーの配布方法はパッケージ開発を参照してください。

タグはサービスの登録キーをまとめる

tag() は、クラス名などのコンテナ登録キーをタグの一覧に追加します。タグを付けるだけでは、そのサービスを生成しません。 tagged() は、そのタグに登録されたサービスを列挙するための iterable を返します。特定のインターフェースを実装するクラスを自動検索する機能ではなく、各プロバイダーが明示的に参加する仕組みです。vendor:publish --tag の公開リソース用タグとも別です。

拡張可能なレポートパッケージを作る

実装が共有するコントラクトを用意する

例として、複数のレポートを集める acme/reports パッケージを作ります。Acme\Reports\ が src/ に対応するComposerのPSR-4設定を前提とします。 src/Contracts/Report.php に、プラグインが実装するコントラクトを定義します。
最小限の実装として、src/RuntimeReport.php と src/MemoryReport.php を用意します。

集約サービスはiterableを受け取る

src/ReportCollector.php は、各実装を列挙するときに収集処理を呼びます。公開するタグ名を定数にすると、利用側も同じ名前を参照できます。
PHPの iterable 型だけでは要素の型を検証できず、tag() 自身も Report の実装かどうかを確認しません。この例では集約する側でもコントラクトを確認しています。

register()ではバインディングとタグだけを登録する

src/ReportsServiceProvider.php で、標準実装と集約サービスを登録します。集約サービスのファクトリは、取得した iterable をそのまま渡します。
別パッケージや利用アプリケーションは、Report を実装した DailyReport などを用意し、自分のプロバイダーの register() 内で同じタグに追加します。
すべてのプロバイダーの登録後、app(ReportCollector::class)->collect() を呼ぶと、追加した実装も集約されます。参加する実装が増えても、ReportCollector の生成処理を変更する必要はありません。

tagged()とgiveTagged()では生成タイミングが違う

確認した実装では、登録済みタグの tagged() は RewindableGenerator を返します。各サービスの make() は列挙時に呼ばれ、途中で列挙を止めれば残りのサービスは生成されません。 また、RewindableGenerator は列挙のたびに新しいGeneratorを作ります。サービスの結果をキャッシュする機能ではなく、通常の bind() なら再列挙でインスタンスも再生成されます。singleton() や scoped() のサービスでは、それぞれの共有ルールに従います。 一方、コンテキストバインディングの giveTagged() は、解決時に tagged() の結果を iterator_to_array() で配列へ変換します。 配列として一度に依存を解決したい場合は、先ほどのファクトリ付き bind() を次の登録に置き換えられます。
$reports の型が iterable でも、この方法で保持する値は配列です。引数の型だけでは遅延解決を保証できません。また、giveTagged() が行うのはインスタンスの解決であり、各実装の collect() の呼び出しではありません。 Tag 属性の resolve() は tagged() をそのまま返します。属性を使う場合はコンテキスト属性のようにコンストラクター引数へ指定し、クラスの構築をコンテナに任せます。独自ファクトリ内の new に属性が自動適用されるわけではありません。

プラグインの登録契約を壊さない

重複登録と順序を明示する

tag() は一覧への追加処理で、同じ登録キーをもう一度追加しても重複を除去しません。同じ実装が2回列挙されると、singletonで同じオブジェクトが返っても collect() は2回呼ばれます。 タグ登録は、各実装を担当するプロバイダーの1か所に集約します。bindIf() はバインディングの上書きを避けるAPIであり、その後の tag() の重複を防ぐものではありません。 列挙順はタグ一覧へ追加された順です。タグには優先度ソートの処理がないため、異なるパッケージ間の処理順をプロバイダーの読み込み順だけに依存させないでください。順序が公開仕様として必要なら、優先度や識別子をコントラクトに含め、集約側で明示的に並べ替える設計にします。

タグ追加担当のプロバイダーを先に登録する

未登録タグの tagged() は例外ではなく空配列を返します。先に受け取った空配列や、giveTagged() で生成済みの配列へ、後からタグ登録が反映されることはありません。 登録済みタグの遅延列挙についても、実行途中の一覧変更を前提にしないでください。確認した tagged() は件数を取得時に渡す一方、列挙時にはタグ一覧を参照します。全プロバイダーの登録を終えてから使う設計にすると、件数と列挙対象の不一致も避けられます。
タグの列挙は、未読み込みのプロバイダーをタグ名から探す処理ではありません。タグ追加担当を DeferrableProvider にすると、そのサービスが別の経路で解決されるまでタグ登録が実行されず、一覧から抜ける場合があります。常に参加させたい実装のタグ登録には通常のプロバイダーを使い、登録時には実装を生成しない設計にします。
遅延プロバイダーの起動条件はDeferrableProvider、インストール時の読み込みはパッケージ自動検出で確認できます。

タグとサービスのライフサイクルを分ける

タグを付けても、サービスが自動的にsingletonやscopedになるわけではありません。この例は通常の bind() を使い、集約オブジェクトに収集結果を保存しません。 特に giveTagged() で解決した配列をsingletonの集約サービスに保持すると、配列内のscopedサービスも保持したままになり得ます。スコープのリセットは、すでに別オブジェクトへ保存した参照を差し替えません。Octaneやキューワーカーで使う設計はパッケージサービスのライフサイクルも参照してください。

遅延解決と配列化をテストする

パッケージのComposerオートロードとPHPUnit環境で、tests/ReportCollectorTest.php に次のテストを追加できます。コンテナ単体でプロバイダーの登録を呼び、生成回数を検証します。アプリケーションへの統合や自動検出まで含めるテストはOrchestra Testbenchで補います。
さらに、パッケージ更新時には次の組み合わせを確認します。

参照した一次情報

最終更新日 2026年10月26日