> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# パッケージのコンテナタグとプラグイン集約

> Laravel 13のコンテナタグで複数パッケージの実装を集約し、tagged()の遅延解決、giveTagged()との違い、重複登録とライフサイクルを検証します。

複数のパッケージや利用アプリケーションから、同じ集約サービスへ機能を追加できるようにしたい場合、コンテナの `tag()` と `tagged()` を使えます。集約する側がすべての実装クラスを知る必要はありません。

このページはLaravel 13を対象とし、Framework `v13.38.0` の実装と公式テストを確認しています。バインディングの基本は[サービスコンテナ](https://kawax.biz/jp/service-container.md)、プロバイダーの配布方法は[パッケージ開発](https://kawax.biz/jp/advanced/package-development.md)を参照してください。

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

`tag()` は、クラス名などのコンテナ登録キーをタグの一覧に追加します。タグを付けるだけでは、そのサービスを生成しません。

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

```mermaid theme={null}
flowchart TD
    A["各プロバイダーのregister()"] --> B["実装をbind()し<br>同じタグに追加"]
    B --> C["集約サービスを解決"]
    C --> D["tagged()のiterableを保持"]
    D --> E["foreachで列挙"]
    E --> F["登録キーごとにmake()"]
    F --> G["実装のcollect()を呼ぶ"]
```

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

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

例として、複数のレポートを集める `acme/reports` パッケージを作ります。`Acme\Reports\` が `src/` に対応するComposerのPSR-4設定を前提とします。

`src/Contracts/Report.php` に、プラグインが実装するコントラクトを定義します。

```php theme={null}
<?php

namespace Acme\Reports\Contracts;

interface Report
{
    public function collect(): array;
}
```

最小限の実装として、`src/RuntimeReport.php` と `src/MemoryReport.php` を用意します。

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;

class RuntimeReport implements Report
{
    public function collect(): array
    {
        return ['php_version' => PHP_VERSION];
    }
}
```

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;

class MemoryReport implements Report
{
    public function collect(): array
    {
        return ['allocated_bytes' => memory_get_usage(true)];
    }
}
```

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

`src/ReportCollector.php` は、各実装を列挙するときに収集処理を呼びます。公開するタグ名を定数にすると、利用側も同じ名前を参照できます。

```php theme={null}
<?php

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;
use UnexpectedValueException;

class ReportCollector
{
    public const TAG = 'acme.reports.collectors';

    /** @param iterable<Report> $reports */
    public function __construct(private iterable $reports)
    {
    }

    public function collect(): array
    {
        $results = [];

        foreach ($this->reports as $report) {
            if (! $report instanceof Report) {
                throw new UnexpectedValueException('Tagged services must implement Report.');
            }

            $results[] = $report->collect();
        }

        return $results;
    }
}
```

PHPの `iterable` 型だけでは要素の型を検証できず、`tag()` 自身も `Report` の実装かどうかを確認しません。この例では集約する側でもコントラクトを確認しています。

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

`src/ReportsServiceProvider.php` で、標準実装と集約サービスを登録します。集約サービスのファクトリは、取得した `iterable` をそのまま渡します。

```php theme={null}
<?php

namespace Acme\Reports;

use Illuminate\Contracts\Container\Container;
use Illuminate\Support\ServiceProvider;

class ReportsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(RuntimeReport::class);
        $this->app->bind(MemoryReport::class);

        $this->app->tag([
            RuntimeReport::class,
            MemoryReport::class,
        ], ReportCollector::TAG);

        $this->app->bind(ReportCollector::class, function (Container $app): ReportCollector {
            return new ReportCollector($app->tagged(ReportCollector::TAG));
        });
    }
}
```

別パッケージや利用アプリケーションは、`Report` を実装した `DailyReport` などを用意し、自分のプロバイダーの `register()` 内で同じタグに追加します。

```php theme={null}
use Acme\Reports\ReportCollector;
use App\Reports\DailyReport;

