0

How does the service container resolve dependencies?

Advanced5 min read·eng-10-002
interview

Concept

The Service Container is Laravel's IoC (Inversion of Control) container — a class that knows how to build objects and their dependencies automatically.

How resolution works:

  1. Simple binding: $app->bind(OrderService::class, fn($app) => new OrderService($app->make(OrderRepository::class))). When resolved: runs the closure.

  2. Auto-wiring: If no binding is registered, the container inspects the class's constructor via reflection. It reads each parameter's type hint and recursively resolves them. This is how most Laravel classes work — no explicit binding needed.

  3. Singletons: $app->singleton(LoggerInterface::class, FileLogger::class). Resolved once; same instance returned every time.

  4. Contextual binding: Different implementations for different consumers. $this->app->when(PaymentController::class)->needs(LoggerInterface::class)->give(AuditLogger::class).

  5. Interface binding: $app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class). When any class type-hints OrderRepositoryInterface, gets an EloquentOrderRepository.

Resolution stack:

  1. Check if a binding exists for the given abstract.
  2. If binding is a closure: call it.
  3. If binding is a class string: recursively resolve that class.
  4. If no binding: attempt auto-wiring via reflection.
  5. Pass all resolved dependencies to the constructor.

The make() method: app()->make(OrderService::class) is how you manually resolve. Rarely needed — prefer type-hinting in constructors and letting the container inject.

Tags: Group related bindings. $app->tag([SmtpMailer::class, SesMailer::class], 'mailers'). Resolve all: $app->tagged('mailers').

Code Example

php
<?php
// AppServiceProvider::register() — where you bind
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Interface → implementation
        $this->app->bind(
            OrderRepositoryInterface::class,
            EloquentOrderRepository::class,
        );

        // Singleton — same instance every time
        $this->app->singleton(ReportCache::class, fn($app) => new ReportCache(
            $app->make(CacheInterface::class),
            ttl: 3600,
        ));

        // Contextual binding — different implementations for different consumers
        $this->app->when(CheckoutController::class)
            ->needs(LoggerInterface::class)
            ->give(AuditLogger::class);

        $this->app->when(StatsController::class)
            ->needs(LoggerInterface::class)
            ->give(NullLogger::class);
    }
}

// Auto-wiring — no binding needed if type hints are concrete
class OrderService
{
    public function __construct(
        private readonly OrderRepository $orders,      // auto-resolved
        private readonly PaymentService  $payments,   // auto-resolved
        private readonly LoggerInterface $logger,      // resolved via binding above
    ) {}
}

// Explicit resolution
$orderService = app(OrderService::class);
// Container: reads __construct, finds OrderRepository, resolves it
//            finds PaymentService, resolves it
//            finds LoggerInterface, checks binding → gives AuditLogger

// Manual make()
$service = app()->make(OrderService::class, [
    'logger' => new NullLogger(), // override specific argument
]);

// Closure binding — run arbitrary code at resolution time
app()->bind(ExchangeRateProvider::class, function ($app) {
    return new ExchangeRateProvider(
        apiKey: config('services.exchangerate.key'),
        cache:  $app->make(CacheInterface::class),
    );
});

// How the container resolves via reflection (simplified):
// 1. $reflector = new ReflectionClass(OrderService::class);
// 2. $constructor = $reflector->getConstructor();
// 3. foreach $constructor->getParameters() as $param:
//        $type = $param->getType()->getName();
//        $dependencies[] = $this->make($type); // recursive
// 4. return $reflector->newInstanceArgs($dependencies);