Service Container Deep Dive: Contextual Binding, Tagging, and Method Injection
#laravel #service-container #architecture #dependency-injection

Service Container Deep Dive: Contextual Binding, Tagging, and Method Injection

4 min read Mohamed Said Mohamed Said

Beyond app()->make(): The Container Features You're Probably Underusing

Most Laravel developers know how to bind an interface to a concrete class. Fewer know how to tell the container to resolve different implementations depending on who is asking, group related services under a tag, or inject dependencies directly into arbitrary methods without a constructor. These three features — contextual binding, tagging, and method injection — unlock a cleaner architecture that scales without a service locator smell.


Contextual Binding

Contextual binding answers the question: "Give class A one implementation, but give class B a different one."

// AppServiceProvider::register()
$this->app
    ->when(OrderExporter::class)
    ->needs(FilesystemInterface::class)
    ->give(fn () => Storage::disk('s3'));

$this->app
    ->when(ReportGenerator::class)
    ->needs(FilesystemInterface::class)
    ->give(fn () => Storage::disk('local'));

Both classes type-hint FilesystemInterface, but the container resolves the correct disk for each. No factory, no service locator, no if chain inside the class.

Contextual Primitives

You can also inject scalar values contextually:

$this->app
    ->when(StripeGateway::class)
    ->needs('$apiKey')
    ->giveConfig('services.stripe.secret');

This keeps environment-specific values out of constructors and config facades inside domain classes.


Tagging

Tagging lets you group related bindings and resolve them all at once — perfect for plugin-style architectures, notification channels, or report drivers.

// 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'
);
// Resolve all tagged services
class BroadcastService
{
    public function __construct(
        private iterable $notifiers
    ) {}

    public function notify(string $message): void
    {
        foreach ($this->notifiers as $notifier) {
            $notifier->send($message);
        }
    }
}

// Bind the iterable via tagged
$this->app->bind(BroadcastService::class, function ($app) {
    return new BroadcastService($app->tagged('notifiers'));
});

$app->tagged('notifiers') returns a lazy TaggedIterator — services are only resolved when iterated, keeping boot time low.

Package Integration Pattern

Packages can push their own implementations into a host application's tag without modifying the host's service provider:

// In the package's service provider
$this->app->tag(PushNotifier::class, 'notifiers');

The host's BroadcastService picks it up automatically.


Method Injection

The container can resolve dependencies for any callable, not just constructors. This is how route closures, controller methods, and console commands work internally.

$result = app()->call(
    [ReportService::class, 'generate'],
    ['format' => 'pdf']
);

The container resolves ReportService and all type-hinted parameters of generate(), then merges in the explicitly passed $format.

Practical Use: Pipeline Steps Without Constructor Injection

class EnrichOrderData
{
    public function handle(
        Order $order,
        TaxCalculator $tax, // resolved by container
        CurrencyConverter $fx // resolved by container
    ): Order {
        $order->tax = $tax->calculate($order);
        $order->total = $fx->convert($order->subtotal, $order->currency);
        return $order;
    }
}

// Invoke it
$enriched = app()->call([new EnrichOrderData, 'handle'], ['order' => $order]);

This is cleaner than injecting every dependency into the constructor when a step only needs them once.


Testing Implications

All three features make testing straightforward:

// Swap a contextual binding in a test
app()->when(OrderExporter::class)
    ->needs(FilesystemInterface::class)
    ->give(fn () => new FakeFilesystem);

// Or use the standard swap
app()->instance(FilesystemInterface::class, new FakeFilesystem);

Tagged services can be replaced by re-tagging a fake before the test runs. Method injection means you can call pipeline steps directly with mocked dependencies without rebuilding the whole object graph.


Takeaways

  • Contextual binding eliminates factory classes and if chains when the same interface needs different implementations in different contexts.
  • Scalar contextual binding (needs('$apiKey')) keeps config out of domain classes cleanly.
  • Tagging enables open/closed plugin architectures — new implementations register themselves without touching existing code.
  • $app->tagged() is lazy; services are instantiated only when iterated.
  • Method injection via app()->call() is the container's most underused feature for pipeline steps, console commands, and ad-hoc service invocations.
  • All three patterns improve testability by keeping the container as the single source of truth for dependency resolution.

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 decision about which implementation to use is purely based on which class is consuming it, and that decision is stable at boot time. Factories are better when the choice depends on runtime data (e.g., a user's plan or a request parameter).
Q02 Does `$app->tagged()` resolve all services immediately?
No. `$app->tagged()` returns a lazy TaggedIterator. Each service is resolved from the container only when the iterator reaches it, so unused services in the group are never instantiated.
Q03 Can I use method injection outside of controllers and console commands?
Yes. `app()->call($callable, $parameters)` works on any callable — closures, static methods, instance methods, or invokable classes. The container resolves all type-hinted parameters automatically and merges any explicitly passed values.

Continue reading

More Articles

View all