Dependency Injection innerhalb von OXID Modulen

Teil 2: DI innerhalb von Modulen
Imersten Teil dieser kleinen Serieüberdependency injectionim Rahmen desOXID eShop frameworkshaben wir uns damit auseinandergesetzt, warum die Technik derinversion of controlwichtig ist, um wart- und testbare Software zu schreiben. In diesem und dem nächsten Teil geht es um die konkrete Umsetzung.
Schauen wir uns zunächst einmal an, wie man innerhalb eines Moduls den DI container nutzen kann, um seinen eigenen Code besser zu strukturieren undinversion of controlzu praktizieren. Für die Erweiterung desshop codesbelassen wir es hier bei der traditionellen Methode der Erweiterung mittels des oxNew()-Mechanismus. Neuere Arten der Erweiterung lernen wir dann im dritten Teil der Serie kennen.
Symfony DI container und Inversion of Control
Wie bereits erwähnt benutzt OXID denSymfony DI containeruminversion of controlzu unterstützen. Bei einer vollständig aufinversion of controlberuhenden Applikation kommt man in der Regel überhaupt nicht mit demDI containerkaum in Berührung, höchstens bei dessen Konfiguration. In Symfony beispielsweise injiziert dieroutingKomponente dieservicesaus demcontainerdirekt in diecontroller- man fasst dencontaineralso selbst gar nicht an.
In OXID geht das leider nicht ganz so einfach, da derDI containernoch auf traditionelles Routing stößt. Deshalb gibt es die Möglichkeit, sich direkt denDI containerzu holen und benötigteservicesvomcontaineranzufordern - ihn also zumindest teilweise eher alsresource locatorzu benutzen. Dazu gibt es die ContainerFactory-Klasse. Diese stellt dann eine getContainer()-Methode zur Verfügung. Diefactoryselbst wird über einenstaticAufruf instantiiert:
Dercontainerselbst ist einPSR-11kompatiblerSymfony DI container, der im Normalfall aus einercache-Datei gelesen wird, was ihn recht schnell macht. Diesecache-Datei heißt container_cache.php und liegt im tmp-Verzeichnis der Applikation. Wenn man also manuell die Konfiguration descontainersändert, muss man diese Datei löschen, um dencontainerzu aktualisieren.
Den Container konfigurieren
Wie wird nun dercontainerkonfiguriert? Wir haben uns bei OXID dafür entschieden, dencontainervollständig mityaml-Dateien zu konfigurieren. Dabei werden der Reihe nach die folgenden Dateien gelesen, falls sie existieren:
Die Logik für die ersten drei services.yaml-Dateien ist dabei einigermaßen offensichtlich: Zunächst wird die Konfiguration derservicesfür diecommunity editiongelesen. Danach bekommen dieprofessional editionund dieenterprise editiondie Chance, bestimmteserviceserneut zu konfigurieren: In der Regel geht es dabei darum, die einfachen Services aus dercommunity editiondurch komplexereservicesaus den höherwertigen Editionen zu ersetzen.
Was aber hat es mit dergenerated_services.yaml-Datei auf sich? Hier wird es interessant für Modulentwickler: Diese Datei wird vomOXID frameworkselbst geschrieben. Sie sollte also weder von Hand editiert noch gelöscht werden. Unter anderem kann sich diese Datei ändern, wenn ein Modul aktiviert oder deaktiviert wird. Wenn nämlich im root-Verzeichnis eines OXID-Moduls eineservices.yamlliegt, dann wird diese Datei in dergenerated_services.yaml-Datei inkludiert. Für Module-Schreiber heißt dies: Wenn ich mir selbstservicesfür mein Modul schreiben will, dann muss ich weiter nichts tun als eineservices.yaml-Datei mit meinenservice-Definitionen in das root-Verzeichnis meines Moduls legen. Beim Aktivieren des Moduls werden meineservicesdann über den Container zugänglich sein.
Ein Beispiel aus der Praxis
Wie sieht das in der Praxis aus? Nehmen wir einmal an, jemand will ein Modul für die Preiskalkulation schreiben, das komplett die Preiskalkulation in der Article-Klasse überschreibt. Dann wird zunächst einmal ein Einstiegspunkt erstellt, ganz traditionell:
Dann wird diese Klasse in der metadata.php des Moduls als Erweiterung der Article-Klasse registriert, auch das ganz traditionell. Neu ist, wie dann der eigentliche Code für die Preisberechnung ausgeführt wird:
Wir machen nur eine Sache: Wir holen uns dencontainer, holen uns von dort eine Einstiegsklasse und führen auf dieser genau eine Methode aus. Den gesamten Rest der Implementierung können wir dann nach dem Prinzip derinversion of controlaufbauen, Tests schreiben etc.
Natürlich müssen wir unsere Implementierung auch noch im container registrieren. Dazu legen wir eine services.yaml Datei in unserem Modul an. Diese könnte typischerweise folgendermaßen aussehen (genauere Informationen über die Struktur und die Möglichkeiten einer solchen Datei finden sich in derSymfony-Dokumentation):
Zunächst werden Standardparameter für dieservice-Definitionen erstellt: Es sollautowiringverwendet werden und dieservicessollen nichtpublicsein.Autowiringheißt, dass wenn im Konstruktor einesservicesdasinterfaceeines anderenservicessteht und dies eindeutig ist, dann braucht die Abhängigkeit gar nicht konfiguriert werden, dercontainerlöst diese Abhängigkeit automatisch auf. Das funktioniert natürlich nur, wenn alsservice keysdieinterfacesder Klassen verwendet werden. Aber das ist sowieso gute Praxis, die wir auch bei OXID verwenden. Und auch Modulschreiber sollten das beherzigen, wenn möglich.
Dass dieservicesnichtpublicsein sollen ist ebenfalls gute Praxis, um die öffentliche API so klein wie möglich zu halten. Wir haben uns bei OXID entschieden, diese öffentlichen Klassen "Bridges" zu nennen und würden dies auch Moduleentwicklern empfehlen. Danach haben wir in diesem Beispiel noch zwei weitereservice-Klassen, den PriceCalculationService, der die Geschäftslogik implementiert, und ein PriceCalculationDao, also eindata access object, in dem wir die Datenbanklogik kapseln.
Dasdaowird in dendomain serviceinjiziert, derdomain servicedann in diebridgeund dankautowiringgeht das automatisch, wenn wir die Signatur der Konstruktoren entsprechend anlegen:
Bei der Aktivierung des Moduls wird der Import dieser services.yaml-Datei dann in die generated_services.yaml-Datei eingefügt und dieservicessind imcontainerverfügbar.
Im nächsten Teil beschäftigen wir uns dann mit der configurable_services.yaml-Datei und alternativen Möglichkeiten, den Code und die Geschäftslogik des OXID eShop frameworks zu erweitern, jenseits der traditionellen oxNew()-Methode.
