Skip to content

Contextual Binding and Method Injection in Laravel's Service Container

Go beyond basic singleton registration. Learn how contextual binding, tagged services, and method injection let you wire complex dependency graphs cleanly without a single service locator call.

Published · Updated · 3 min read

Contextual Binding and Method Injection in Laravel's Service Container

Beyond app()->make(): The Container You're Not Using Fully

Most Laravel developers know bind, singleton, and make. That covers 80% of day-to-day container use. The remaining 20% — contextual binding, tagged services, and method injection — is where the container earns its keep in complex domain code. This article walks through each with production-grade examples.


Contextual Binding

Contextual binding answers the question: what should the container inject when two different classes both depend on the same interface, but need different implementations?

// AppServiceProvider::register()
$this->app
    ->when(OrderProcessor::class)
    ->needs(PaymentGatewayInterface::class)
    ->give(StripeGateway::class);

$this->app
    ->when(SubscriptionRenewalJob::class)
    ->needs(PaymentGatewayInterface::class)
    ->give(BraintreeGateway::class);

Both OrderProcessor and SubscriptionRenewalJob type-hint PaymentGatewayInterface. The container resolves each to a different concrete without any conditional logic inside the classes themselves.

Giving a Primitive Contextually

Contextual binding isn't limited to interfaces. You can inject config values or primitive scalars:

$this->app
    ->when(S3Uploader::class)
    ->needs('$bucketName')
    ->give(fn () => config('filesystems.disks.s3.bucket'));

The $bucketName constructor parameter is resolved from config at build time — no config() call inside the class, no hidden coupling.


Tagged Services

Tagging lets you resolve a collection of implementations registered under a shared label. This is the clean alternative to a hand-rolled registry array.

// Register
$this->app->bind(CsvExporter::class);
$this->app->bind(JsonExporter::class);
$this->app->bind(XlsxExporter::class);

$this->app->tag(
    [CsvExporter::class, JsonExporter::class, XlsxExporter::class],
    'exporters'
);
// Resolve all tagged exporters
class ExportManager
{
    /** @param iterable<ExporterInterface> $exporters */
    public function __construct(
        private readonly iterable $exporters
    ) {}
}

// In the service provider
$this->app->bind(ExportManager::class, fn ($app) =>
    new ExportManager($app->tagged('exporters'))
);

$app->tagged() returns a lazy generator — implementations are not instantiated until iterated. Adding a new exporter is a one-line tag registration; ExportManager never changes.


Method Injection

The container can inject dependencies into arbitrary methods, not just constructors. This is useful for controller actions, console commands, and domain handlers where you want to keep the constructor lean.

class ReportController extends Controller
{
    public function generate(
        Request $request,
        ReportBuilder $builder,   // injected by container
        CacheManager $cache       // injected by container
    ): JsonResponse {
        // ...
    }
}

Laravel's router already does this for controller methods. You can invoke the same mechanism manually:

$result = app()->call(
    [SomeService::class, 'handle'],
    ['extraParam' => $value]  // merge with container-resolved args
);

This is powerful inside pipeline stages, command handlers, or test helpers where you want full DI without making the class a singleton.

Method Injection in Artisan Commands

class SyncInventoryCommand extends Command
{
    public function handle(InventorySync $sync, LoggerInterface $log): int
    {
        $log->info('Starting sync');
        $sync->run();
        return self::SUCCESS;
    }
}

Artisan resolves handle() through the container automatically. No constructor injection needed for command-specific dependencies.


Testing Contextual Bindings

Swap a contextual binding in a test without touching the service provider:

it('uses the sandbox gateway in tests', function () {
    $this->app
        ->when(OrderProcessor::class)
        ->needs(PaymentGatewayInterface::class)
        ->give(SandboxGateway::class);

    $processor = app(OrderProcessor::class);

    expect($processor->gateway())->toBeInstanceOf(SandboxGateway::class);
});

The override is scoped to the test; no global state leaks.


Takeaways

  • Contextual binding eliminates conditional logic inside classes when multiple implementations of an interface are needed in different contexts.
  • Primitive injection (needs('$param')) keeps config and environment values out of class bodies.
  • Tagged services replace hand-rolled registries with a lazy, extensible collection resolved by the container.
  • Method injection via app()->call() gives you full DI on any callable — useful in pipelines, handlers, and tests.
  • All of these are testable in isolation: swap bindings per-test without touching service providers.

Frequently asked questions

When should I use contextual binding instead of a factory or strategy pattern?

Use contextual binding when the decision of which implementation to use is purely about *which class is being constructed*, not runtime data. If the choice depends on user input or a database value, a factory or strategy resolved at runtime is more appropriate.

Does `app()->tagged()` instantiate all services immediately?

No. `app()->tagged()` returns a generator. Each implementation is resolved lazily as you iterate, so unused implementations in the collection are never instantiated.

Can I use method injection outside of controllers and Artisan commands?

Yes. `app()->call($callable, $parameters)` works on any callable — closures, static methods, or instance methods. The container merges your explicit parameters with anything it can resolve by type-hint.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project