Den folgenden Vortrag habe ich auf der Contao Konferenz 2018 in Salzburg gehalten. Da Folien alleine meinst nur verständlich sind, wenn man den Vortrag gesehen hat, habe ich diesen Blogartikel verfasst. Außerdem ist der Quelltext aller Beispiele auf Github unter eS-IT/EventDrivenDevelopment_ck2018 verfügbar.
Event-Driven-Development
Einleitung
Was sind Events?
Ein Ereignis (englisch event) dient in der Softwaretechnik – bei Entwicklung nach dem ereignisorientieren Programmierparadigma – zur Steuerung des Programmflusses. Das Programm wird nicht linear durchlaufen, sondern es werden spezielle Ereignisbehandlungsroutinen (engl. listener, observer, event handler) immer dann ausgeführt, wenn ein bestimmtes Ereignis auftritt.
Quelle: https://de.wikipedia.org/wiki/Ereignis_(Programmierung)
Mit Events ist es also möglich, den Programmablauf dynamisch zu beeinflussen. Es kann somit sehr flexibel auf geänderte Anforderungen reagiert werden. Zusätzlich ist es für Drittentwickler möglich, nachträglich Änderungen und Erweiterungen der Software vorzunehmen.
Und warum will man das?
Durch die lose Kopplung der Komponenten steigt die Flexibilität. Es entsteht besser zu testender Quelltext, die Codequalität steigt. Prinzipien wie SOLID, Clean Code und Test-Driven Development sind prädestiniert dafür, mit Events umgesetzt zu werden.
Beispiele aus der Praxis
Universelle Import-/Exportmodule
Da man nicht voraussetzen kann, dass es für alle Erweiterungen Contao-Models, oder Doctrine-Entities gibt, wird man hier auf SQL zurückgreifen müssen. Die Querys kann man sehr gut über Events erstellen. So kann leicht auf geänderte Rahmenbedingungen reagiert werden.
Frontend-Ansichten
Für die Ausgabe im Frontend müssen oft Werte aus der DB konvertiert werden. Schreibt man allgemeingültige Module, weiß man natürlich im Vorfeld nicht, welche Daten damit angezeigt werden und wie die Daten konvertiert werden sollen. Stellt man nun ein entsprechendes Event zur Verfügung, kann der Nutzer hier bequem seine individuelle Konvertierung einfügen, indem er einfach seinen Listener registriert.
Berechnungen
Auch Berechnungen für Bestellsysteme oder Konfiguratoren lassen sich hervorragend mit Events umsetzten. Zum einen können die einzelnen Bestandteile besser aufgeteilt werden, zum anderen kann auch nachträglich noch sehr einfach auf geänderte Brechungsgrundlagen reagiert werden.
Es gibt sicher zahlreiche weitere Beispiele. Für diese Einführung soll diese aber erst einmal reichen.
Grundlagen
Bevor wir nun zu den Events kommen, will ich noch ganz kurz auf einige Grundlagen eingehen. Die hier gezeigten Grundlagen sind bewusst sehr einfach gehalten und stellen in der Regel die kleinst mögliche Lösung dar. Es wird in Kauf genommen, dass es sich nicht um optimale Lösungen handelt. Im Laufe der Ausführungen werden bessere Lösungen erarbeitet.
Autoloading
In der Regel wird man in seinem Projekt verschiedene Abhängigkeiten haben. In diesem Fall wird vermutlich composer zum Einsatz kommen. Dieser kann dann auch gleich das Autoloading für uns übernehmen. Wenn wir eine Klasse Foo\\Bar\\Baz
haben, die wir in src/Bar/Baz.php
gespeichert haben, reicht folgender Eintrag im autoload
-Abschnitt unserer composer.json
:
Der Vendornamespace Foo
wird nicht berücksichtigt und der Rest wird auf die Ordnerstruktur unter src/
abgebildet.
Da bei mir in der Regel der Vendornamespace unter src/
liegt, sieht der Eintrag bei mir so aus:
Wenn eine Klasse den Namen \Esit\SuperBundle\Classes\Helper\Url
trägt, bezieht sich dies auf die Datei ROOT/src/Esit/SuperBundle/Classes/Helper/Url.php
. Hier wird also nichts weggelassen.
Nutzt man nicht composer
, kann man auch folgenden Code benutzen.
Quelle: simast at gmail dot com @ php.net
Braucht man für Events Dependency Injection?
Nein, aber es hilft! Oftmals benötigen die Klassen, die sich um die Verarbeitung der Events kümmern andere Klassen. Will man sich nicht selbst um die Instanzierung komplexer Abhängigkeitshierarchien kümmern, kann man einen Dependency Injection Container nutzen. Wikipedia meint hierzu:
Als Dependency Injection (englisch dependency ‚Abhängigkeit‘ und injection ‚Injektion‘; Abkürzung DI) wird in der objektorientierten Programmierung ein Entwurfsmuster bezeichnet, welches die Abhängigkeiten eines Objekts zur Laufzeit reglementiert: Benötigt ein Objekt beispielsweise bei seiner Initialisierung ein anderes Objekt, ist diese Abhängigkeit an einem zentralen Ort hinterlegt – es wird also nicht vom initialisierten Objekt selbst erzeugt.
Quelle: https://de.wikipedia.org/wiki/Dependency_Injection
Ein sehr einfacher Dependency Injection Container ist z.B. Dice. Hier ist keine zusätzliche Konfiguration nötig, da dice
die Type Hints nutzt, um die Abhängigkeiten ausfindig zu machen.
Quelle: r.je/dice.html
Wie man sieht, reicht auch bei komplexen Vererbungsstrukturen ein einfacher Aufruf von $dice->create('...');
. Installiert wird dice einfach mit composer require level-2/dice
im ROOT des Projekts.
EventSubscriber
EventSubscriber sind Klassen, die sich selbst für ein Event registrieren. Es gibt also keine zentrale Konfigurationsdatei mehr. Dies macht die Sache noch wesentlich flexibler, aber auch sehr unübersichtlich. Um herauszufinden in welcher Reihenfolge die Listener aufgerufen werden, ist hier mit unter sehr viel Aufwand nötig.
Ich setze deshalb EventSubscriber nicht mehr ein. Dies ist aber vermutlich Geschmackssache.
Events ganz einfach
Da wir nun alle nötigen Grundlagen haben, machen wir uns an ein erstes Beispiel für einen simplen Event Dispatcher. Wir gehen davon aus, dass alle Klassen unter src/
liegen und per Autoload gefunden werden.
Konfiguration
Als erstes kümmern wir uns um die Konfiguration. In der Datei ROOT/config/events.php
tragen wir die Listener ein, die bei einem bestimmten Event aufgerufen werden sollen.
Event Dispatcher
Hier wird ein sehr einfacher Event Dispatcher gezeigt. Es wird an dieser Stelle bewusst auf Feinheiten verzichtet.
Definition eines Events
Mit diesem Event wird eine Grußbotschaft erstellt. Beim Erstellen wird ihm ein Name übergeben und der Listener erstellt dann die Grußbotschaft. Es gibt zwei Eigenschaften: name
für den Namen und message
für die Grußbotschaft. Zusätzlich wird noch eine Konstante definiert (NAME
). Hierbei handelt es sich um den Namen, mit dem das Event aufgerufen wird. Wichtig ist, dass der Name einzigartig sein muss!
Listener
Die Verarbeitung ist sehr einfach, es wird einfach der Methode das Event übergeben. Da Objekte immer als Referenz übergeben werden, muss man sich nicht um die Rückgabe kümmern.
Aufruf eines Events
Will man nun ein Event aufrufen, benötigt man das Event und den Event Dispatcher.
Einfügen eines weiteren Listeners
Wollen wir der Grußbotschaft nun z.B. später noch etwas hinzufügen, registrieren wir in ROOT/config/events.php
einfach einen weiteren Listener. Dieser kann in der selben Datei, oder ganz woanders sein.
Nun erstellen wir den neuen Listener und führen die gewünschten Operationen durch.
Nun würde in der Klasse ROOT/src/Helper/EventCaller.php
am Ende nicht mehr "Hallo Leo!" ausgegeben, sondern "Hallo Leo! Contao ist toll!".
Selbstverständlich ist es auch möglich, Listener zu löschen, oder vor bzw. zwischen bestehenden Listenern neue einzufügen. Des Weiteren ist es meist so, dass die Listener ihrerseits wieder Events erstellen und somit andere Listener aufrufen. Der Phantasie sind hier keine Grenzen gesetzt. Zusätzlich führt diese Art der Programmierung zu besser wartbarem und vor allem besser testbarem Code.
Verzeichnisstruktur
Zur Verdeutlichung der Vorgänge hier noch einmal die komplette Verzeichnisstruktur:
Alle relevanten Dateien sind auch auf Github zu finden. Sie liegen im Ordner 03_SimpleEvent
. Dort kann alles nachvollzogen werden.
Events unter Contao 3?
Da Contao 3 nicht mehr lange gepflegt wird, soll hier nur der Vollständigkeit halber auf die Hooks und Callbacks eingegangen werden. Das Autoloading geschieht über ROOT/system/moduel/ERWEITERUNG/config/autoload.php
. Die Klassen können im Prinzip überall gespeichert werden (z.B. unter ROOT/system/moduel/ERWEITERUNG/src/
).
Events, Hooks und Callbacks?
Was ist nun der Unterschied zwischen Events, Hooks und Callbacks? Die in Contao verwendeten Hooks und Callbacks sind Spezialformen von Events. Der größte Unterschied ist, dass hier statt mit Event-Objekten, mit Parametern gearbeitet wird. Mit den Hooks beeinflusst man den grundsätzlichen Programmablauf, mit den Callbacks greift man in die Verarbeitung des DCA ein.
Hooks
Die Hooks werden unter 04_Contao3/ROOT/system/moduel/ERWEITERUNG/config/config.php
konfiguriert.
Auf das Autoloading soll an dieser Stelle nicht eingegangen werden!
Der Listener wäre in diesem Fall die Klasse MyHook
. Sie könnte z.B. so aussehen:
Quelle: Contao Handbuch
Callbacks
Im Gegensatz zu Hooks werden die Callbacks im DCA konfiguriert.
Die Klasse MyCallback
könnte wie folgt aussehen:
Quelle: e@sy Solutions IT - Blog
EventHelper
Es spricht nichts dagegen, das eben gezeigt Vorgehen mit der Klasse \Esit\Helper\EventHelper
unter Contao 3 einzusetzen. Ich habe eine ähnliche Klasse bereits im Einsatz, es funktioniert sehr gut. Die Klasse mit dem Aufruf (\Esit\Content\EventCaller
) könnte dann z.B. die Klasse eines Inhaltselements oder Module sein.
Aufruf
Hier ein Beispiel für den Aufruf des oben definierten Events in einem normalen Inhaltselement:
Wenn man die Grundlagen einmal verstanden hat, ist es ganz einfach unter Contao 3 mit Events zu arbeiten.
Verzeichnisstruktur
Damit dieses Beispiel funktioniert, müssen die Namespaces angepasst werden! Diese ergeben sich aus der folgenden Ordnerstruktur mit dem Vendornamespace Esit
.
Alle relevanten Dateien sind auch auf Github zu finden. Sie liegen im Ordner 04_Contao3
. Dort kann alles nachvollzogen werden.
Events unter Contao 4 mit Symfony
Contao Manager
Damit das Bundle geladen wird, benötigen wir das Plugin des Contao Managers. Zunächst müssen wir den Ort des Plugins in die composer.json
eintragen und die Einstellungen für das Autoloading unseres Bundles vornehmen. Wir ergänzen die Datei wie folgt:
Nun können wir unser Bundle für den Manager konfigurieren. Wichtig ist, dass die Klasse ContaoManagerPlugin
heißt und im globalen Namespace liegt. Der Pfad wird in der composer.json
unter classmap
angegeben.
Weitere Detail findet man im Handbuch.
Zusätzlich benötigen wir noch die unter Symfony obligatorische Bundle-Datei:
Laden der Services
Für das Laden der Konfigurationen wird eine Extenstion-Datei benötigt. Diese wird vom \Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass
beim Laden der ServiceContainer aufgerufen. Die Datei muss im Ordner DependencyInjection
des Bundles liegen und nach dem Schema VENDORNAMESPACE``BUNDLENAME``Extension
benannt sein. In unserem Beispiel ist dies VendorPackageExtension
. Außerdem muss die Klasse von der Klasse Symfony\Component\DependencyInjection\Extension\Extension
erben. Es können hier auch Werte im Container gespeichert werden. Diese sind ähnlich den Werten, die Contao in $GLOBALS
speichert.
Konfigurieren der Services und Listener
Die Services und Listener werden in einer YAML-Datei konfiguriert. Der Name der Datei ist hier nicht so wichtig, da sie über die Extension-Klasse (VendorPackageExtension
) geladen werden und dort die Namen angegeben werden können.
Über die Tags wird definiert, dass es sich bei einem Services um EventListener (name: kernel.event_listener
) handelt und mit event: greeting.event
wird der Name des Events angegeben, auf das der Listener reagiert.
Der Eintrag unter class
zeigt die Klasse (z. B. Vendor\Package\EventListener\OnGreetingListener
) die aufgerufen wird und der Tag method
die Methode (z.B. generateGreeting
). Der Name des Eintrags (vendor_package_bundle.listener.greeting.listener.generateGreeting
) muss eindeutig sein, ist aber frei wählbar. Ich habe mir deshalb angewöhnt, hier eine Kombination aus Bandlename, Klassenname des Listeners, gefolgt von der aufzurufenden Methode zu verwenden. Dies ist zwar lang, aber ziemlich eindeutig.
Event
Das Event enthält wie immer den Namen des Events in der Konstante NAME
, die Eigenschaften und die Getter und Setter. Es erbt von \Symfony\Component\EventDispatcher\Event
.
Listener
Der Listener kümmert sich um die eigentliche Verarbeitung. Mit dem Aufruf des ersten Listeners OnGreetingListener::generateGreeting()
wird der Gruß erzeugt.
Mit dem Aufruf des zweiten Listeners OnGreetingListenerTwo::generateMessage()
wird die Botschaft erzeugt.
Aufruf
Um nun ein Event aufrufen zu können, benötigt man einen EventDispatcher. Dieser kann über die System-Klasse von Contao mit \System::getContainer()->get('event_dispatcher')
bezogen werden. Dann wird wie immer eine Instanz des Events erstellt, mit Daten befüllt und ausgelöst.
Der Rest wie DCA, Language-Dateien usw. bleibt wie bei Contao 3.
Verzeichnisstruktur
Hier ein Listing der Verzeichnisstruktur des Bundles:
Alle relevanten Dateien sind auch auf Github zu finden. Sie liegen im Ordner 05_Contao4
. Dort kann alles nachvollzogen werden.
Ich helfe gerne
Da die Anforderungen im Bereich Softwareentwicklung immer komplexer werden, fällt es zunehmend schwerer, fortwährend höchste Qualität zu erbringen. Ich beschäftige mich seit Jahren mit Themen wie Softwarequalität, Softwarestrukturierung und Event-driven Development. Ich habe hierzu einige Veranstaltungen (u. a. bei Sebastian Bergmann) besucht und selbst mehrere Vorträge (z.B. auf der Contao Konferenz oder dem Contao Barcamp) gehalten. Gerne helfe ich anderen Entwicklern bei der Einführen entsprechender Strukturen und Techniken in ihren Firmen.
Kommentare
Einen Kommentar schreiben