Viele PHP-Projekte starten klein: eine Klasse, ein paar Helfer, ein Datenbankzugriff. Nach einigen Wochen entstehen jedoch versteckte Abhängigkeiten, doppelte Logik und Tests, die kaum möglich sind. Ein häufiger Grund: Klassen erstellen ihre Abhängigkeiten selbst (z. B. new Mailer(), new PDO()) statt sie von außen zu bekommen.
Dependency Injection (kurz DI) bringt Ordnung hinein: Eine Klasse sagt nur, was sie braucht, und bekommt es von außen geliefert. Das klingt simpel – und ist es im Kern auch. Entscheidend ist, die Technik sauber und ohne „Framework-Magie“ zu verstehen.
Dependency Injection in PHP: Was genau ist damit gemeint?
Eine Abhängigkeit ist alles, was eine Klasse benötigt, um zu funktionieren: zum Beispiel ein Logger, ein Repository (Datenzugriff), ein HTTP-Client oder ein Mailer. Bei Dependency Injection werden diese Abhängigkeiten nicht in der Klasse erzeugt, sondern „injiziert“ (eingespritzt) – meist über den Konstruktor.
Ohne DI: harte Kopplung durch new
Ein typisches Beispiel ohne DI:
Eine Klasse NewsletterService erstellt sich intern einen Mailer. Das Problem: Der Service ist fest an genau diese Implementierung gebunden. Ein Austausch (z. B. für Tests oder für einen anderen Provider) wird unnötig schwer.
Mit DI: Abhängigkeiten kommen von außen
Mit DI bekommt NewsletterService den Mailer übergeben. Damit wird die Klasse flexibler: Für Tests kann ein Fake-Mailer genutzt werden, in Produktion ein SMTP-Mailer, später vielleicht ein API-basierter Mailer – ohne dass die Business-Logik umgeschrieben werden muss.
Konstruktor, Setter oder Interface: welche DI-Variante passt?
DI ist kein einzelner Trick, sondern ein Prinzip. In PHP sind vor allem drei Formen verbreitet.
Konstruktor-Injection: der Standard für „Pflicht“-Abhängigkeiten
Wenn eine Klasse ohne Abhängigkeit nicht sinnvoll arbeiten kann (z. B. Repository, Logger), ist Konstruktor-Injection meist die beste Wahl. Vorteil: Das Objekt ist nach dem Erstellen sofort in einem gültigen Zustand.
Praktischer Nebeneffekt: Die Signatur des Konstruktors zeigt klar, was die Klasse braucht. Das verbessert Lesbarkeit und Wartbarkeit.
Setter-Injection: für optionale Abhängigkeiten (mit Vorsicht)
Setter-Injection eignet sich, wenn eine Abhängigkeit optional ist oder erst später gesetzt werden kann. Nachteil: Es entsteht leicht ein „halbfertiges“ Objekt. Dann treten Fehler erst zur Laufzeit auf, weil der Setter vergessen wurde.
Wenn Setter genutzt werden, hilft eine klare Regel: Nur für wirklich optionale Features einsetzen, und möglichst defensive Checks einbauen (z. B. frühzeitig abbrechen, wenn die Abhängigkeit fehlt).
Injection über Interfaces: entkoppeln, ohne zu übertreiben
In PHP wird DI besonders nützlich, wenn statt konkreter Klassen Interfaces (Verträge) genutzt werden. Eine Klasse hängt dann nicht mehr von „MailerXY“ ab, sondern von „MailerInterface“. Das macht Austauschbarkeit und Testbarkeit deutlich besser.
Wichtig ist dabei Maßhalten: Nicht jedes kleine Helferlein braucht sofort ein Interface. Sinnvoll ist es vor allem an Grenzen des Systems: Datenbankzugriff, HTTP-Calls, Mailversand, Dateisystem, Cache.
Warum DI Tests einfacher macht (und Bugs reduziert)
Tests scheitern häufig nicht an komplizierter Logik, sondern an Seiteneffekten: echte Datenbank, echte API, echte E-Mails. DI hilft, diese Effekte zu isolieren.
Fake, Stub, Mock: kurz erklärt
Für Tests werden Abhängigkeiten oft ersetzt:
- Fake: eine einfache Ersatz-Implementierung (z. B. Mailer, der nur in ein Array schreibt).
- Stub: liefert vordefinierte Antworten (z. B. Repository gibt immer denselben Datensatz zurück).
- Mock: prüft Interaktionen (z. B. „wurde
send()einmal aufgerufen?“).
DI sorgt dafür, dass diese Ersatzobjekte überhaupt sauber eingehängt werden können. Ohne DI sind Tests oft gezwungen, mit globalen Zuständen oder komplizierten Workarounds zu arbeiten.
Typische Bug-Quelle: versteckte Abhängigkeiten
Wenn eine Klasse intern new PDO(...) baut, ist Konfiguration plötzlich verteilt: DSN hier, Credentials dort, Options irgendwo. Bei Änderungen wird leicht eine Stelle vergessen. DI bündelt Konfiguration an einem Ort (z. B. in einem Bootstrap oder Container) und reduziert so „Konfigurations-Bugs“.
DI-Container verstehen: praktische Hilfe, kein Muss
Ein DI-Container (auch Service Container) ist eine zentrale Stelle, die Objekte erstellt und Abhängigkeiten automatisch zusammensetzt. Viele Frameworks bringen das mit, aber das Prinzip funktioniert auch ohne Framework.
Wann lohnt sich ein Container?
- Es gibt viele Services, die sich gegenseitig brauchen.
- Objekte müssen konfiguriert werden (z. B. Logger mit Handlern).
- Abhängigkeiten sollen je Umgebung variieren (Dev/Test/Prod).
In sehr kleinen Projekten reicht oft ein einfacher „Composition Root“: eine Stelle, an der Objekte manuell zusammengesetzt werden (z. B. in index.php oder einem Bootstrap-Skript). Das ist bereits DI – nur ohne Automatisierung.
Wichtiger Begriff: Composition Root (wo alles verdrahtet wird)
Der Composition Root ist der Ort, an dem konkrete Implementierungen gewählt werden. Innerhalb der eigentlichen Fachlogik sollte möglichst wenig „Wiring“ passieren. Das Ziel: Business-Code bleibt frei von new-Ketten und Infrastrukturdetails.
So geht’s: Dependency Injection Schritt für Schritt einführen
- Eine Klasse auswählen, die heute viele Dinge selbst erstellt (z. B. Datenbank, Logger, Mailer).
- Diese Abhängigkeiten als Konstruktor-Parameter definieren und intern speichern.
- Die Erstellung der Abhängigkeiten an eine zentrale Stelle verschieben (Bootstrap/Controller/Container).
- Als nächsten Schritt an Systemgrenzen Interfaces einführen (z. B.
MailerInterface), damit Implementierungen austauschbar werden. - Für Tests Fakes oder Stubs bereitstellen und prüfen, ob der Service ohne Seiteneffekte testbar ist.
Häufige Fehler bei DI (und wie sie sich vermeiden lassen)
DI ist schnell erklärt, aber in echten Projekten passieren immer wieder dieselben Stolpersteine.
Zu viele Abhängigkeiten im Konstruktor
Wenn ein Konstruktor zehn Parameter hat, ist das oft ein Geruch: Die Klasse macht wahrscheinlich zu viel. DI macht dieses Problem sichtbar, löst es aber nicht automatisch. Dann hilft Refactoring: Verantwortlichkeiten trennen, Logik in kleinere Services aufteilen.
Ein guter nächster Schritt ist oft, fachliche Schritte zu bündeln: statt „Mailer, TemplateRenderer, Translator, Logger“ vielleicht ein „NotificationService“, der diese Details kapselt.
Service Locator statt DI (versteckte Abhängigkeiten)
Manchmal wird ein Container überall hineingereicht und dann mitten im Code genutzt: „Hol mir mal X aus dem Container“. Das wirkt bequem, ist aber das Gegenteil von DI: Abhängigkeiten werden wieder versteckt. Besser: Abhängigkeiten explizit injizieren und den Container nur im Composition Root verwenden.
Primitive Obsession: Konfiguration als Parameterflut
Wenn statt Services plötzlich überall Strings und Arrays herumgereicht werden (API-Keys, Pfade, Optionen), wird es unübersichtlich. Besser ist ein kleines Konfigurationsobjekt oder ein Value Object, das zusammengehörige Werte bündelt. So bleibt der Konstruktor verständlich und Änderungen sind lokal.
Mini-Fallbeispiel: Newsletter-Versand wird testbar
Ausgangslage: Ein Newsletter-Service erstellt intern einen Mailer und sendet direkt. Tests scheitern, weil sie echte E-Mails vermeiden müssen. Außerdem lässt sich der Versand später nicht ohne weiteres auf einen neuen Provider umstellen.
Umstellung mit DI:
- Der Service bekommt ein Interface für Mailversand über den Konstruktor.
- In Produktion wird eine konkrete Implementierung genutzt (SMTP oder API).
- Im Test wird ein Fake-Mailer injiziert, der nur protokolliert, welche E-Mails „gesendet“ worden wären.
Ergebnis: Der Service kann ohne Netzwerk, ohne Credentials und ohne Seiteneffekte getestet werden. Gleichzeitig ist ein Provider-Wechsel eine Konfigurationsfrage, keine Code-Operation.
Checkliste: Wann ist Dependency Injection hier sinnvoll?
- Es gibt Logik, die unabhängig von Infrastruktur sein sollte (z. B. Geschäftsregeln).
- Code soll automatisiert testbar sein (Unit-Tests ohne Datenbank/API).
- Implementierungen müssen austauschbar sein (Provider, Datenquelle, Cache).
- Konfiguration soll an einem Ort liegen und nicht in vielen Klassen verteilt sein.
- Abhängigkeiten sollen sichtbar sein (Konstruktor zeigt Anforderungen).
FAQ: Dependency Injection in PHP kurz beantwortet
Ist DI nur etwas für Frameworks wie Symfony oder Laravel?
Nein. Frameworks liefern oft einen Container, aber das Prinzip funktioniert auch ohne. Schon das manuelle Übergeben von Abhängigkeiten ist DI.
Macht DI den Code nicht komplizierter?
DI verschiebt Komplexität: weg von vielen Klassen hin zu einer zentralen Verdrahtung. In kleinen Projekten kann das zunächst „mehr Code“ sein. Langfristig wird Wartung aber oft einfacher, weil Abhängigkeiten nicht mehr versteckt sind.
Was ist der Unterschied zwischen DI und Inversion of Control?
Inversion of Control (IoC) ist das übergeordnete Prinzip: Kontrolle über die Erstellung und den Ablauf wird umgedreht (das System „ruft“ den Code, nicht umgekehrt). DI ist eine konkrete Technik, um IoC umzusetzen: Abhängigkeiten werden von außen geliefert.
Welche Verbindung hat DI zu Clean Code?
DI fördert kleine, gut testbare Einheiten und macht Kopplungen sichtbar. Das passt zu vielen Clean-Code-Praktiken. Passend dazu hilft auch der Überblick zu Clean Code im Alltag (auch wenn das Beispiel dort aus dem Frontend kommt, sind die Prinzipien ähnlich).
Wie passt DI zu sicheren Webanwendungen?
DI selbst ist keine Sicherheitsfunktion, unterstützt aber saubere Grenzen zwischen Schichten. Dadurch lassen sich Sicherheitsmaßnahmen (z. B. Validierung) konsistenter platzieren. Für Eingaben ist zudem Input-Validierung im Backend eine sinnvolle Ergänzung.
Wer häufig externe APIs nutzt, profitiert ebenfalls: Infrastruktur-Code bleibt austauschbar, während Fehlerbehandlung und Statuscodes zentral definiert werden können. Dazu passt API-Design mit Statuscodes.

