Artikel
von Patrick Froch

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.

Zurück

Kommentare

Aufgrund der unklaren Rechtslage durch die DSGV habe ich mich entschlossen, die Kommentare bis auf Weiteres zu deaktivieren.