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

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() and give(Closure) helpers cover nearly every real-world wiring scenario without a service locator.

Found this useful?

Frequently Asked Questions

3 questions
Q01 When should I use contextual binding instead of a factory class?
Use contextual binding when the correct implementation is determined entirely by which class is being constructed — it keeps wiring declarative and in the service provider. Use a factory when the choice depends on runtime data (user input, a database value) that isn't available at container build time.
Q02 Does tagging services affect performance?
Tags are resolved lazily; the container only instantiates tagged services when you iterate over them. For most applications the overhead is negligible. If you have dozens of heavy services under one tag, consider wrapping them in a lazy proxy or resolving only the one you need.
Q03 Can method injection be used in Artisan commands?
Yes. Define your dependencies as parameters on the `handle()` method of your command class. Laravel resolves them from the container automatically, the same way it does for controller actions.

Continue reading

More Articles

View all