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.