Automatic resolution — how the container reads constructor types via Reflection
Concept
Laravel's container can resolve a class without any explicit binding — a feature called automatic resolution or autowiring. When you call app()->make(UserService::class) and UserService has never been bound, the container uses PHP's Reflection API to inspect the constructor, determine what types it needs, recursively resolve each dependency, and construct the object. This is how the vast majority of Laravel's dependency injection works with zero configuration.
The resolution algorithm lives in Illuminate\Container\Container::resolve() and Container::build(). When make($abstract) is called:
- Check the
$aliasesarray — if it's an alias, resolve the canonical name. - Check the
$instancesarray — if it's a singleton already resolved, return it. - Check the
$bindingsarray — if there's a factory closure, call it. - If none of the above, call
build($concrete)for automatic resolution.
build() creates a ReflectionClass for the concrete class and calls getConstructor(). If there's no constructor, new $concrete() is returned. If there is a constructor, getParameters() returns each ReflectionParameter. For each parameter, the container checks if there's a type-hint (via getType()). If the type-hint is a class or interface name, it recursively calls make() on that type. If the parameter has a default value and can't be resolved, the default is used.
The Reflection API calls have a real performance cost. To mitigate this, the container caches the resolved parameter list in $buildStack and uses $this->methodBindings for common paths. Production apps should use php artisan optimize to pre-generate the class manifest, reducing the number of first-resolution Reflection calls.
One crucial limitation: the container cannot automatically resolve primitive types (string, int, bool, etc.) because a type-hint like string $apiKey gives no information about which value to inject. You must handle these with a closure binding that provides the primitive values, or use contextual bindings with $this->app->when()->needs()->give().
Code Example
<?php
// Automatic resolution — zero configuration needed
namespace App\Services;
use App\Repositories\UserRepository;
use Illuminate\Mail\Mailer;
use Illuminate\Log\LogManager;
// The container can build this automatically because:
// 1. UserRepository — concrete class, no primitives in constructor
// 2. Mailer — bound as singleton by MailServiceProvider
// 3. LogManager — bound as singleton by LogServiceProvider
class UserService
{
public function __construct(
private readonly UserRepository $users, // Auto-resolved
private readonly Mailer $mailer, // Resolved from singleton binding
private readonly LogManager $log, // Resolved from singleton binding
) {}
}
// In a controller:
class UserController extends Controller
{
// Route model injection + container injection happen automatically
public function show(UserService $service, int $id): \Illuminate\Http\JsonResponse
{
// $service was resolved by the container — no manual new()
return response()->json($service->find($id));
}
}<?php
// What happens when automatic resolution FAILS
// This class has primitive parameters — Reflection cannot determine values
class ApiClient
{
public function __construct(
private readonly string $baseUrl, // Cannot auto-resolve
private readonly int $timeout, // Cannot auto-resolve
private readonly \GuzzleHttp\Client $http, // Can auto-resolve
) {}
}
// Solution 1: Explicit binding with closure
app()->bind(ApiClient::class, function ($app) {
return new ApiClient(
baseUrl: config('services.api.url'),
timeout: config('services.api.timeout', 30),
http: $app->make(\GuzzleHttp\Client::class),
);
});
// Solution 2: Contextual binding (when a specific class needs this)
app()->when(SomeService::class)
->needs('$baseUrl')
->give(fn() => config('services.api.url'));<?php
// How the container uses Reflection internally — simplified
private function build(string $concrete): mixed
{
$reflector = new \ReflectionClass($concrete);
if (!$reflector->isInstantiable()) {
throw new BindingResolutionException("Target [$concrete] is not instantiable.");
}
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
return new $concrete; // No dependencies
}
$dependencies = $this->resolveDependencies($constructor->getParameters());
return $reflector->newInstanceArgs($dependencies);
}
private function resolveDependencies(array $parameters): array
{
$results = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
// Type-hinted class or interface — recurse
$results[] = $this->make($type->getName());
} elseif ($parameter->isDefaultValueAvailable()) {
// Primitive with default — use the default
$results[] = $parameter->getDefaultValue();
} else {
// Primitive without default — cannot resolve
throw new BindingResolutionException("Unresolvable dependency: \${$parameter->getName()}");
}
}
return $results;
}Interview Q&A
Q: How does Laravel's container resolve a class that has never been explicitly bound?
The container's build() method creates a ReflectionClass for the requested concrete class. It calls getConstructor() to get a ReflectionMethod, then getParameters() on that to get all constructor parameters. For each ReflectionParameter, it checks getType(): if the type is a class or interface name (not a primitive), it recursively calls $this->make() on that type. This process continues depth-first until all dependencies are resolved. If a parameter has no type-hint and no default value, the container throws BindingResolutionException. The whole tree is constructed bottom-up: deepest dependencies first, then the requested class last with all its dependencies already instantiated.
Q: What is the performance cost of automatic resolution, and how does production optimization mitigate it?
Each automatic resolution creates a ReflectionClass instance, which reads and parses the class's compiled opcodes. For deeply nested dependency graphs (a controller resolves a service which resolves a repository which resolves a connection), dozens of ReflectionClass objects are created per request. In production, php artisan optimize (which runs config:cache, route:cache, and event:cache) reduces the number of files that need to be read. Some teams use packages like laravel-preload or enable opcache.preload to warm the Reflection cache. The most impactful optimization is explicit singleton bindings for heavily-used services — the container skips Reflection entirely when it finds the abstract in $instances.
Q: Why can't the container automatically resolve primitive constructor parameters like string $apiKey?
PHP's ReflectionParameter::getType() for a parameter typed as string returns a ReflectionNamedType with isBuiltin() === true. There is no way for the container to determine which string value to inject from the type alone — string could be an API key, a URL, a name, or anything else. The container would need external information (a binding or contextual binding) to know the correct value. Class or interface type-hints solve this because the type name itself uniquely identifies the dependency. The container can look it up in bindings or use Reflection to construct it. This is why best practices suggest wrapping primitive configuration in value objects or named configuration classes: app()->bind(StripeConfig::class, fn() => new StripeConfig(config('services.stripe.key'))) makes the primitive invisible to dependent classes.