Skip to main content
要让包不仅能在常规 HTTP 请求中使用,也能在 Octane 和队列 worker 中使用,就需要确定服务可以在多长时间内保存什么。如果把每个请求的数据保存到 singleton 中,其他处理也可能引用到旧数据。 本页基于 Laravel Framework v13.36.0 的容器、队列 worker 以及 Octane 的实现,将服务容器的基本注册方式与包的长期维护联系起来。仅仅选择 scoped(),并不会重置对象之间的所有引用和静态状态。

根据服务的保存期限选择注册方式

此表比较的是常规解析。显式传入参数的解析、contextual binding 等特殊路径请另行确认。 即使使用 bind(),如果将获取到的服务保存到其他 singleton 的属性中,该引用也会保留下来。注册方式是容器的创建与复用规则,而不是强制重新创建使用方所持有对象的规则。

scoped 将共享注册与移除对象的记录相结合

所确认的 Container::scoped() 会执行以下两件事:
  1. 将注册键记录到 scopedInstances 中。
  2. 调用 singleton(),作为共享绑定进行注册。
实例本身与 singleton 一样保存在 instances 中。forgetScopedInstances() 会对已记录的键执行 unset(),移除已保存的实例。绑定的创建逻辑仍然保留,因此在下一次常规解析时可以创建新的对象。 该处理不会初始化旧对象的属性,也不会改写其他对象所持有的引用。仍被引用的旧对象依然可以继续使用。

边界由运行环境创建

容器本身并不会监视 HTTP 请求或任务的开始。
  • 队列 worker:QueueServiceProvider 会将重置作用域的回调传给 Worker。Worker::daemon() 在获取下一个任务之前的循环中调用它,并在回调中执行 forgetScopedInstances() 以及清除 Facade 的已解析实例等操作。
  • Octane:官方默认配置会在 OperationTerminated 上注册 FlushTemporaryContainerInstances。该监听器会调用 forgetScopedInstances()。
  • 自定义的常驻命令或循环:仅仅重复调用 make() 并不会形成边界。需要在运行基础设施一侧设计生命周期。
请不要在常规的应用程序处理或包的 boot() 中自行调用 forgetScopedInstances()。这会影响整个容器中的 scoped 服务。此外,包括同步执行在内的所有任务路径,并不一定都会经过与常驻 Worker 相同的重置路径。

将处理内共享的状态设为 scoped

假设一个配送包希望在单个请求或任务内共享配送目的地的标识符。不直接保存 HTTP 的 Request,而只保存所需的值。
在包的服务提供者中,创建时不读取使用方的状态,而是注册一个空的 context。
在使用该包的应用程序的控制器或任务的 handle() 中,设置已完成输入验证和权限确认的标识符。在同一生命周期内解析该类型的其他服务,可以引用同一个 context。
此 context 并不是授权功能。能够设置标识符,与被允许访问该配送目的地是两回事。另外,scoped() 也不是持久化到数据库或外部缓存的机制。需要传递给下一个任务的值,请作为任务数据显式传递。
在 Octane 中,服务提供者的 register() 和 boot() 会在 worker 初次启动时执行。请不要设计成在 boot() 中将当前用户或配送目的地保存到 context 中。每次处理的值应在处理开始后再设置。

从 singleton 到 scoped 的引用不会自动更新

下面的注册即使只将 context 设为 scoped,也无法达到预期效果。
如果 DeliveryService 在构造函数中将 context 保存到属性中,它就会一直持有最初创建的 context。即使从容器中移除了已保存的 context,已经创建的 DeliveryService 的属性也不会改变。 应根据服务的职责选择对策。
  1. 通过方法参数传递每次处理所需的值。如果不需要共享状态,这是最简单的方式。
  2. 将持有方也设为 scoped。按处理单位同时重新创建 DeliveryService 和 context。
  3. 传入在使用时进行解析的 resolver。如果需要保持 singleton,则每次都从当前容器获取所需的服务。
在第 3 种方式的示例中,不是将旧的 context 捕获到闭包中,而是在调用时进行解析。
此示例中的 DeliveryService 需要改为在构造函数中接收 Closure,并提供每次操作时调用它的 API。如果在构造函数中只调用一次 resolver 并保存其结果,问题就无法解决。这是将官方 Octane 文档中的 container resolver 应用到包专属 context 的设计示例。

也要确认 scoped 无法清除的状态

scoped() 不会自动移除以下状态:
  • 添加到包自身静态属性中的值
  • 保存在其他 singleton 或全局变量中的 context 引用
  • 保存在 singleton 内部的”当前用户""当前配送目的地”
  • 自定义 Facade 或缓存层所持有的引用
请区分标准运行基础设施所进行的重置与包自身状态的重置。减少保存每次处理状态的位置,比在所有运行环境中维护自定义的清除处理更简单。

编写在同一容器中跨越边界的测试

如果 HTTP 测试只是每次都创建新的应用程序,就可能遗漏常驻进程持续使用同一容器所引发的问题。首先在容器单元层面验证同一处理内的共享以及边界之后的重新创建。 以下测试采用 PHPUnit 格式。第一个测试与官方的 Container 测试一样使用 forgetScopedInstances(),在不启动整个 Worker 的情况下确认边界的作用。
第二个测试记录的是引用会被保留这一行为。在实际的包中,还应添加测试,验证所采用的对策会使用新的 context 或值。 容器单元测试并不能验证 Octane 的 sandbox 或队列 worker 的实际执行路径。还需要在声明支持的运行环境中进行连续处理的集成测试。在变更了注册方式或服务实现的部署中,请重新加载常驻进程以应用新代码。

相关页面

包的服务解析后钩子

了解 afterResolving、共享实例的获取以及重新解析时配置的差异。

Laravel Octane

了解 Request 与容器的持有,以及常驻进程运维上的注意事项。

参考的一手资料

最后修改于 2026年10月14日