> ## 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/zh-TW/service-container.md)，提供者的發布方式請參閱[套件開發](https://kawax.biz/zh-TW/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` 套件。前提是 Composer 的 PSR-4 設定中，`Acme\Reports\` 對應到 `src/`。

在 `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()` 的服務則依照各自的共用規則。

另一方面，contextual binding 的 `giveTagged()` 會在解析時以 `iterator_to_array()` 將 `tagged()` 的結果轉換為陣列。

| 注入方式 | 個別服務被解析的時機 | 在同一個彙整物件上重新列舉 |
| - | - | - |
| 直接傳入 `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/zh-TW/service-container.md#情境式屬性)那樣指定在建構子參數上，並將類別的建構交給容器。屬性並不會自動套用到自訂工廠內的 `new`。

## 不破壞外掛的註冊契約

### 明確處理重複註冊與順序

`tag()` 是加入清單的處理，即使再次加入相同的註冊鍵，也不會移除重複項目。若同一個實作被列舉兩次，即使 singleton 回傳的是同一個物件，`collect()` 仍會被呼叫兩次。

請將標籤註冊集中在負責各實作的提供者中的單一位置。`bindIf()` 是避免覆寫綁定的 API，並不能防止之後 `tag()` 的重複。

列舉順序就是加入標籤清單的順序。標籤沒有依優先度排序的處理，因此請勿讓不同套件之間的處理順序僅依賴提供者的載入順序。若順序需要作為公開規格，請將優先度或識別碼納入契約，並設計成由彙整端明確排序。

### 先註冊負責新增標籤的提供者

未註冊標籤的 `tagged()` 不會拋出例外，而是回傳空陣列。之後的標籤註冊，並不會反映到先前取得的空陣列，或已由 `giveTagged()` 建立的陣列中。

對於已註冊標籤的延遲列舉，也請勿以執行途中變更清單為前提。所確認的 `tagged()` 會在取得時傳入數量，而在列舉時才參照標籤清單。若設計成在所有提供者註冊完成後才使用，也能避免數量與列舉對象不一致。

> [!WARNING]
>
> 標籤的列舉並不會依標籤名稱去尋找尚未載入的提供者。若將負責新增標籤的提供者設為 `DeferrableProvider`，在該服務透過其他途徑被解析之前，標籤註冊都不會執行，可能會從清單中遺漏。對於希望一律參與的實作，請使用一般的提供者進行標籤註冊，並設計成在註冊時不建立實作。

延遲提供者的啟動條件可參閱 [DeferrableProvider](https://kawax.biz/zh-TW/advanced/deferred-provider.md)，安裝時的載入則可參閱[套件自動探索](https://kawax.biz/zh-TW/advanced/package-discovery.md)。

### 將標籤與服務的生命週期分開

加上標籤並不會讓服務自動成為 singleton 或 scoped。本範例使用一般的 `bind()`，也不會將收集結果儲存在彙整物件中。

特別是若將以 `giveTagged()` 解析的陣列保存在 singleton 的彙整服務中，陣列內的 scoped 服務也可能一直被保留。重設 scope 並不會替換已儲存在其他物件中的參照。在 Octane 或佇列 worker 中使用的設計，也請參閱[套件服務的生命週期設計](https://kawax.biz/zh-TW/advanced/package-service-lifetimes.md)。

## 測試延遲解析與陣列化

在套件的 Composer 自動載入與 PHPUnit 環境中，可以在 `tests/ReportCollectorTest.php` 加入以下測試。這裡僅使用容器呼叫提供者的註冊，並驗證建立次數。包含整合到應用程式與自動探索在內的測試，請以 [Orchestra Testbench](https://kawax.biz/zh-TW/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;
    }
}
```

此外，更新套件時請確認以下組合。

| 情境 | 確認事項 |
| - | - |
| 將新增的實作註冊到相同標籤 | 會被納入彙整對象，並通過契約的型別檢查 |
| 重複註冊相同實作 | 能偵測到收集處理重複執行 |
| 標籤未註冊・標籤名稱錯誤 | 明確決定是允許空清單，還是由套件端視為錯誤 |
| 在其他請求・Job 中重複使用 | 不延續 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/zh-TW/advanced/index.md)
- [Service Container](https://kawax.biz/zh-TW/service-container.md)
- [套件服務的生命週期設計](https://kawax.biz/zh-TW/advanced/package-service-lifetimes.md)
- [套件的服務解析後掛鉤](https://kawax.biz/zh-TW/advanced/package-service-resolution.md)
- [我的套件](https://kawax.biz/zh-TW/packages/index.md)


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