Contextual Binding and Method Injection in Laravel's Service Container
#laravel #service-container #dependency-injection #architecture

Contextual Binding and Method Injection in Laravel's Service Container

3 min read Mohamed Said Mohamed Said

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 ContextualBindingServiceProvider to keep AppServiceProvider readable.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does contextual binding work with interfaces resolved via constructor injection automatically?
Yes. When the container autowires a constructor it checks contextual bindings first. If the consuming class has a contextual rule for the requested type, that rule wins over any global binding.
Q02 Is `app()->tagged()` eager or lazy?
It returns a generator, so tagged implementations are instantiated only when you iterate over them. This means unused notifiers in a broadcast scenario cost nothing.
Q03 Can I use method injection in Artisan commands?
Yes. Override `handle()` with any type-hinted dependencies beyond the default Command signatures and Laravel will resolve them via the container automatically, the same way it does for controller methods.

Continue reading

More Articles

View all