> ## 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-CN/service-container.md)，服务提供者的分发方式请参阅[包开发](https://kawax.biz/zh-CN/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()` 的服务则遵循各自的共享规则。

另一方面，上下文绑定的 `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-CN/service-container.md#上下文属性)那样将其指定在构造函数参数上，并把类的构建交给容器。在自定义工厂中通过 `new` 创建时，属性不会被自动应用。

## 不要破坏插件的注册约定

### 明确重复注册与顺序

`tag()` 是向列表追加的操作，即使再次添加相同的注册键也不会去重。如果同一实现被遍历两次，即便 singleton 返回的是同一个对象，`collect()` 也会被调用两次。

请将标签注册集中在负责各实现的服务提供者中的一个位置。`bindIf()` 是用于避免覆盖绑定的 API，并不能防止之后 `tag()` 的重复。

遍历顺序就是添加到标签列表的顺序。标签没有按优先级排序的处理，因此不要让不同包之间的处理顺序仅依赖于服务提供者的加载顺序。如果顺序需要作为公开规范，请在契约中包含优先级或标识符，并设计为由聚合一方显式排序。

### 先注册负责添加标签的服务提供者

对未注册的标签调用 `tagged()` 不会抛出异常，而是返回空数组。之后的标签注册不会反映到先前获取的空数组，或已通过 `giveTagged()` 创建的数组中。

对于已注册标签的延迟遍历，也不要以执行过程中列表发生变化为前提。所确认的 `tagged()` 在获取时传入数量，而在遍历时引用标签列表。设计为在所有服务提供者注册完成后再使用，也可以避免数量与遍历对象不一致的问题。

> [!WARNING]
>
> 标签的遍历并不会根据标签名查找尚未加载的服务提供者。如果将负责添加标签的服务提供者设为 `DeferrableProvider`，在该服务通过其他途径被解析之前，标签注册不会执行，实现可能会从列表中缺失。对于始终需要参与的实现，请使用常规服务提供者进行标签注册，并设计为在注册时不创建实现。

延迟服务提供者的启动条件请参阅 [DeferrableProvider](https://kawax.biz/zh-CN/advanced/deferred-provider.md)，安装时的加载请参阅[包自动发现](https://kawax.biz/zh-CN/advanced/package-discovery.md)。

### 区分标签与服务的生命周期

添加标签并不会让服务自动成为 singleton 或 scoped。本例使用常规的 `bind()`，并且不在聚合对象中保存收集结果。

特别是，如果将通过 `giveTagged()` 解析得到的数组保存在 singleton 的聚合服务中，数组内的 scoped 服务也可能一直被持有。作用域的重置不会替换已经保存到其他对象中的引用。关于在 Octane 或队列 worker 中使用的设计，也请参阅[包服务的生命周期设计](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)。

## 测试延迟解析与数组化

在包的 Composer 自动加载与 PHPUnit 环境中，可以在 `tests/ReportCollectorTest.php` 中添加以下测试。它仅使用容器调用服务提供者的注册，并验证创建次数。包含应用程序集成与自动发现在内的测试，请使用 [Orchestra Testbench](https://kawax.biz/zh-CN/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/zh-CN/service-container.md)
- [进阶主题](https://kawax.biz/zh-CN/advanced/index.md)
- [包的 CHANGELOG 与发布管理](https://kawax.biz/zh-CN/advanced/package-changelog.md)
- [包的公开资源与更新](https://kawax.biz/zh-CN/advanced/package-assets.md)
- [包服务的生命周期设计](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)


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