v13.36.0, and connects the basic registration methods of the service container to the long-term maintenance of a package. Simply choosing scoped() does not reset every reference between objects or every piece of static state.
Choose a registration method based on how long the service lives
This table compares regular resolution. Check special paths such as resolution with explicit parameters or contextual binding separately.
Even with
bind(), if you store the retrieved service in a property of another singleton, that reference remains. Registration methods are rules for how the container creates and reuses objects; they do not force objects held by consumers to be recreated.
scoped combines a shared registration with a record of what to remove
TheContainer::scoped() method we examined does two things:
- It records the registration key in
scopedInstances. - It calls
singleton()and registers the service as a shared binding.
instances array as singletons. forgetScopedInstances() calls unset() on the stored instances for the recorded keys. Because the binding’s factory remains, the next regular resolution can create a new object.
This process does not reset the properties of the old object, nor does it rewrite references held by other objects. An old object that is still referenced can continue to be used.
The runtime creates the boundary
The container itself does not watch for the start of HTTP requests or jobs.- Queue workers:
QueueServiceProviderpasses a callback that resets the scope to the Worker.Worker::daemon()calls it in the loop before fetching the next job, and the callback performs tasks such asforgetScopedInstances()and clearing resolved Facade instances. - Octane: The official default configuration registers
FlushTemporaryContainerInstancesforOperationTerminated. This listener callsforgetScopedInstances(). - Custom long-running commands or loops: Simply calling
make()repeatedly does not create a boundary. The runtime must design the lifecycle.
Make state shared within an operation scoped
Consider a delivery package that wants to share a destination identifier within a single request or job. Rather than holding the HTTP Request directly, it stores only the value it needs.handle(), set an identifier that has already passed input validation and authorization checks. Other services that resolve this type within the same lifecycle can refer to the same context.
scoped() does not persist anything to a database or an external cache. Pass values needed by the next job explicitly as job data.
In Octane, a service provider’s
register() and boot() run when the worker first starts. Do not design your package to store the current user or destination in the context from boot(). Set per-operation values after the operation has started.References from a singleton to a scoped service are not updated automatically
With the following registration, making only the context scoped does not work as intended.DeliveryService stores the context in a property in its constructor, it keeps holding the first context it was created with. Even if the container removes its stored context, the property of the already created DeliveryService does not change.
Choose a remedy that fits the service’s responsibilities.
- Pass the values needed for each operation as method arguments. If there is no need to share state, this is the simplest option.
- Make the holder scoped as well. Recreate both
DeliveryServiceand the context per operation. - Pass a resolver that resolves at the time of use. If the service must remain a singleton, retrieve the needed service from the current container each time.
DeliveryService in this example must be changed to accept a Closure in its constructor and expose an API that calls it on every operation. Calling the resolver only once in the constructor and storing the result does not solve the problem. This is a design example that applies the container resolver from the official Octane documentation to a package-specific context.
Check for state that scoped does not clear
scoped() does not automatically clear the following state:
- Values added to the package’s own static properties
- References to the context stored in another singleton or a global variable
- A “current user” or “current destination” held inside a singleton
- References held by custom Facades or cache layers
Write tests that cross a boundary in the same container
If your HTTP tests only create a new application each time, you may miss problems that arise when a long-running process keeps using the same container. Start by verifying, with the container alone, that state is shared within an operation and recreated after the boundary. The following tests use PHPUnit. Like the official Container tests, the first test usesforgetScopedInstances() to check the effect of the boundary without starting an entire Worker.
Container-only tests do not verify Octane’s sandbox or the actual execution path of queue workers. You also need integration tests that run consecutive operations in each runtime you claim to support. For deployments that change registration methods or service implementations, reload long-running processes so that the new code takes effect.
Related pages
Package service resolution hooks
Review afterResolving, retrieval of shared instances, and how configuration differs on re-resolution.
Laravel Octane
Review caveats about holding the Request or container and operating long-running processes.
Primary sources consulted
- Laravel documentation: Binding scoped singletons
- Laravel documentation: Dependency injection and Octane
- Container: scoped(), singleton(), forgetScopedInstances(), and resolve()
- ContainerTest: scoped sharing and recreation
- QueueServiceProvider: resetScope passed to the Worker
- Worker: scope reset in daemon()
- Octane: FlushTemporaryContainerInstances
- Octane: default listeners for OperationTerminated