Laravel container bindings in web requests and queue workers
This post contains just enough about container bindings to prevent shooting yourself in the foot.
TLDR
-
Using
scoped()inside a service provider'sregisteris probably what you want. - PHP-FPM web requests always start with a clean slate.
- Queue workers remember singletons and static variables between jobs.
-
Queue workers only call service providers'
registerandbootonce on startup. - Binding anything outside a service provider will leak into all future queued jobs.
Container binding flavors
Laravel has four different ways to bind something in the container:
-
->bind(): Takes a closure and calls it every time the binding is resolved. -
->scoped(): Takes a closure, calls it on the first resolve, and reuses the result only for the current job or web request. -
->singleton(): Takes a closure, calls it on the first resolve, and reuses the result until the worker process dies (e.g. viaqueue:restart). -
->instance(): Same as a singleton, but takes a pre-built object instead of calling a closure.
For web requests, ->scoped() and ->singleton() are identical.
Every PHP-FPM web request starts with a clean slate.
A queue worker running via queue:work only calls service providers' register and boot once.
The same singleton object is reused until the worker process dies (e.g. via queue:restart).
The same goes for static variables: PHP-FPM flushes them on every request.
A queue worker remembers them until the process dies.
Binding in service providers
Rule of thumb: in a service provider, use the register method only for registering bindings.
Don't resolve anything, don't call facades, don't use the config.
Anything else you'd like to do goes in the boot method.
Registering simple bindings in boot is also fine.
Complex bindings that depend on other bindings should go in register.
Second rule of thumb: don't bind anything outside a service provider.
Queue workers only call register and boot once per process.
Any bindings done outside service providers will leak into all future jobs.
Usage examples
// These three use the result of a closure
$this->app->bind('some-key', fn () => new SomeClass);
$this->app->scoped('some-key', fn () => new SomeClass);
$this->app->singleton('some-key', fn () => new SomeClass);
// This takes anything and returns it as-is when resolved
$this->app->instance('some-key', new SomeClass); // resolving gives the object
$this->app->instance('some-key', fn () => new SomeClass); // resolving gives the closure
$this->app->instance('some-key', 123); // resolving gives the int
You can also bind a class name string. Under the hood it is wrapped in a closure:
// These are identical (same goes for "bind" and "singleton")
$this->app->scoped(SomeClass::class);
$this->app->scoped(SomeClass::class, fn () => new SomeClass);
Bindings in unit tests
Laravel flushes the container before each test, meaning that tests behave roughly the same as a PHP-FPM web request (except that static variables aren't flushed).
Using app()->instance() in tests works well:
#[Test]
#[TestWith([1])]
#[TestWith([2])]
#[TestWith([1000])]
public function some_test_method(int $chunkSize)
{
app()->instance('override-chunk-size', $chunkSize);
app()->instance(Slack::class, $fakeSlack = new class extends Slack {
// override methods here...
});
// ...
}
class SomeJob
{
private int $chunkSize = 1000;
public function __construct()
{
if (app()->has('override-chunk-size')) {
$this->chunkSize = app()->get('override-chunk-size');
}
}
public function handle(Slack $slack)
{
// ...
}
}