The container is the backbone of Laravel: understanding it makes code testable and the framework predictable.
The service container builds objects and their dependencies for you. When Laravel calls a controller, job, listener or command, it reads the type-hints of the constructor and method and resolves each one, recursively:
public function __construct(private PaymentGateway $payments) {}Concrete classes with resolvable constructor parameters need no configuration at all (auto-wiring). You only register bindings when the container can't figure it out:
$this->app->bind(PaymentGateway::class, StripeGateway::class)maps an interface to an implementation, creating a new instance each time.singleton(...)creates one instance per application (per request in PHP-FPM), andscoped(...)one per request or job, which is safer with Octane and queue workers.- A closure configures construction:
singleton(Client::class, fn ($app) => new Client(config('services.x.key'))). - Contextual binding gives different implementations to different consumers:
when(ReportController::class)->needs(Filesystem::class)->give(...).
Bindings belong in service providers: register() only binds things into the container, and boot() runs after all providers are registered, so it can use other services (define gates, observers, macros, preventLazyLoading). Providers are listed in bootstrap/providers.php; AppServiceProvider is where most apps start.
Resolve manually with app(Foo::class) or app()->make(Foo::class), but prefer constructor injection. In tests, $this->app->instance(PaymentGateway::class, $fake) swaps an implementation for the whole app.
Example
<?php
interface PaymentGateway { public function charge(int $cents, string $token): string; }
class StripeGateway implements PaymentGateway
{
public function __construct(private string $secret) {}
public function charge(int $cents, string $token): string { /* call Stripe */ return 'ch_123'; }
}
// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PaymentGateway::class,
fn ($app) => new StripeGateway(config('services.stripe.secret')));
}
}
// Auto-wired: no binding needed for CheckoutController itself
class CheckoutController
{
public function __construct(private PaymentGateway $payments) {}
public function __invoke(Request $r) { return ['id' => $this->payments->charge(999, $r->input('token'))]; }
}
// In a test: $this->app->instance(PaymentGateway::class, new FakeGateway());Remember
- The container auto-wires constructor dependencies by type-hint
- Bind interfaces with bind, singleton or scoped in a provider's register()
- boot() runs after all providers register and may use other services
Watch out for
- Resolving services inside register(), before other providers have registered their bindings.