Erweiterung des OXID eShops
mit Hilfe des Symfony DI containers
Teil 3: Erweiterung der Shop-Logik mit Hilfe des Symfony DI containers
Im ersten Teil dieser kleinen Serie über dependency injection im Rahmen des OXID eShop frameworks haben wir uns damit auseinandergesetzt, warum die Technik der inversion of control wichtig ist, um wart- und testbare Software zu schreiben. Im zweiten Teil dieser Serie haben wir gesehen, wie Module den DI container nutzen können. Und zwar einfach dadurch, dass eine services.yaml-Datei in das root-Verzeichnis des Moduls gelegt wird.
Die eigentliche Codeerweiterung haben wir allerdings auf die traditionelle Weise vorgenommen, indem wir eine Erweiterung in der metadata.php-Datei für eine Shopklasse registriert haben.
Generelle Probleme bei direkten Erweiterungen von Shop Services
Aber warum nicht einfach in der services.yaml-Datei direkt einen Shop-Service erweitern? Dann fiele die Dateierweiterung mit dem etwas seltsamen <classname>_parent-Konstrukt weg. Warum nicht einfach eine Unterklasse eines shop services erstellen und diese unter dem key der Originalklasse registrieren? Das läge schließlich auf der Hand.
Das wird aber nicht funktionieren. Tatsächlich wird sich das OXID framework weigern, ein Modul zu aktivieren, in dem ein service aus dem shop überschrieben wird. Warum diese Schikane? Dafür gibt es zwei Gründe:
Zum einen könnte immer nur ein einziges Modul einen service erweitern. Wenn zwei verschiedene Module das versuchen, dann gewinnt einfach das später aktivierte Modul. Das ist kein wünschenswertes Verhalten.
Multishop Umgebungen erweitern
Und zum anderen wäre die Änderung in einer multishop Umgebung automatisch für alle shops aktiv. Einer der Vorteile von OXID Modulen ist, dass sie selektiv für einzelne shops aktiviert werden können, während ihre Installation in anderen shops keine Auswirkung hat. Das würde aber nicht funktionieren, wenn shop services ersetzt würden. Diese würden dann immer für die gesamte Installation geändert.
Aber gilt das nicht auch für interne services eines Moduls? Im Prinzip ja, in der Praxis nein. Denn wir benötigen ja, wie im letzten Blogbeitrag beschrieben, einen traditionellen Einstiegspunkt, der über oxNew() instantiiert wird. Und dieser Einstieg ist abhängig davon, ob das Modul in einem shop aktiviert ist oder nicht. Das verhindert, dass interne services eines Moduls für shops aufgerufen werden, für die sie nicht gedacht sind.
Shop Services mit Modulen überschreiben
Was aber, wenn ich ganz genau weiß, dass nur ein einziges von meinen Modulen diesen einen shop service überschreibt und dass ich die Änderung für alle meine subshops brauche? Sollte ich da nicht die Möglichkeit haben, von den Fähigkeiten des DI containers Gebrauch zu machen? Das ist in der Tat eine berechtigte Forderung.
Und genau hier kommt die bereits im letzten Blogbeitrag erwähnte Datei var/configuration/configurable_services.yaml ins Spiel. Hier ist der einzige Ort, wo alle services durch alternative Implementierungen ersetzt werden dürfen.
Die Implementierung selbst kann ruhig in einem Modul liegen und auch durch die services.yaml-Datei des Moduls konfiguriert werden. Nur die Alternativimplementierung des einen services, der ersetzt werden soll muss in die configurable_services.yaml-Datei eingetragen werden. Und zwar manuell. Die Entwicklerin soll ganz bewusst die Entscheidung treffen: Ja, ich will einen shop service überschreiben und ich will das genau mit dieser einen Klasse aus meinem Modul tun. Dadurch wird die configurable_services.yaml-Datei gleichzeitig zu einer Dokumentation, welche shop services in einem bestimmten Projekt durch Alternativimplementierungen ersetzt wurden.
OXID composer packages
Diese Implementierungen müssen nicht zwingend in einem OXID-Modul liegen. Es gibt auch die Möglichkeit, das in einem ganz normalen composer package zu machen. Und in diesem package kann auch der DI container genutzt werden: Wenn in der composer.json-Datei eines composer packages der Typ auf "oxideshop-component" gesetzt wird, dann überprüft das OXID composer plugin, ob in diesem package eine services.yaml-Datei liegt. Wenn ja, wird sie wie bei einem OXID Modul in die generated_services.yaml-Datei eingebunden. Hier gilt dasselbe, was auch für die services.yaml-Datei in Modulen gilt: Es können services innerhalb des packages konfiguriert werden, es ist aber nicht möglich, shop services zu ersetzen. Dazu muss der service in die configurable_services.yaml-Datei eingetragen werden.
Diese Art der Erweiterung ist allerdings ein Entweder-Oder. Entweder ein service wird ersetzt oder nicht. Was aber ist mit Codeerweiterungen, die nur in bestimmten subshops aktiv sein sollen? Der traditionelle OXID Programmierstil mit oxNew() erlaubt es bekanntlich, die Erweiterung von Klassen gezielt für einzelne subshops zu aktivieren oder zu deaktivieren. Mit Eintragungen in die configurable_services.yaml-Datei geht das nicht. Was hier eingetragen wird, ist für die gesamte Installation, unabhängig vom jeweiligen shop, gültig.
Shopspezifische Symfony Events
Hier kommt die andere Erweiterungsmöglichkeit ins Spiel, die uns der Symfony DI container zur Verfügung stellt: Events. Eine ausführliche Erläuterung, wie events funktionieren und wie sie konfiguriert werden können, findet sich in der Symfony Dokumentation.
Das Prinzip ist einfach: An verschiedenen Stellen des shop codes sind Event-Aufrufe platziert. Für diese Events kann man sich mit Hilfe eines sogenannten EventSubscribers anmelden; und wenn die Ausführung des Codes an die entsprechende Stelle kommt, wird der Code des EventSubscribers aufgerufen. Der subscriber bekommt dabei das Event-Object übergeben, das, je nach Event, auch eine gewisse payload enthalten kann. Welche Events es gibt, steht in der Entwicklerdokumentation; dort finden sich auch Codebeispiele, die die Verwendung erläutern.
Hier soll nur darauf hingewiesen werden, dass die EventSubscriber so geschrieben werden können, dass sie nur für shops aufgerufen werden, für die das Modul auch aktiviert ist. Das lässt sich ganz einfach dadurch bewerkstelligen, dass man den subscriber nicht einfach das von Symfony bereitgestellte EventSubscriberInterface implementieren läßt, sondern von der Klasse AbstractShopAwareEventSubscriber erbt, die Teil des OXID frameworks ist. Das sorgt automatisch dafür, dass die entsprechenden EventSubscriber so konfiguriert werden, dass sie wissen, für welche shops sie aktiv sind. Dies geschieht ebenfalls in der generated_services.yaml-Datei, die im vorhergehenden Blogpost dieser Reihe bereits erwähnt wurde.
Der gleiche Mechanismus ist übrigens für die OXID console implementiert, die auf der Symfony console basiert. Auch hier kann man console commands so im DI container registrieren, dass sie nur für bestimmte shops aktiv sind. Die abstrakte Klasse, von der hier geerbt werden muss, heißt wenig originell AbstractShopAwareCommand.
Und damit sind wir am Ende unserer kleinen Tour angekommen, auf der wir gezeigt haben, wie der in OXID integrierte Symfony DI container bei der Modul- und Projektentwicklung genutzt werden kann. Wir hoffen, dass Euch das bei Eurer Arbeit hilft; und natürlich freuen wir uns immer über Feedback.