Beyond app()->make(): The Container Features Most Teams Ignore
Every Laravel developer knows bind and singleton. Fewer reach for contextual binding, tagging, or method injection — features that eliminate a surprising amount of conditional wiring code.
Contextual Binding
Contextual binding answers the question: "What should the container resolve when class A asks for interface X, versus when class B asks for the same interface?"
// AppServiceProvider::register()
$this->app
->when(ReportExporter::class)
->needs(FilesystemInterface::class)
->give(fn () => Storage::disk('s3'));
$this->app
->when(LocalPreviewGenerator::class)
->needs(FilesystemInterface::class)
->give(fn () => Storage::disk('local'));
Both classes declare __construct(FilesystemInterface $fs). The container resolves the correct disk without either class knowing about the other or about config flags. No if (app()->environment(...)) inside a constructor.
You can also bind a primitive this way:
$this->app
->when(StripeWebhookProcessor::class)
->needs('$secret')
->give(config('services.stripe.webhook_secret'));
This keeps secrets out of the class body while remaining fully testable — swap the binding in a test service provider and the constructor still receives a plain string.
Tagging Services
Tagging lets you resolve a group of implementations at once. This is the clean alternative to a hand-rolled registry array.
// Register
$this->app->bind(SlackNotifier::class);
$this->app->bind(EmailNotifier::class);
$this->app->bind(PushNotifier::class);
$this->app->tag(
[SlackNotifier::class, EmailNotifier::class, PushNotifier::class],
'notifiers'
);
// Consume
class BroadcastAlert
{
/** @param iterable<NotifierInterface> $notifiers */
public function __construct(
private readonly iterable $notifiers
) {}
public function send(Alert $alert): void
{
foreach ($this->notifiers as $notifier) {
$notifier->notify($alert);
}
}
}
// Wire it
$this->app->bind(BroadcastAlert::class, function ($app) {
return new BroadcastAlert($app->tagged('notifiers'));
});
$app->tagged('notifiers') returns a lazy generator — implementations are only instantiated when iterated. Adding a new notifier is a one-line tag registration; BroadcastAlert never changes.
Method Injection
The container can resolve dependencies for any callable, not just constructors. app()->call() accepts a class-method pair, a closure, or an invokable:
class ReportController
{
public function generate(
Request $request,
ReportBuilder $builder, // resolved by container
PdfRenderer $renderer // resolved by container
): Response {
$report = $builder->build($request->validated());
return response($renderer->render($report));
}
}
// In a test or CLI command:
$response = app()->call(
[app(ReportController::class), 'generate'],
['request' => $fakeRequest]
);
This is how Laravel resolves route controller methods internally. You can use the same mechanism in console commands, event listeners, or pipeline stages without pulling the container into the class itself.
Practical Pattern: Pipeline Stage Injection
class EnrichOrderData
{
public function handle(
Order $order,
Closure $next,
TaxCalculator $tax, // injected by app()->call()
FraudScorer $fraud
): Order {
$order->tax_amount = $tax->calculate($order);
$order->fraud_score = $fraud->score($order);
return $next($order);
}
}
When you drive the pipeline with app()->call([$stage, 'handle'], $payload), each stage gets its own dependencies resolved fresh — no constructor pollution, no service locator.
Takeaways
- Contextual binding removes environment checks and config reads from constructors, keeping classes ignorant of their deployment context.
- Tagging replaces hand-rolled registries with a lazy, extensible collection resolved by the container.
- Method injection via
app()->call()is not just a framework internal — use it in pipelines, commands, and test helpers to keep classes lean. - All three features are fully compatible with constructor injection; they compose rather than replace it.
- Prefer registering contextual bindings in a dedicated
ContextualBindingServiceProviderto keepAppServiceProviderreadable.