$this->app->bind(DailyReport::class);
$this->app->tag(DailyReport::class, ReportCollector::TAG);
```

すべてのプロバイダーの登録後、`app(ReportCollector::class)->collect()` を呼ぶと、追加した実装も集約されます。参加する実装が増えても、`ReportCollector` の生成処理を変更する必要はありません。

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

確認した実装では、登録済みタグの `tagged()` は `RewindableGenerator` を返します。各サービスの `make()` は列挙時に呼ばれ、途中で列挙を止めれば残りのサービスは生成されません。

また、`RewindableGenerator` は列挙のたびに新しいGeneratorを作ります。サービスの結果をキャッシュする機能ではなく、通常の `bind()` なら再列挙でインスタンスも再生成されます。`singleton()` や `scoped()` のサービスでは、それぞれの共有ルールに従います。

一方、コンテキストバインディングの `giveTagged()` は、解決時に `tagged()` の結果を `iterator_to_array()` で配列へ変換します。

| 注入方法 | 個々のサービスが解決されるタイミング | 同じ集約オブジェクトで再列挙 |
| - | - | - |
| `tagged()` の結果をそのまま渡す | 各要素を列挙するとき | 再び `make()` する |
| `#[Tag(...)]` を付けた `iterable` 引数 | 各要素を列挙するとき | 再び `make()` する |
| `giveTagged()` で注入する | 集約サービスの依存を解決するとき | 保持した配列の要素を再利用する |

配列として一度に依存を解決したい場合は、先ほどのファクトリ付き `bind()` を次の登録に置き換えられます。

```php theme={null}
use Acme\Reports\ReportCollector;

$this->app->bind(ReportCollector::class);

$this->app->when(ReportCollector::class)
    ->needs('$reports')
    ->giveTagged(ReportCollector::TAG);
```

`$reports` の型が `iterable` でも、この方法で保持する値は配列です。引数の型だけでは遅延解決を保証できません。また、`giveTagged()` が行うのはインスタンスの解決であり、各実装の `collect()` の呼び出しではありません。

