Erweiterung des OXID eShopsmit Hilfe des Symfony DI containers

Erweiterung des OXID eShops
mit Hilfe des Symfony DI containers
Teil 3: Erweiterung der Shop-Logik mit Hilfe desSymfony DI containers
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. Imzweiten Teil dieser Seriehaben wir gesehen, wie Module denDI containernutzen können. Und zwar einfach dadurch, dass eine services.yaml-Datei in dasroot-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 einesshop serviceserstellen und diese unter demkeyder Originalklasse registrieren? Das läge schließlich auf der Hand.
Das wird aber nicht funktionieren. Tatsächlich wird sich das OXIDframeworkweigern, ein Modul zu aktivieren, in dem einserviceaus demshopüberschrieben wird. Warum diese Schikane? Dafür gibt es zwei Gründe:
Zum einen könnte immer nur ein einziges Modul einenserviceerweitern. 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 einermultishopUmgebung automatisch für alleshopsaktiv. Einer der Vorteile von OXID Modulen ist, dass sie selektiv für einzelneshopsaktiviert werden können, während ihre Installation in anderenshopskeine Auswirkung hat. Das würde aber nicht funktionieren, wennshop servicesersetzt würden. Diese würden dann immer für die gesamte Installation geändert.
Aber gilt das nicht auch für interneserviceseines 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 einemshopaktiviert ist oder nicht. Das verhindert, dass interneserviceseines Moduls fürshopsaufgerufen 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 einenshop serviceüberschreibt und dass ich die Änderung für alle meinesubshopsbrauche? Sollte ich da nicht die Möglichkeit haben, von den Fähigkeiten desDI containersGebrauch 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 alleservicesdurch 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 einenservices, 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 einenshop 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, welcheshop servicesin 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 normalencomposer packagezu machen. Und in diesempackagekann auch derDI containergenutzt werden: Wenn in der composer.json-Datei einescomposer packagesder Typ auf "oxideshop-component" gesetzt wird, dann überprüft das OXIDcomposer plugin, ob in diesempackageeine 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önnenservicesinnerhalb despackageskonfiguriert werden, es ist aber nicht möglich, shop services zu ersetzen. Dazu muss derservicein die configurable_services.yaml-Datei eingetragen werden.
Diese Art der Erweiterung ist allerdings ein Entweder-Oder. Entweder einservicewird ersetzt oder nicht. Was aber ist mit Codeerweiterungen, die nur in bestimmtensubshopsaktiv sein sollen? Der traditionelle OXID Programmierstil mit oxNew() erlaubt es bekanntlich, die Erweiterung von Klassen gezielt für einzelnesubshopszu 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 jeweiligenshop, gültig.
Shopspezifische Symfony Events
Hier kommt die andere Erweiterungsmöglichkeit ins Spiel, die uns derSymfony DI containerzur Verfügung stellt:Events. Eine ausführliche Erläuterung, wie events funktionieren und wie sie konfiguriert werden können, findet sich in derSymfony Dokumentation.
Das Prinzip ist einfach: An verschiedenen Stellen desshop codessind 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. Dersubscriberbekommt dabei das Event-Object übergeben, das, je nach Event, auch eine gewissepayloadenthalten kann. Welche Events es gibt, steht in derEntwicklerdokumentation; 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ürshopsaufgerufen werden, für die das Modul auch aktiviert ist. Das lässt sich ganz einfach dadurch bewerkstelligen, dass man densubscribernicht einfach das von Symfony bereitgestellte EventSubscriberInterface implementieren läßt, sondern von der Klasse AbstractShopAwareEventSubscriber erbt, die Teil des OXIDframeworksist. Das sorgt automatisch dafür, dass die entsprechenden EventSubscriber so konfiguriert werden, dass sie wissen, für welcheshopssie 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 dieOXID consoleimplementiert, die auf derSymfony consolebasiert. Auch hier kann manconsole commandsso imDI containerregistrieren, dass sie nur für bestimmteshopsaktiv 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.
