In PHP, how does a tiny dependency-injection container use ReflectionClass and getParameters() to autowire a class's constructor dependencies?
answer
- getConstructor() may be null
- getParameters() then getType()
- ReflectionNamedType::isBuiltin()
- defaults and allowsNull() for the rest
- guard against circular dependencies
basics
~20 sFor a requested class it calls getConstructor(), walks getParameters(), and for each parameter whose type is a ReflectionNamedType naming a class, resolves that class recursively; scalars need a default or explicit config. It then calls newInstanceArgs() with the resolved list.
solid answer
~40 sThe container reflects the class with `new ReflectionClass($id)`, refuses it unless `isInstantiable()`, and takes `getConstructor()` — `null` means `new $id()` with no arguments. For each `ReflectionParameter` from `getParameters()` it reads `getType()`: a `ReflectionNamedType` with `isBuiltin()` false names a class or interface, so the container resolves `getName()` recursively (after mapping interfaces to implementations). Builtin types such as `string` or `int`, untyped parameters, and union or intersection types cannot be guessed, so it falls back to `isDefaultValueAvailable()`/`getDefaultValue()`, to `null` if `allowsNull()`, or to explicit configuration, and otherwise throws a clear error naming the parameter. It collects arguments keyed by `getName()` and calls `newInstanceArgs()`. A resolution stack detects cycles, and a cache of built singletons avoids constructing shared services twice.
code
php · 29 lines<?php
declare(strict_types=1);
final class Container
{
private array $instances = [];
private array $resolving = [];
public function __construct(private array $bindings = []) {}
public function get(string $id): object
{
if (isset($this->instances[$id])) return $this->instances[$id];
if (isset($this->resolving[$id])) throw new LogicException("Circular dependency on $id");
$this->resolving[$id] = true;
$ref = new ReflectionClass($this->bindings[$id] ?? $id);
if (!$ref->isInstantiable()) throw new LogicException("$id is not instantiable");
$args = [];
foreach ($ref->getConstructor()?->getParameters() ?? [] as $p) {
$t = $p->getType();
if ($t instanceof ReflectionNamedType && !$t->isBuiltin()) $args[$p->getName()] = $this->get($t->getName());
elseif (!$p->isDefaultValueAvailable()) throw new LogicException("Cannot resolve \${$p->getName()} of $id");
}
unset($this->resolving[$id]);
return $this->instances[$id] = $ref->newInstanceArgs($args);
}
}go deeper
Know that reflection can list a constructor's parameters and their types, which lets a container build objects automatically.
Walk through getConstructor(), getParameters(), getType(), isBuiltin() and the fallbacks for scalars, then newInstanceArgs() with named keys.
Handle cycles, shared instances, union types and interface bindings, and produce errors that name the exact unresolvable parameter.
Decide how much a codebase should autowire versus bind explicitly, balancing boilerplate against how visible the wiring is to reviewers.
## What autowiring means A **dependency-injection container** builds objects for you. **Autowiring** is the part where it works out a class's constructor arguments by itself, by reading the parameter types with the Reflection API instead of requiring a hand-written factory for every service. Given ```php final class InvoiceMailer { public function __construct( private MailerInterface $mailer, private InvoiceRepository $invoices, private string $fromAddress = '[email protected]', ) {} } ``` the container should resolve `MailerInterface` and `InvoiceRepository` and use the default for `$fromAddress`. ## The reflection calls involved | Call | Role in autowiring | |---|---| | `new ReflectionClass($id)` | describe the requested class; `ReflectionException` if it does not exist | | `isInstantiable()` | reject interfaces, abstract classes, non-public constructors | | `getConstructor()` | `?ReflectionMethod`; `null` means no arguments needed | | `getParameters()` | ordered `ReflectionParameter` list | | `ReflectionParameter::getType()` | `?ReflectionType`: named, union, intersection, or `null` when untyped | | `ReflectionNamedType::isBuiltin()` | `true` for `int`, `string`, `array`, `mixed` and the like | | `ReflectionNamedType::getName()` | the class or interface name, without the `?` | | `isDefaultValueAvailable()` / `getDefaultValue()` | fallback for parameters it cannot resolve | | `ReflectionType::allowsNull()` | whether `null` is an acceptable fallback | | `newInstanceArgs($args)` | construct, with string keys as named arguments | ## The resolution algorithm 1. If the id is bound to a factory or a concrete class (for example `MailerInterface` to `SmtpMailer`), use that binding. 2. Reflect the class; if `isInstantiable()` is false, throw — an interface with no binding cannot be built. 3. If `getConstructor()` is `null`, return `new $class()`. 4. For each parameter: - a `ReflectionNamedType` that is not builtin: resolve `getName()` recursively; - otherwise, if configuration supplies a value for this parameter name, use it; - otherwise, if `isDefaultValueAvailable()`, use `getDefaultValue()` (or simply omit the argument); - otherwise, if the type `allowsNull()`, pass `null`; - otherwise throw an error naming the class and parameter. 5. Call `newInstanceArgs($args)` with the arguments keyed by parameter name. ## Edge cases a real container must handle - **Circular dependencies.** `A` needs `B` and `B` needs `A`: naive recursion never ends. Keep a stack of ids being resolved and throw when an id reappears, showing the chain. - **Shared instances.** Most services should be built once per request; cache them by id. - **Union and intersection types.** `getType()` returns `ReflectionUnionType` or `ReflectionIntersectionType`; guessing which member to build is unsafe, so require configuration. - **Variadic parameters.** `isVariadic()` marks `LoggerInterface ...$loggers`; autowiring cannot know how many, so it needs explicit wiring. - **Nullable classes.** `?Logger` is a `ReflectionNamedType` whose `getName()` is `Logger` and whose `allowsNull()` is `true`; decide whether to resolve it or pass `null`. - **Self-referencing types.** A parameter typed `self` must be resolved relative to the declaring class before the container looks it up. ## Testing a container A container is small but easy to break silently, so its tests usually cover: 1. a class with no constructor, built with no arguments; 2. a chain of two or three class-typed dependencies, checking that shared services are the same instance (`assertSame`); 3. an interface with and without a binding, expecting a clear error in the second case; 4. a scalar parameter with a default, which must be left to PHP, and one without, which must fail naming the parameter; 5. a two-class cycle, which must fail fast with the chain in the message instead of exhausting memory. Those five cases exercise every branch of the reflection walk above, and each failure message should name the class and the parameter so a developer can fix the wiring without a debugger. ## Why not autowire everything Autowiring removes boilerplate but hides wiring decisions: which implementation of an interface a service gets is no longer visible in code. Most containers therefore combine autowiring for concrete classes with explicit bindings for interfaces and scalars. The standard interface for fetching services from a container is a separate concern from how the container builds them.
- Why does the container skip parameters that have defaults instead of passing getDefaultValue()?Because it keys arguments by parameter name, `newInstanceArgs()` treats them as named arguments, and any optional parameter left out simply takes its default. That avoids evaluating defaults twice and works for defaults that are constant expressions or `new` objects.
- What does getType()->getName() return for a PHP parameter typed ?LoggerInterface?`LoggerInterface`, without the question mark. The nullability is reported separately by `allowsNull()`, which is `true`. Casting the type to string gives `?LoggerInterface`, which is why comparing strings from `__toString()` is fragile.
saying these in an interview costs you the question
- getConstructor() always returns a ReflectionMethod, even without a constructor.
- getType() always returns a ReflectionNamedType for any typed parameter.
- The container can resolve an interface-typed parameter without any binding.
- isBuiltin() is true for any class that ships with PHP, like DateTime.
- Recursion alone handles circular dependencies because PHP detects the loop.