With dependency injection, a class receives its collaborators through its constructor instead of building them, so a test can pass fakes. A container reads constructor types with Reflection and builds and shares each dependency.
<?php
final class Container {
private array $made = [];
public function get(string $class): object {
return $this->made[$class] ??= $this->autowire($class); // one shared instance
}
private function autowire(string $class): object { // read the constructor
$params = new ReflectionClass($class)->getConstructor()?->getParameters() ?? [];
return new $class(...array_map(fn(ReflectionParameter $p) => $p->isDefaultValueAvailable()
? $p->getDefaultValue() : $this->get($p->getType()->getName()), $params));
}
}
final class Mailer { function send(string $to, string $m): void { echo "to $to: $m\n"; } }
final class Numbers { private int $n = 5001; function next(): int { return $this->n++; } }
final class Checkout { // asks for what it needs, builds nothing
public function __construct(private Numbers $numbers, private Mailer $mailer) {}
public function place(string $to): void {
$this->mailer->send($to, "order #{$this->numbers->next()} confirmed");
}
}
$c = new Container();
$c->get(Checkout::class)->place('ann@example.com');
$c->get(Checkout::class)->place('raj@example.com');Output
to ann@example.com: order #5001 confirmed to raj@example.com: order #5002 confirmed
The shared Numbers gave #5002. Real containers add bindings for interfaces and configuration. Call the container only at the entry point; a service locator hides dependencies again. PSR-11 standardizes get()/has(), PHP-DI 2,774 (https://github.com/PHP-DI/PHP-DI 2,774 ) (7.1.1, composer require php-di/php-di) adds bindings and a compiled container, and Laravel covers Laravel 2,157 's.