v13.36.0 的容器、队列 worker 以及 Octane 的实现,将服务容器的基本注册方式与包的长期维护联系起来。仅仅选择 scoped(),并不会重置对象之间的所有引用和静态状态。
根据服务的保存期限选择注册方式
此表比较的是常规解析。显式传入参数的解析、contextual binding 等特殊路径请另行确认。
即使使用
bind(),如果将获取到的服务保存到其他 singleton 的属性中,该引用也会保留下来。注册方式是容器的创建与复用规则,而不是强制重新创建使用方所持有对象的规则。
scoped 将共享注册与移除对象的记录相结合
所确认的Container::scoped() 会执行以下两件事:
- 将注册键记录到
scopedInstances中。 - 调用
singleton(),作为共享绑定进行注册。
instances 中。forgetScopedInstances() 会对已记录的键执行 unset(),移除已保存的实例。绑定的创建逻辑仍然保留,因此在下一次常规解析时可以创建新的对象。
该处理不会初始化旧对象的属性,也不会改写其他对象所持有的引用。仍被引用的旧对象依然可以继续使用。
边界由运行环境创建
容器本身并不会监视 HTTP 请求或任务的开始。- 队列 worker:
QueueServiceProvider会将重置作用域的回调传给 Worker。Worker::daemon()在获取下一个任务之前的循环中调用它,并在回调中执行forgetScopedInstances()以及清除 Facade 的已解析实例等操作。 - Octane:官方默认配置会在
OperationTerminated上注册FlushTemporaryContainerInstances。该监听器会调用forgetScopedInstances()。 - 自定义的常驻命令或循环:仅仅重复调用
make()并不会形成边界。需要在运行基础设施一侧设计生命周期。
将处理内共享的状态设为 scoped
假设一个配送包希望在单个请求或任务内共享配送目的地的标识符。不直接保存 HTTP 的 Request,而只保存所需的值。handle() 中,设置已完成输入验证和权限确认的标识符。在同一生命周期内解析该类型的其他服务,可以引用同一个 context。
scoped() 也不是持久化到数据库或外部缓存的机制。需要传递给下一个任务的值,请作为任务数据显式传递。
在 Octane 中,服务提供者的
register() 和 boot() 会在 worker 初次启动时执行。请不要设计成在 boot() 中将当前用户或配送目的地保存到 context 中。每次处理的值应在处理开始后再设置。从 singleton 到 scoped 的引用不会自动更新
下面的注册即使只将 context 设为 scoped,也无法达到预期效果。DeliveryService 在构造函数中将 context 保存到属性中,它就会一直持有最初创建的 context。即使从容器中移除了已保存的 context,已经创建的 DeliveryService 的属性也不会改变。
应根据服务的职责选择对策。
- 通过方法参数传递每次处理所需的值。如果不需要共享状态,这是最简单的方式。
- 将持有方也设为 scoped。按处理单位同时重新创建
DeliveryService和 context。 - 传入在使用时进行解析的 resolver。如果需要保持 singleton,则每次都从当前容器获取所需的服务。
DeliveryService 需要改为在构造函数中接收 Closure,并提供每次操作时调用它的 API。如果在构造函数中只调用一次 resolver 并保存其结果,问题就无法解决。这是将官方 Octane 文档中的 container resolver 应用到包专属 context 的设计示例。
也要确认 scoped 无法清除的状态
scoped() 不会自动移除以下状态:
- 添加到包自身静态属性中的值
- 保存在其他 singleton 或全局变量中的 context 引用
- 保存在 singleton 内部的”当前用户""当前配送目的地”
- 自定义 Facade 或缓存层所持有的引用
编写在同一容器中跨越边界的测试
如果 HTTP 测试只是每次都创建新的应用程序,就可能遗漏常驻进程持续使用同一容器所引发的问题。首先在容器单元层面验证同一处理内的共享以及边界之后的重新创建。 以下测试采用 PHPUnit 格式。第一个测试与官方的 Container 测试一样使用forgetScopedInstances(),在不启动整个 Worker 的情况下确认边界的作用。
容器单元测试并不能验证 Octane 的 sandbox 或队列 worker 的实际执行路径。还需要在声明支持的运行环境中进行连续处理的集成测试。在变更了注册方式或服务实现的部署中,请重新加载常驻进程以应用新代码。
相关页面
包的服务解析后钩子
了解 afterResolving、共享实例的获取以及重新解析时配置的差异。
Laravel Octane
了解 Request 与容器的持有,以及常驻进程运维上的注意事项。
参考的一手资料
- Laravel 官方文档:Binding scoped singletons
- Laravel 官方文档:Dependency injection and Octane
- Container:scoped()、singleton()、forgetScopedInstances()、resolve()
- ContainerTest:scoped 的共享与重新创建
- QueueServiceProvider:传给 Worker 的 resetScope
- Worker:daemon() 中的作用域重置
- Octane:FlushTemporaryContainerInstances
- Octane:OperationTerminated 的默认监听器