Dependency injection and the Laravel container — automatic resolution and binding interfaces
In chapter 10 we handed OrderPlacementService to the constructor of OrderController.
Nowhere did we write code like new OrderPlacementService(...).
That is because Laravel looks at the constructor's type declarations and creates the instances it needs automatically.
This chapter looks at how that automatic resolution works, and at how the dependency on an interface from chapter 6 is realized in Laravel.
Automatic resolution in the constructor
The OrderPlacementService from chapter 10 depended on the concrete class StockChecker.
class OrderPlacementService
{
public function __construct(
private readonly StockChecker $stockChecker,
) {
}
}
When you type-declare OrderPlacementService in the constructor of OrderController, Laravel's service container works like this1.
- It notices that building
OrderControllerrequires anOrderPlacementService - It notices that building
OrderPlacementServicerequires aStockChecker StockCheckerhas no constructor arguments, so it simply doesnew StockChecker()- It passes the
StockCheckerin and doesnew OrderPlacementService($stockChecker) - It passes the
OrderPlacementServicein and doesnew OrderController($orderPlacementService)
As long as the dependencies are between concrete classes, this resolution happens automatically with no configuration at all.
Passing an interface requires a binding
Chapter 6 showed an example where OrderService depends on an interface called Logger.
class OrderService
{
public function __construct(
private readonly Logger $logger,
) {
}
}
Logger is an interface and has no substance of its own.
When the container tries to build OrderService and reaches Logger, it has no way to decide whether to new a FileLogger or a CloudWatchLogger.
So you tell the container in advance which implementation class corresponds to which interface.
// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(Logger::class, FileLogger::class);
}
}
Calling bind() inside the register() method registers the correspondence "when a Logger is asked for, hand over a FileLogger"2.
From then on, simply type-declaring Logger in the OrderService constructor is enough for the container to pass in a FileLogger instance.
When you want to send logs to CloudWatch instead, you change nothing in OrderService; you just swap the second argument of bind() for CloudWatchLogger::class.
The benefit from chapter 6 — being able to swap the implementation without touching OrderService — is realized by this bind() registration.
Sharing one instance with singleton
bind() creates a new instance every time it resolves.
When you want to reuse the same instance across the whole application, use singleton()3.
$this->app->singleton(Logger::class, FileLogger::class);
bind() and singleton() are the same in that both register which implementation class to hand over when an interface is asked for.
The difference is whether a new instance is created on every call, or created once and reused.
Handing a different implementation to each injection site
A binding registered with bind() returns the same implementation no matter which class asks for it.
When you want the implementation to differ by injection site — ordinary logs going to a file while the order flow goes to CloudWatch — use contextual binding4.
// The default goes to a file
$this->app->bind(Logger::class, FileLogger::class);
// Only the Logger that OrderService asks for goes to CloudWatch
$this->app->when(OrderService::class)
->needs(Logger::class)
->give(fn () => new CloudWatchLogger());
when() takes the injection site, needs() the requested type, and give() the implementation to hand over.
when() accepts an array, so classes that share an implementation can be listed together.
Whatever the closure passed to give() returns is what that injection site receives.
The bind() registration stays in place and is overridden only at the sites you named.
Its reach is narrow.
It only helps when the choice of implementation is decided by the class being injected into.
If the injection site stays the same and you want the implementation to change with a config value or the contents of a request, contextual binding cannot express that. You can write that branch inside the closure passed to give(), but it then hides in the container registration and gets hard to follow.
Write a factory that picks the implementation, and register that factory with bind().
What this chapter covers
bind(), singleton(), and contextual binding are the central uses of the service container.
Beyond them are tag() and tagged(), which resolve several implementations of the same role together, and extend(), which modifies an instance after it is resolved4.
Both only change how the registration is written; the mechanism of asking for an interface and receiving an implementation stays the same.
Summary
- When a constructor's type declaration is a concrete class, Laravel's service container resolves it automatically with no configuration
- When the type declaration is an interface, you have to register the corresponding implementation class in advance with
bind() bind()creates a new instance on every resolution;singleton()creates one the first time and reuses it- When the implementation should differ by injection site, register it with
when()->needs()->give(); when it should switch on a runtime value, write a factory - The benefit from chapter 6 — swapping the implementation without changing code — is realized by this binding registration
What to read next
The next chapter covers exception design and validation. We look at custom exception classes and at checking the shape of a request with a Form Request.
Exercises
For each situation below, say whether a container registration is needed, and why
- The constructor of
Notificationtype-declares the concrete classEmailChannel - The constructor of
Notificationtype-declares the interfaceNotificationChannel
Sample answer
- Not needed:
EmailChannelis a concrete class, so the container can look at the constructor andnewit directly - Needed:
NotificationChannelis an interface, so unless you register which implementation class to hand over withbind(), the container cannot resolve it