Service container and providers

Laravel · Practical · 15 min

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), and scoped(...) 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.

Check yourself

  1. What is the difference between bind and singleton?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: laravel.com/docs/12.x/container