`Tag` 属性の `resolve()` は `tagged()` をそのまま返します。属性を使う場合は[コンテキスト属性](https://kawax.biz/jp/service-container.md#コンテキスト属性)のようにコンストラクター引数へ指定し、クラスの構築をコンテナに任せます。独自ファクトリ内の `new` に属性が自動適用されるわけではありません。

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

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

`tag()` は一覧への追加処理で、同じ登録キーをもう一度追加しても重複を除去しません。同じ実装が2回列挙されると、singletonで同じオブジェクトが返っても `collect()` は2回呼ばれます。

タグ登録は、各実装を担当するプロバイダーの1か所に集約します。`bindIf()` はバインディングの上書きを避けるAPIであり、その後の `tag()` の重複を防ぐものではありません。

列挙順はタグ一覧へ追加された順です。タグには優先度ソートの処理がないため、異なるパッケージ間の処理順をプロバイダーの読み込み順だけに依存させないでください。順序が公開仕様として必要なら、優先度や識別子をコントラクトに含め、集約側で明示的に並べ替える設計にします。

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

未登録タグの `tagged()` は例外ではなく空配列を返します。先に受け取った空配列や、`giveTagged()` で生成済みの配列へ、後からタグ登録が反映されることはありません。

登録済みタグの遅延列挙についても、実行途中の一覧変更を前提にしないでください。確認した `tagged()` は件数を取得時に渡す一方、列挙時にはタグ一覧を参照します。全プロバイダーの登録を終えてから使う設計にすると、件数と列挙対象の不一致も避けられます。

> [!WARNING]
>
> タグの列挙は、未読み込みのプロバイダーをタグ名から探す処理ではありません。タグ追加担当を `DeferrableProvider` にすると、そのサービスが別の経路で解決されるまでタグ登録が実行されず、一覧から抜ける場合があります。常に参加させたい実装のタグ登録には通常のプロバイダーを使い、登録時には実装を生成しない設計にします。

遅延プロバイダーの起動条件は[DeferrableProvider](https://kawax.biz/jp/advanced/deferred-provider.md)、インストール時の読み込みは[パッケージ自動検出](https://kawax.biz/jp/advanced/package-discovery.md)で確認できます。

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

タグを付けても、サービスが自動的にsingletonやscopedになるわけではありません。この例は通常の `bind()` を使い、集約オブジェクトに収集結果を保存しません。

特に `giveTagged()` で解決した配列をsingletonの集約サービスに保持すると、配列内のscopedサービスも保持したままになり得ます。スコープのリセットは、すでに別オブジェクトへ保存した参照を差し替えません。Octaneやキューワーカーで使う設計は[パッケージサービスのライフサイクル](https://kawax.biz/jp/advanced/package-service-lifetimes.md)も参照してください。

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

パッケージのComposerオートロードとPHPUnit環境で、`tests/ReportCollectorTest.php` に次のテストを追加できます。コンテナ単体でプロバイダーの登録を呼び、生成回数を検証します。アプリケーションへの統合や自動検出まで含めるテストは[Orchestra Testbench](https://kawax.biz/jp/advanced/package-testing.md)で補います。

```php theme={null}
<?php

namespace Acme\Reports\Tests;

use Acme\Reports\ReportCollector;
use Acme\Reports\ReportsServiceProvider;
use Acme\Reports\RuntimeReport;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class ReportCollectorTest extends TestCase
{
    public function test_tagged_reports_are_lazy_and_can_be_resolved_again(): void
    {
        $created = 0;
        $app = $this->makeContainer($created);
        $collector = $app->make(ReportCollector::class);

        $this->assertSame(0, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertSame(1, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertSame(2, $created);
    }

    public function test_give_tagged_resolves_an_array_once(): void
    {
        $created = 0;
        $app = $this->makeContainer($created);
        $app->bind(ReportCollector::class);
        $app->when(ReportCollector::class)
            ->needs('$reports')
            ->giveTagged(ReportCollector::TAG);

        $collector = $app->make(ReportCollector::class);

        $this->assertSame(1, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertCount(2, $collector->collect());
        $this->assertSame(1, $created);
    }

    private function makeContainer(int &$created): Container
    {
        $app = new Container;
        (new ReportsServiceProvider($app))->register();
        $app->bind(RuntimeReport::class, function () use (&$created): RuntimeReport {
            $created++;

            return new RuntimeReport;
        });

        return $app;
    }
}
```

さらに、パッケージ更新時には次の組み合わせを確認します。

| ケース | 確認すること |
| - | - |
| 追加実装を同じタグへ登録 | 集約対象に含まれ、コントラクトの型チェックを通る |
| 同じ実装を重複登録 | 収集処理が重複することを検出できる |
| タグ未登録・誤ったタグ名 | 空一覧を許容するか、パッケージ側でエラーにするかが明確 |
| 別のリクエスト・ジョブで再利用 | scopedサービスや収集結果を持ち越さない |
| タグ名・コントラクトを変更 | 利用側のプロバイダーが参照する公開APIとして互換性を評価する |

## 参照した一次情報

* [Laravel公式ドキュメント: Tagging](https://github.com/laravel/docs/blob/13.x/container.md#tagging)
* [Laravel公式ドキュメント: Binding primitives・Variadic tag dependencies](https://github.com/laravel/docs/blob/13.x/container.md#binding-primitives)
* [Laravel公式ドキュメント: サービスプロバイダー](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Laravel公式ドキュメント: パッケージ開発](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container: tag()・tagged()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Container.php)
* [ContextualBindingBuilder: giveTagged()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/ContextualBindingBuilder.php)
* [RewindableGenerator: 再列挙と件数](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/RewindableGenerator.php)
* [Tag属性: resolve()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Attributes/Tag.php)
* [公式テスト: ContainerTaggingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContainerTaggingTest.php)
* [公式テスト: ContextualBindingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContextualBindingTest.php)


## Related topics

- [サービスコンテナ](https://kawax.biz/jp/service-container.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [Laravelパッケージ開発](https://kawax.biz/jp/advanced/package-development.md)
- [パッケージのArtisanコマンド登録と依存解決](https://kawax.biz/jp/advanced/package-commands.md)
- [パッケージのサービス解決後フック](https://kawax.biz/jp/advanced/package-service-resolution.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.