tag() 和 tagged()。负责聚合的一方无需知道所有实现类。
本页面向 Laravel 13,基于对 Framework v13.38.0 的实现与官方测试的确认。绑定的基础知识请参阅服务容器,服务提供者的分发方式请参阅包开发。
标签用于归集服务的注册键
tag() 会将类名等容器注册键添加到标签列表中。仅仅添加标签并不会创建该服务。
tagged() 会返回一个用于遍历该标签下已注册服务的 iterable。它并不是自动查找实现了特定接口的类的功能,而是由各个服务提供者显式加入的机制。它也与 vendor:publish --tag 中用于公开资源的标签无关。
创建可扩展的报告包
准备实现共享的契约
作为示例,我们创建一个汇总多个报告的acme/reports 包。前提是 Composer 的 PSR-4 配置将 Acme\Reports\ 映射到 src/。
在 src/Contracts/Report.php 中定义插件需要实现的契约。
src/RuntimeReport.php 和 src/MemoryReport.php。
聚合服务接收 iterable
src/ReportCollector.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() 会在解析时通过 iterator_to_array() 将 tagged() 的结果转换为数组。
如果希望以数组形式一次性解析依赖,可以将前面带工厂的
bind() 替换为下面的注册方式。
$reports 的类型是 iterable,通过这种方式持有的值也是数组。仅凭参数类型无法保证延迟解析。此外,giveTagged() 执行的是实例的解析,而不是调用各实现的 collect()。
Tag 属性的 resolve() 会直接返回 tagged()。使用属性时,请像上下文属性那样将其指定在构造函数参数上,并把类的构建交给容器。在自定义工厂中通过 new 创建时,属性不会被自动应用。
不要破坏插件的注册约定
明确重复注册与顺序
tag() 是向列表追加的操作,即使再次添加相同的注册键也不会去重。如果同一实现被遍历两次,即便 singleton 返回的是同一个对象,collect() 也会被调用两次。
请将标签注册集中在负责各实现的服务提供者中的一个位置。bindIf() 是用于避免覆盖绑定的 API,并不能防止之后 tag() 的重复。
遍历顺序就是添加到标签列表的顺序。标签没有按优先级排序的处理,因此不要让不同包之间的处理顺序仅依赖于服务提供者的加载顺序。如果顺序需要作为公开规范,请在契约中包含优先级或标识符,并设计为由聚合一方显式排序。
先注册负责添加标签的服务提供者
对未注册的标签调用tagged() 不会抛出异常,而是返回空数组。之后的标签注册不会反映到先前获取的空数组,或已通过 giveTagged() 创建的数组中。
对于已注册标签的延迟遍历,也不要以执行过程中列表发生变化为前提。所确认的 tagged() 在获取时传入数量,而在遍历时引用标签列表。设计为在所有服务提供者注册完成后再使用,也可以避免数量与遍历对象不一致的问题。
延迟服务提供者的启动条件请参阅 DeferrableProvider,安装时的加载请参阅包自动发现。
区分标签与服务的生命周期
添加标签并不会让服务自动成为 singleton 或 scoped。本例使用常规的bind(),并且不在聚合对象中保存收集结果。
特别是,如果将通过 giveTagged() 解析得到的数组保存在 singleton 的聚合服务中,数组内的 scoped 服务也可能一直被持有。作用域的重置不会替换已经保存到其他对象中的引用。关于在 Octane 或队列 worker 中使用的设计,也请参阅包服务的生命周期设计。
测试延迟解析与数组化
在包的 Composer 自动加载与 PHPUnit 环境中,可以在tests/ReportCollectorTest.php 中添加以下测试。它仅使用容器调用服务提供者的注册,并验证创建次数。包含应用程序集成与自动发现在内的测试,请使用 Orchestra Testbench 进行补充。