Why the Basics Are Not Enough
Most Laravel developers know app()->bind() and constructor injection. That covers 80% of cases. The remaining 20% — multiple implementations of the same interface, controller method injection, and runtime-selected strategies — is where the container's real power lives.
Contextual Binding
Contextual binding answers: "Give class A one implementation, but give class B a different one."
// AppServiceProvider::register()
$this->app
->when(OrderExporter::class)
->needs(StorageContract::class)
->give(S3Storage::class);
$this->app
->when(ReportArchiver::class)
->needs(StorageContract::class)
->give(LocalStorage::class);
Both classes declare StorageContract in their constructors. The container resolves the correct driver per consumer — zero if statements, zero service locator calls.
Giving a Closure Instead of a Class
When the implementation needs runtime data, pass a closure:
$this->app
->when(InvoicePdfRenderer::class)
->needs(StorageContract::class)
->give(function (Application $app) {
return $app->make(S3Storage::class, [
'bucket' => config('invoices.bucket'),
]);
});
Tagging Services
Tagging lets you resolve all implementations of a concept at once — perfect for pipelines, reporters, or notification channels.
// Register
$this->app->bind(SlackNotifier::class);
$this->app->bind(EmailNotifier::class);
$this->app->bind(SmsNotifier::class);
$this->app->tag(
[SlackNotifier::class, EmailNotifier::class, SmsNotifier::class],
'notifiers'
);
// Consume
class AlertDispatcher
{
/** @param iterable<NotifierContract> $notifiers */
public function __construct(
private readonly iterable $notifiers,
) {}
public function send(Alert $alert): void
{
foreach ($this->notifiers as $notifier) {
$notifier->notify($alert);
}
}
}
// Wire the tagged group
$this->app
->when(AlertDispatcher::class)
->needs('$notifiers')
->giveTagged('notifiers');
Adding a new channel later means registering one class and appending it to the tag — the dispatcher never changes.
Method Injection
Constructor injection is resolved once at build time. Method injection is resolved per-call, which suits controllers, console commands, and one-off invokables.
// Any public method resolved via app()->call()
class GenerateReport
{
public function handle(
Request $request,
ReportBuilder $builder,
CacheContract $cache,
): JsonResponse {
$report = $cache->remember(
'report.' . $request->query('type'),
3600,
fn () => $builder->build($request->query('type')),
);
return response()->json($report);
}
}
// Dispatch from a controller or route:
return app()->call([app(GenerateReport::class), 'handle']);
Laravel's router already does this for controller actions, but app()->call() works on any callable — closures, [object, method] pairs, or 'ClassName@method' strings.
Passing Extra Primitives
app()->call([GenerateReport::class, 'handle'], [
'extraParam' => 'value', // merged with container-resolved args
]);
The container resolves typed parameters from the IoC graph and fills named primitives from the array.
Practical Pattern: Strategy Selector
Combine contextual binding with a factory to select strategies at runtime:
class PaymentGatewayFactory
{
public function __construct(
private readonly Application $app,
) {}
public function for(string $provider): GatewayContract
{
return match ($provider) {
'stripe' => $this->app->make(StripeGateway::class),
'paddle' => $this->app->make(PaddleGateway::class),
default => throw new InvalidArgumentException("Unknown provider: {$provider}"),
};
}
}
Each gateway can still receive its own contextual dependencies — the factory just delegates resolution to the container.
Takeaways
- Contextual binding eliminates conditional wiring for consumers of the same interface.
- Tagged services enable open/closed extensibility — add implementations without touching consumers.
- Method injection via
app()->call()is useful for invokable actions and command handlers that need per-request dependencies. - Avoid
app()->make()inside domain classes; push resolution to service providers and factories. - The container's
giveTagged()andgive(Closure)helpers cover nearly every real-world wiring scenario without a service locator.