跳转到主要内容
有时你希望在保留使用方应用所选服务实现的同时,从包中追加处理。使用容器的 extend(),无需重新定义原有的创建逻辑,就能用装饰器包装已解析的服务。 本页参考 Laravel 官方文档的默认分支 13.x,以及 2026 年 11 月 9 日确认的 Framework 最新版本 v13.40.0 的官方实现与测试。除了与包的服务解析后钩子的区别外,还会说明随 注册时实例是否已被保存 而变化的保留范围。

区分添加配置与替换对象

extend() 的回调会接收目标服务和容器。既可以配置同一个对象后返回,也可以返回新的装饰器。如果忘记 return,后续的解析处理会收到 null。
这里讨论的是 Illuminate\Container\Container::extend()。它与 Cache、Mail 等 Manager 所具有的、用于注册驱动名称和创建逻辑的 extend() 是不同的 API。

创建实现相同契约的装饰器

不替换原有实现而是进行包装

为便于说明,将以下用于格式化消息的契约放在 src/MessageFormatter.php 中。以下各个类也假定分别在 src/ 中创建同名的 PHP 文件。
装饰前的实现会原样返回输入。
装饰器实现相同的接口,并将处理委托给传入的服务。本例中,会在委托对象的返回值前加上包特有的前缀。
如果使用方将具体类 PlainMessageFormatter 作为类型声明,那么仅实现相同接口的装饰器无法替代它。请将公开的依赖统一为 MessageFormatter,并保持参数、返回值、异常等契约。仅靠动态的方法转发,无法替代 PHP 的接口实现。

在提供者中注册装饰

提供装饰的 src/PrefixServiceProvider.php 不会覆盖创建方式,只向容器注册扩展。关于提供者的加载,请参阅包自动发现的内部结构。
回调的第 2 个参数也会传入容器,但本例不需要,因此没有声明。目标服务使用参数 $formatter。如果在回调内再次对同一个键调用 make(),会导致解析循环。 在使用方应用的 AppServiceProvider::register() 等位置绑定原有实现。
extend() 也可以在目标的 bind() 之前注册。如果目标尚未解析且没有已保存的实例,仅注册不会创建原有服务。官方测试中也验证了延迟创建和绑定前的注册。 之后,app(MessageFormatter::class)->format('已发货。') 会返回 [Acme] 已发货。。即使把原有实现换成其他 MessageFormatter 实现,装饰器也会包装那个实现。
原则上请在首次解析之前注册。其他提供者可能会先使用该服务,因此仅仅移到 boot() 并不能防止注册前的解析。另外,如果轻易地把只负责注册扩展的提供者设为延迟提供者,注册本身可能会失去触发条件。

理解应用目标与装饰顺序

在通常的解析中,容器会创建原有服务,按注册顺序替换为各 extender 的返回值,然后作为共享实例保存,并调用解析后钩子。 按 A、B 的顺序注册时,结果为 B(A(原有服务))。使用方的方法调用从外侧的 B 开始,因此不要混淆注册顺序与处理开始顺序。如果依赖多个包的装饰顺序,请在使用方应用中明确顺序,并测试组合。 extender 应用于容器规范化 alias 之后的 解析键。对 MessageFormatter::class 的注册,并不会仅因类型一致就应用于直接解析 PlainMessageFormatter::class 的情况。请统一通过接口使用。

对已保存实例的装饰,保留方式不同

v13.40.0 的 Container::extend() 会根据目标实例是否已保存在容器中而分支。 最后一行很重要。装饰已解析的 singleton 之后,即使调用 forgetInstance() 或重新绑定同一个键,仅当场应用的装饰 也不会在下次创建时重新应用。这与在首次解析前注册到 extender 数组的情况不同。 scoped() 也会使用已保存的实例,因此不要认为在作用域中途延迟注册的装饰也会保留到下一个作用域。请结合包服务的生命周期设计一起确认。 另外,如果之后用 instance() 直接保存对象,它不会经过通常创建时应用 extender 的循环。包括测试中的替换在内,并不一定是”已经注册了,所以传入的实体也会被自动装饰”。

已经分发出去的引用不会被自动替换

像本例这样返回新的包装器时,即使容器的保存值发生变化,已经保存在其他服务属性中的原对象也不会改变。rebinding() 的通知是将新服务传给已注册回调的机制,并不会查找并改写所有依赖方。 此外,通过 rebinding() 注册通知回调也并不总是延迟完成。如果目标已绑定,注册时也会返回 make() 的结果。在对过去解析过的普通绑定执行 extend() 的路径中,如果存在 rebound 回调,也可能为了通知而执行 make()。不要一概认定初始化和副作用的时机是”下次使用时”。

避免双重装饰和状态延续

extend() 不会自动对同一个闭包或同一个装饰器的注册去重。如果对同一个容器应用两次该提供者的 register(),前缀也可能被加上两次。
  • 将装饰注册集中在提供者固定的启动路径中,不要在每个请求或任务中重复。
  • 不要为了防止重复而使用所有容器共享的单个静态标志。测试或新的应用中会缺少必要的注册。
  • singleton 的装饰也会在相同的共享期间内保留。不要把当前用户或请求等固定到包装器中。
  • forgetExtenders() 只会清除注册数组,并不是把已创建的包装器恢复原状的操作。
本例修改了输出字符串。前缀、委托次数和异常处理也会成为包的公开行为,因此不要简单地视为”实现的内部变更”,请确认兼容性。

用同一个容器测试注册时机

以下 PHPUnit 示例假定上述类可以通过包的 autoload 加载。它将提供者的 register() 直接应用于 Container,在不使用 HTTP 或外部服务的情况下确认装饰的保留范围。自动发现和整个应用的启动请另外通过使用 Orchestra Testbench 测试 Laravel 包进行验证。
最后一个测试并不是推荐延迟注册,而是明确所确认版本的边界。不要把 forgetInstance() 单独设计为解除或重新注册装饰的 API。 发布前,还要确认多个 extender 的顺序、重复注册、通过 instance() 的替换,以及使用方已经持有的引用。增加支持的 Laravel 版本时,除了解析键和类型契约外,还要重新阅读 extend() 的分支和官方测试。

相关页面

包的服务解析后钩子

了解不替换对象而进行配置的 callAfterResolving(),以及应用次数的注意事项。

包服务的生命周期设计

了解 bind・singleton・scoped,以及已创建服务所持有的依赖关系。

参考的一手资料

最后修改于 2026年11月9日