Was bedeutet Dependency Injection in TYPO3?
Dependency Injection steht für ein Architekturprinzip. Dabei werden Objekte, die in Abhängigkeit stehen, vom Framework bereitgestellt. Die benötigten Abhängigkeiten werden einem Objekt von außen zugeführt (injiziert). Das passiert meist über den Konstruktor. Wenn beispielsweise eine Newsletter-Komponente einen E-Mail-Versandservice benötigt, soll sie diesen nicht selbst instanziieren, da dies zu einer festen Kopplung führt. Stattdessen wird der gewünschte Mail-Service vom Framework in die Newsletter-Komponente injiziert. Dieses Vorgehen entkoppelt den Code und ermöglicht, den Mail-Service bei Bedarf später auszutauschen, ohne den Newsletter-Code ändern zu müssen. Die Konfiguration entscheidet dadurch darüber, welche konkrete Implementierung als Abhängigkeit übergeben wird – nicht der aufrufende Code.
Der Einsatz von Dependency Injection in TYPO3 bringt verschiedene Vorteile mit sich:
Vor TYPO3 Version 10 erfolgte die Objekterstellung in TYPO3 meist prozedural über Helfer wie GeneralUtility::makeInstance(). Ab TYPO3 Version 4.x ermöglichte das Extbase-Framework erstmals Dependency Injection ähnliche Konzepte. Der Extbase ObjectManager konnte mittels Annotation @inject Abhängigkeiten in Extbase-Klassen bereitstellen. Diese Lösung funktionierte, hatte aber Nachteile. Der ObjectManager nutzte Reflection zur Laufzeit, um benötigte Abhängigkeiten zu erkennen und über inject*-Methoden zuzuweisen. Der Vorgang war relativ langsam und wurde nur durch Caching in der Praxis performant nutzbar. Die notwendige Reflection bei jeder Instanziierung brachte Performance-Overhead und komplexe Cache-Invalidierungen mit sich. Daher blieb diese Dependency-Injection-Lösung auf Extbase beschränkt und wurde nie für den gesamten Core übernommen.
Ab TYPO3 Version 10 wurde eine moderne Dependency-Injection-Lösung auf Basis des Symfony Dependency Injection Components in den Core integriert. Diese Container-Architektur ersetzt den Extbase ObjectManager vollständig und steht Core-weit sowie in Extensions zur Verfügung. Wichtig ist, dass auch GeneralUtility::makeInstance() seither intern auf den Symfony-Container zurückgreift, falls dort ein Service registriert ist. Der große Vorteil der Symfony-Lösung liegt darin, dass alle Abhängigkeitsinformationen build-time (beim Cache-Warmup) ausgelesen und in einer einzigen Container-Cache-Datei gespeichert werden. Bei der eigentlichen Anfrage entfällt so die teure Laufzeit-Analyse. Objekte werden sehr performant aus dem vorbereiteten Container abgerufen. Dies verbessert die Performance und Skalierbarkeit von TYPO3 deutlich.
Im Core von TYPO3 wurden Stück für Stück alte Techniken abgelöst. Mit TYPO3 v11 wurde im Core begonnen, den Extbase ObjectManager nicht mehr zu nutzen. Aufrufe erzeugten Deprecation-Warnungen. In TYPO3 v12 wurde der Extbase ObjectManager schließlich entfernt, sodass nur noch der Symfony-Container als Dependency-Injection-Mechanismus übrig bleibt. Alte Extbase-Property-Injections funktionieren seitdem nicht mehr. Stattdessen setzt der Core konsequent auf Constructor Injection und Service-Configuration. Diese Modernisierung wird in TYPO3 v13 fortgeführt. DI ist nun ein fester Bestandteil der TYPO3-Architektur. Durch den Rückgriff auf etablierte Symfony-Komponenten profitiert TYPO3 von einem vertrauten, gut dokumentierten Ökosystem. Das erleichtert Entwicklern auch den Einstieg. Die Verwendung bewährter Standard-Tools macht TYPO3 zudem zukunftssicher und reduziert Entwicklungsaufwand im Core.
Damit eigene Extbase-Extensions bzw. custom PHP-Klassen vom DI-Container erfasst werden, ist eine Konfigurationsdatei erforderlich. In EXT:meine_extension/Configuration/Services.yaml werden die Services der Extension definiert. Eine minimale Services.yaml nutzt typischerweise Symfony-Konventionen mit _defaults und einem Namespace-Prefix der Extension-Klassen. Alle PHP-Klassen unterhalb eines bestimmten Namespaces werden als Services registriert, ausgenommen Domain-Model-Klassen. Standardmäßig sollte Autowiring und Autoconfigure aktiviert werden. Services sollten zunächst nicht als public definiert werden, sofern kein direkter Container-Zugriff von außen nötig ist.
Mit autowire: true in der Services.yaml wird festgelegt, dass der DI-Container Abhängigkeiten anhand der Typdeklarationen automatisch auflösen soll. Der Symfony-Container scannt Konstruktoren und mit Extbase auch vorhandene inject*-Setter auf benötigte Parameter und ermittelt passende Services nach Type-Hint. Dieses sogenannte Autowiring erzeugt beim Cache-Warmup entsprechenden Code für die Objekterstellung und legt ihn im TYPO3-Core-Cache ab. Entwickler müssen dann nicht jedes Dependency-Mapping manuell konfigurieren. So wird Aufwand gespart und Fehlerquellen reduziert..
Mit autoconfigure: true werden Services basierend auf bestimmten Interfaces oder Basisklassen automatisch mit zusätzlichen Attributen/Tags versehen. Beispielsweise sorgt Autokonfiguration dafür, dass Klassen, die das TYPO3-Interface SingletonInterface implementieren, vom Container automatisch als Singleton-Services behandelt werden (d.h. shared, eine Instanz) und öffentlich verfügbar sind. So bleibt das Verhalten von Singletons ohne manuelle Einstellung erhalten. Autoconfigure reduziert den Konfigurationsaufwand, da viele TYPO3-typische Klassen automatisch erkannt und korrekt registriert werden.
Domain-Modelle (Extbase Domain/Model Klassen) werden explizit vom Dependency-Injection-Container ausgeschlossen. Diese Modelle repräsentieren Datenobjekte und werden weiterhin von Extbase selbst (z.B. via Repository) erstellt. Sie sind keine Services im klassischen Sinn und sollten keine weiteren Abhängigkeiten benötigen. Außerdem unterliegt die Extbase-Persistenz nicht dem Dependency-Injection-Container, weshalb eine Injection in Models nicht funktioniert. Eine bewährte Herangehensweise ist, nur Service-Klassen (Controller, Repository, Helper, Manager etc.) im Container zu verwalten, nicht die reinen Daten-Objekte.
Nach Änderungen an der Services-Konfiguration (Services.yaml) oder an Constructor-Signaturen muss der DI-Container-Cache erneuert werden. Ein einfacher „Clear Cache“ im Backend reicht dabei nicht aus. TYPO3 verlangt einen gezielten Flush des Symfony Containers. Dies ist entweder über Admin Tools > Maintenance oder den CLI-Befehl cache:flush bzw. composer dumpautoload möglich. So ist sichergestellt, dass der Container die aktuelle Konfiguration neu einliest und alle Services korrekt (re)kompiliert. Bei Entwicklungsprojekten sollte dieser Schritt nach Anpassungen stets im Hinterkopf behalten werden, um unerklärliche Fehler bei der Dependency Injection zu vermeiden.
TYPO3 selbst liefert viele seiner APIs und Dienste über den Dependency-Injection-Container aus. Diese Core-Services stehen direkt per Injection zur Verfügung. Eigene Extensions können und sollten dieses Muster übernehmen, um Konsistenz zu gewährleisten. Statt globale Helper zu verwenden, werden Core-Dienste wie LoggerInterface, ConnectionPool oder CacheManager einfach als Abhängigkeiten in den eigenen Klassen eingebunden.
Die bevorzugte Methode, Dependencies einzubinden, ist über den Konstruktor (Konstruktor-Injektion). Man deklariert die benötigten Services als Parameter im __construct(). TYPO3 (bzw. der Symfony-Container) erkennt diese beim Erzeugen der Klasse und übergibt automatisch passende Instanzen. Ein Extbase-Controller, der einen eigenen UserRepository nutzt, definiert diesen als Konstruktor-Parameter. Beim Erstellen des Controllers liefert der Container dann automatisch eine fertige UserRepository-Instanz hinein, ohne dass im Controller-Code makeInstance o.ä. aufgerufen werden muss. Konstruktor-Injektion macht Abhängigkeiten explizit, immutable (bei Verwendung von private readonly in PHP 8+) und erleichtert so Verständnis und Testbarkeit der Klasse.
TYPO3 unterstützt auch weiterhin die Injektion via Setter-Methoden (Setter-Injection bzw. Inject-Methoden) nach dem Muster injectXYZ(). Diese Methode wurde aus Extbase übernommen und im Core zusätzlich implementiert. Bei Setter-Injection werden Abhängigkeiten zunächst ohne Parameter instanziiert (z.B. via makeInstance) und danach durch Aufruf der inject*-Methoden in die Zielklasse gesetzt. In der Praxis sollte Setter-Injection nur genutzt werden, wenn Konstruktor-Injektion nicht möglich ist. Dies kann der Fall sein, um optionale Abhängigkeiten bereitzustellen oder Abhängigkeiten aufzulösen. Bei Verwendung von Inject-Methoden muss die Klasse ohne diese Abhängigkeiten instanziierbar sein (kein zwingender Konstruktor-Parameter). Der DI-Container ruft die inject*-Methoden dann automatisch auf, sobald das Objekt erstellt wurde.
Die frühere Praxis, Dependencies per @inject-Annotation direkt in protected/private Eigenschaften injizieren zu lassen (Property-Injection), ist in neueren TYPO3-Versionen nicht mehr erlaubt. Seit TYPO3 v10+ wurde diese Property Injection als veraltet markiert und aus Performance-Gründen abgeschafft. Hintergrund ist, dass zur Injektion in private/protected Properties Reflection genutzt und zur Laufzeit Setter generiert werden mussten: Ein recht ineffizienter Prozess. Bestehender Code mit @inject-Properties sollte auf Konstruktor- bzw. Setter-Injection migriert werden. So bleibt der Code kompatibel mit TYPO3 v11+ und profitiert von den Vorteilen des Symfony-Containers.
In modernem TYPO3-Code sollten direkte Aufrufe von GeneralUtility::makeInstance() oder dem Extbase-ObjectManager möglichst vermieden werden, sofern Dependency Injection genutzt werden kann. Statt ein Objekt manuell zu erzeugen, wird es als Abhängigkeit deklariert und lässt den Container liefern. Nur in Ausnahmefällen, zum Beispiel für Klassen, die nicht container-fähig sind, greift man noch zu makeInstance(). Beispiele hierfür sind Extbase-Domainmodelle (werden durch Extbase selbst erstellt) oder bestimmte Core-Klassen, die im Konstruktor zwingende runtime-Daten benötigen. Solche Objekte können gegebenenfalls über Factory-Services injiziert werden. Dabei wird eine Service-Klasse geschrieben, die gewünschte Objekte erzeugt. Anschließend wird sie sie als public im Container markiert und schließlich via $container->get() bzw. makeInstance() aufgerufen. So bleibt der eigentliche Anwendungscode trotzdem DI-konform und testbar, auch wenn im Hintergrund noch ein manuelles Instanziieren nötig ist.
Dependency Injection hat sich in TYPO3 seit Version 10 zum zentralen Bestandteil der Core-Architektur entwickelt. Insbesondere in den aktuellen LTS-Versionen wie v12 und v13 sorgt DI für konsistente, performante Abläufe bei der Objekterzeugung und fördert eine saubere Trennung der Verantwortlichkeiten im Code. Für Entwicklungsteams bedeutet dies besser wartbaren Code, weniger Fehlersuche nach versteckten Abhängigkeiten und leichtere Anpassbarkeit an zukünftige Anforderungen. Wer noch Legacy-Patterns im Einsatz hat, sollte jetzt den Wechsel vollziehen, um TYPO3-Projekte zukunftssicher und update-freundlich zu halten.
Wir von 3m5. als spezialisierte TYPO3 Agentur verfügen über mehr als 15 Jahre Erfahrung mit dem Enterprise-CMS TYPO3 und gehören zu den Top 3 der größten TYPO3-Dienstleister - im deutschsprachigen Raum sowie international. Als offizieller TYPO3-Partner mit Fokus auf business-kritische Anwendungen wissen wir, worauf es bei modernen TYPO3-Projekten ankommt – von der Konzeption bis zur Umsetzung auf Code-Ebene. Im Kontext von Dependency Injection bedeutet das: Wir bei 3m5. implementieren Ihre Anwendungen nach den neuesten Best Practices und Core-Richtlinien. Unsere Entwickler nutzen den Symfony Dependency-Injection-Container gezielt, um sauberen, testbaren und erweiterbaren Code zu schreiben. Gleichzeitig unterstützen wir Sie bei der Migration bestehender TYPO3-Installationen auf aktuelle LTS-Versionen und helfen, alte Patterns durch zukunftssichere Lösungen zu ersetzen.

Im Sinne des Open Source Gedanken nutzen wir unser Coding-Wissen gemeinsam mit anderen, um Neos noch besser zu machen. Ein Einblick in die kollaborative Arbeit am Neos Core.

