How does the service container resolve dependencies?
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:
-
Simple binding:
$app->bind(OrderService::class, fn($app) => new OrderService($app->make(OrderRepository::class))). When resolved: runs the closure. -
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.
-
Singletons:
$app->singleton(LoggerInterface::class, FileLogger::class). Resolved once; same instance returned every time. -
Contextual binding: Different implementations for different consumers.
$this->app->when(PaymentController::class)->needs(LoggerInterface::class)->give(AuditLogger::class). -
Interface binding:
$app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class). When any class type-hintsOrderRepositoryInterface, gets anEloquentOrderRepository.
Resolution stack:
- Check if a binding exists for the given abstract.
- If binding is a closure: call it.
- If binding is a class string: recursively resolve that class.
- If no binding: attempt auto-wiring via reflection.
- 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
// 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);