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 不会覆盖创建方式,只向容器注册扩展。关于提供者的加载,请参阅包自动发现的内部结构。
$formatter。如果在回调内再次对同一个键调用 make(),会导致解析循环。
在使用方应用的 AppServiceProvider::register() 等位置绑定原有实现。
extend() 也可以在目标的 bind() 之前注册。如果目标尚未解析且没有已保存的实例,仅注册不会创建原有服务。官方测试中也验证了延迟创建和绑定前的注册。
之后,app(MessageFormatter::class)->format('已发货。') 会返回 [Acme] 已发货。。即使把原有实现换成其他 MessageFormatter 实现,装饰器也会包装那个实现。
理解应用目标与装饰顺序
在通常的解析中,容器会创建原有服务,按注册顺序替换为各 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,以及已创建服务所持有的依赖关系。