Binding Interfaces

Binding Interfaces to Implementations

An interface cannot be built, so it needs a binding. The trace followed a provider's $singletons entry (Writing Service Providers), shorthand for $this->app->singleton(PaymentGateway::class, FakeGateway::class). The method sets the lifetime, which FakeGateway, counting its charges, makes visible:

bind, singleton and scoped with the same gatewaySQL
use App\Contracts\PaymentGateway;
use App\Services\FakeGateway;
foreach (['bind', 'singleton', 'scoped'] as $method) {
    app()->$method(PaymentGateway::class, FakeGateway::class);
    $charge = fn () => app(PaymentGateway::class)->charge(10, 'test');
    $refs = [$charge(), $charge()];
    app()->forgetScopedInstances();                       // what Octane and queue workers do
    $refs[] = $charge();
    printf("%-9s %s\n", $method, implode(' ', $refs));
}
Output
bind      fake_0001 fake_0001 fake_0001
singleton fake_0001 fake_0002 fake_0003
scoped    fake_0001 fake_0002 fake_0001

bind() builds anew on every make(), singleton() keeps one object per process, and scoped() one per request or job, since Octane 4,043 and queue workers flush scoped instances between units of work. Prefer scoped() for per-request state. instance() registers an object you built.

Since Laravel 12 2,157 the interface can carry its binding (Controller Injection). With #[Bind(StripeGateway::class)], #[Bind(FakeGateway::class, environments: ['local', 'testing'])] and #[Singleton] on PaymentGateway and no provider, local resolved a shared FakeGateway and production a StripeGateway. #[Scoped] exists too, and #[BindWhen] takes a closure, which needs PHP 8.5. Explicit bindings override attributes.