Laravel's scoped(): The Singleton That Knows When to Die
Laravel's service container has a familiar method: $this->app->singleton(MyService::class); It means Laravel creates the service once and reuses that instance. But there is an interesting question: What happens when your PHP application doesn't restart between requests? That's where scoped() comes in. The problem with long-running workers With the traditional PHP request lifecycle, things look roughly like this: Request A -> Laravel boots -> Request ends -> PHP ends Request B -> Laravel boots -> Request ends -> PHP ends A singleton therefore appears to live only for one request. But long-running environments such as Laravel Octane and queue workers work differently: Long-running worker | |-- Request A |-- Request B |-- Request C |-- Request D The Laravel application stays alive, that means a singleton can also stay alive. Imagine a service that keeps some request-specific state: class RequestContext { public ?int $userId = null; } If registered as a singleton: $this->app->singleton(RequestContext::class); Request A might do: $context->userId = 10; Then Request B could receive the same instance: $context->userId; // 10 That state has crossed a request boundary. Enter scoped() Laravel provides: $this->app->scoped(RequestContext::class); A scoped binding behaves like a singleton inside the current lifecycle. So during one request: $a = app(RequestContext::class); $b = app(RequestContext::class); $a === $b; // true But when the lifecycle changes, Laravel clears the scoped instance and creates a new one. Conceptually: Request A | |-- RequestContext #1 Request B | |-- RequestContext #2 Request C | |-- RequestContext #3 Laravel documents scoped bindings specifically for this kind of lifecycle: they are resolved once per request or job lifecycle and flushed when a new lifecycle begins. Singleton vs scoped The easiest way to remember the difference is the lifetime: | Binding | Lifetime | |---|---| bind() | New instance when resolved | singleton() | Same instance for the application/container | scoped() | Same instance for the current request/job lifecycle | The important distinction is between the last two. singleton() answers: “Should this application use the same instance?” scoped() answers: “Should this execution use the same instance?” Laravel actually resets scoped instances This isn't just documentation terminology, Laravel's queue worker explicitly calls forgetScopedInstances: $app->forgetScopedInstances(); when resetting the worker between jobs. The container also exposes forgetScopedInstances() as a dedicated operation, showing that scoped instances have their own lifecycle management. So the next job doesn't inherit the scoped objects created by the previous one. When should you use scoped() ? Use it when an object should be shared during one execution, but must not leak into the next one. Good examples can include: - request-specific state - tenant-specific state - per-job state - objects that accumulate information while processing one operation A useful rule is: If the object contains state that belongs to one request or job, scoped() is often a better fit thansingleton() . And this becomes particularly important when your application runs in a long-lived process. At the end scoped() isn't really about creating another kind of singleton, it's about lifetime management. Laravel is telling the container: Reuse this object... | v but only while this execution is alive. That small difference becomes extremely important once PHP stops dying after every request. Top comments (0)
Comments
No comments yet. Start the discussion.