Contextual binding — when UserRepository needs different implementations
Concept
Contextual binding solves a specific problem: the same interface needs different implementations depending on who is asking for it. The classic example is two different controllers that both type-hint LoggerInterface, but one should log to a file and the other to a database. Contextual binding lets you express exactly this: "when building AdminController, give it the DatabaseLogger; when building UserController, give it the FileLogger."
The API uses a fluent builder: $this->app->when(ConcreteClass::class)->needs(AbstractInterface::class)->give(ConcreteImplementation::class). The when() method takes the requesting class. needs() takes the abstract that is requested. give() takes the concrete to provide, either as a class name string, a Closure, or a scalar value.
Contextual bindings for primitive values (the $variableName form) solve the autowiring limitation for strings, ints, and other primitives. ->needs('$apiKey')->give(fn() => config('services.api.key')) tells the container that when building the requesting class and it encounters a constructor parameter named $apiKey, inject this value.
Internally, contextual bindings are stored in $this->contextual on the container — a nested array indexed by [builderClass][abstract]. When build() is resolving a constructor parameter and the container knows the current class being built (tracked in $buildStack), it checks contextual[$buildingClass][$abstract] before falling back to regular bindings. This means contextual bindings override global bindings selectively.
Multiple classes can share a contextual binding by passing an array to when(): $this->app->when([AdminController::class, SuperAdminController::class])->needs(LoggerInterface::class)->give(DatabaseLogger::class).
Contextual binding for interfaces is commonly used for repository patterns (different implementations per context), logging channels (admin actions get audit logs), and feature flags (different strategy implementations per user role context at bootstrap time).
Code Example
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\LoggerInterface;
use App\Services\Loggers\DatabaseLogger;
use App\Services\Loggers\FileLogger;
use App\Services\Loggers\NullLogger;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Default binding for everyone who doesn't have a contextual override
$this->app->bind(LoggerInterface::class, FileLogger::class);
// AdminController gets DatabaseLogger
$this->app->when(\App\Http\Controllers\AdminController::class)
->needs(LoggerInterface::class)
->give(DatabaseLogger::class);
// PaymentService needs a special logger with audit trail
$this->app->when(\App\Services\PaymentService::class)
->needs(LoggerInterface::class)
->give(function ($app) {
return new DatabaseLogger(
connection: 'audit',
level: 'critical',
);
});
// Multiple classes can share the same contextual binding
$this->app->when([
\App\Console\Commands\SyncInventoryCommand::class,
\App\Jobs\SyncInventoryJob::class,
])
->needs(LoggerInterface::class)
->give(NullLogger::class); // Suppress logging for batch jobs
// Primitive contextual binding — inject a specific string/int value
$this->app->when(\App\Services\StripeGateway::class)
->needs('$apiKey')
->give(fn() => config('services.stripe.secret'));
$this->app->when(\App\Services\StripeGateway::class)
->needs('$webhookSecret')
->give(fn() => config('services.stripe.webhook_secret'));
// Inject a tagged group (see lv-02-005 for tagging)
$this->app->when(\App\Services\ReportGenerator::class)
->needs(\App\Contracts\FormatterInterface::class)
->giveTagged('report.formatters');
}
}<?php
// The service that benefits from contextual binding
namespace App\Services;
class StripeGateway
{
public function __construct(
private readonly string $apiKey, // Injected via contextual binding
private readonly string $webhookSecret, // Injected via contextual binding
private readonly \GuzzleHttp\Client $http, // Auto-resolved
) {}
public function charge(int $amountCents, string $currency): array
{
return $this->http->post('https://api.stripe.com/v1/charges', [
'headers' => ['Authorization' => "Bearer {$this->apiKey}"],
'form_params' => [
'amount' => $amountCents,
'currency' => $currency,
],
])->toArray();
}
}
// AdminController gets DatabaseLogger automatically
namespace App\Http\Controllers;
use App\Contracts\LoggerInterface;
class AdminController extends Controller
{
public function __construct(
private readonly LoggerInterface $logger, // Gets DatabaseLogger, not FileLogger
) {}
}Interview Q&A
Q: What problem does contextual binding solve that regular binding cannot?
Regular bind() creates a one-to-one mapping: every class that needs LoggerInterface gets the same implementation. Contextual binding creates a one-to-many conditional mapping: when(AdminController::class)->needs(LoggerInterface::class)->give(DatabaseLogger::class) says "only when building AdminController, inject DatabaseLogger." For all other classes requesting LoggerInterface, the regular binding still applies. This is essential when different parts of an application legitimately need different implementations of the same contract — without contextual binding, you'd have to use factory methods, string tags, or add conditional logic inside the service itself, all of which break the Dependency Inversion Principle.
Q: How does the container's $buildStack enable contextual binding resolution?
The container tracks which class is currently being built in a $buildStack array (a stack because resolution is recursive). When resolving AdminController's constructor, 'AdminController' is pushed onto the buildStack. When a constructor parameter needs LoggerInterface, the container's getContextualConcrete() method checks $this->contextual['AdminController']['LoggerInterface'] before falling back to regular bindings. This check is only possible because the build stack tells the container "I am currently building AdminController." Without this stack, contextual bindings would be impossible — there would be no way to know which class triggered the resolution chain.
Q: Can you use contextual bindings for scalar values like strings and integers? Give a real-world example.
Yes. The ->needs('$variableName') form uses the constructor parameter name (not type) to match. The container uses ReflectionParameter::getName() and looks for a contextual binding under that parameter name. A real example: multiple payment gateways all need an $apiKey string, but each needs a different key. $this->app->when(StripeGateway::class)->needs('$apiKey')->give(fn() => config('services.stripe.key')) and $this->app->when(PaypalGateway::class)->needs('$apiKey')->give(fn() => config('services.paypal.key')). This approach is better than creating a StripeConfig value object when the gateway is a third-party class you cannot modify to add a constructor-injected config object.