PHP Dependency Injection ohne Container
Dependency Injection ist kein Framework-Feature. Es ist ein Prinzip: Abhängigkeiten werden von außen übergeben statt innen erzeugt. Das macht Code testbar, austauschbar und verständlich — ohne Container, ohne Reflection-Magie.
Das Problem
class OrderService
{
public function create(array $data): int
{
// Abhängigkeiten werden intern erzeugt
$db = new PDO('mysql:host=localhost', 'user', 'pass');
$mailer = new Mailer('smtp.example.com', 587);
$logger = new FileLogger('/var/log/app.log');
// ...
}
}
Nicht testbar. Nicht konfigurierbar. Nicht austauschbar.
Constructor Injection
class OrderService
{
public function __construct(
private readonly PDO $db,
private readonly Mailer $mailer,
private readonly LoggerInterface $logger,
) {}
public function create(array $data): int
{
$this->logger->info('Creating order', $data);
// ...
}
}
Alles was die Klasse braucht steht im Konstruktor. Kein Verstecken von Abhängigkeiten.
Interfaces für Austauschbarkeit
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
}
class FileLogger implements LoggerInterface { ... }
class NullLogger implements LoggerInterface
{
public function info(string $message, array $context = []): void {}
public function error(string $message, array $context = []): void {}
}
Im Test: NullLogger übergeben. In Produktion: FileLogger.
Einfaches Composition Root
Statt Container: eine zentrale Stelle wo alles verkabelt wird.
// bootstrap.php — einmal am Anfang, nicht überall
$db = new PDO(
sprintf('mysql:host=%s;dbname=%s', DB_HOST, DB_NAME),
DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$logger = new FileLogger('/var/log/app.log');
$mailer = new SmtpMailer(SMTP_HOST, SMTP_PORT);
$orderService = new OrderService($db, $mailer, $logger);
$userService = new UserService($db, $logger);
Das ist der gesamte "Container". Explizit, nachvollziehbar, kein Magic.
Wann ein Container sinnvoll wird
Wenn das Composition Root sehr groß wird (>50 Services) oder wenn man Lazy-Loading braucht (Service erst erzeugen wenn er gebraucht wird) kann ein einfacher Container helfen.
class Container
{
private array $bindings = [];
private array $instances = [];
public function bind(string $id, callable $factory): void
{
$this->bindings[$id] = $factory;
}
public function get(string $id): mixed
{
if (!isset($this->instances[$id])) {
$this->instances[$id] = ($this->bindings[$id])($this);
}
return $this->instances[$id];
}
}
$c = new Container();
$c->bind(PDO::class, fn() => new PDO(...));
$c->bind(OrderService::class, fn(Container $c) => new OrderService($c->get(PDO::class), ...));
$orderService = $c->get(OrderService::class);
Das sind ~20 Zeilen. Kein Composer-Paket, keine Reflection. Reicht für die meisten Projekte die mehr als Bootstrap.php brauchen aber kein Symfony-DI-Container wollen.