© saicle/Shutterstock.com
CQRS mit dem Zend Framework 2 implementieren

Alles hört auf mein Kommando


Von einem Artikel inspiriert, habe ich mich daran gemacht, CQRS mit dem Zend Framework 2 zu implementieren. Das Ergebnis des Experiments finden Sie in folgendem Artikel.

Stefan Priebsch hat in der letzten Ausgabe des PHP Magazins einen sehr interessanten Artikel über CQRS veröffentlicht, den ich jedem Entwickler nur ans Herz legen kann [1]. Ich hatte bereits Stefans Vortrag „Think big: Architecture is recursive“ auf der letztjährigen IPC in München verfolgt und fand das Thema CQRS damals schon sehr interessant [2]. Durch das Zitat „ […], wenn man denn unbedingt möchte, sogar innerhalb eines MVC-Frameworks […]“ aus besagtem Artikel fühlte ich mich herausgefordert, dass Thema CQRS auch einmal mit dem Zend Framework 2 zu implementieren.

Wir wollen uns hierbei nicht groß mit der Theorie aufhalten, sondern steigen gleich in die Implementierung ein. Wer den erwähnten Artikel noch nicht gelesen hat, sollte dies an dieser Stelle dringend nachholen. Ebenfalls wäre die Lektüre der Artikel „Vor dem Event ist nach dem Event“ [3] und „Stets zu Diensten“ [4] sehr hilfreich für das Verständnis dieses Artikels.

ZF2-Beispielanwendung

Damit Sie die Beispiele selbst nachvollziehen können, steht eine Beispielanwendung auf GitHub für Sie bereit [5]. Darin wollen wir das Backend eines Pizzaservice teilweise umsetzen, um die Prinzipien der Trennung von lesenden und schreibenden Zugriffen zu implementieren. Sie können das Projekt wie folgt klonen (bitte ggf. die Verzeichnisse anpassen) und installieren. Dabei setzen wir auch noch die Schreibrechte des Verzeichnisses /data/:

$ cd /home/devhost $ git clone https://github.com/RalfEggert/phpmagazin.cqrs $ cd phpmagazin.cqrs $ php composer.phar selfupdate $ php composer.phar install $ sudo chmod 777 -R data/

Danach richten Sie einen Virtual Host für die Adresse phpmagazin.cqrs ein. Wenn Sie nun http://phpmagazin.cqrs/ in Ihrem Browser aufrufen, sollte die Seite ungefähr wie in Abbildung 1 aussehen. Die Listings zu diesem Artikel finden Sie sowohl auf GitHub [6] als auch in diesem Repository im Verzeichnis /listings/.

eggert_cqrs_1.tif_fmt1.jpgAbb. 1: Aufruf von phpmagazin.cqrs

Das Prinzip „Command Query Responsibility Segregation“ schreibt nicht nur die Trennung der Lese- und Schreibzugriffe vor, sondern erfordert eine für die Lesezugriffe optimierte Repräsentation der Daten. Für die folgende Implementierung habe ich mich auch für eine Zweiteilung der Daten entschieden. Während die Daten für die Schreibzugriffe in einer MySQL-Datenbank vorgehalten werden, verwenden wir MongoDB für die Lesezugriffe. Dabei müssen wir jedoch sicherstellen, dass die Daten in der MongoDB auch aktualisiert werden, nachdem die Daten in der MySQL-Datenbank verändert worden sind.

Um auf MySQL zugreifen zu können, müssen Sie an dieser Stelle eine MySQL-Datenbank anlegen und dort den Dump aus der Datei /data/import/dump.mysql.sql einspielen. Außerdem sollten Sie einen entsprechenden Benutzer anlegen, der lesend und schreibend auf Ihre MySQL-Datenbank zugreifen kann. Alternativ können Sie auch jede andere Datenbank verwenden, die vom Zend Framework 2 unterstützt wird [7]. Ihre Zugangsdaten legen Sie bitte in der Konfigurationsdatei /config/autoload/local.php im Projektverzeichnis ab.

MongoDB installieren

Unter Ubuntu Linux installieren Sie zuerst mongodb per apt-get und danach den PHP-Treiber per pecl:

$ sudo apt-get install mongodb $ sudo pecl install mongo

Danach fügen Sie die PHP-Extension mongo.so in Ihrer php.ini ein und starten den Apache 2 neu.

Unter Windows laden Sie den passenden Treiber herunter [8] und verschieben ihn ins PHP-Extension-Verzeichnis. Danach fügen Sie die Extension php_mongo.dll in Ihrer php.ini ein und starten den Apache 2 ebenfalls neu.

Denken Sie in beiden Fällen auch daran, die Extension auch für die Kommandozeile CLI zu aktivieren.

Wenn Sie MongoDB bisher noch nicht installiert haben, holen Sie das bitte nach (Kasten: „MongoDB installieren“). Danach können Sie die beiden Dateien ­/ data/import/pizzas.mongodb.json und /data/import/toppings.mongodb.json importieren:

$ cd /home/devhost/phpmagazin.cqrs $ mongoimport -d pizzadb -c pizzas --file data/import/pizzas.mongodb.json $ mongoimport -d pizzadb -c toppings --file data/import/toppings.mongodb.json 

Um zu prüfen, ob die Daten in der MongoDB angekommen sind, starten Sie MongoDB auf der Konsole, wählen die Datenbank pizzadb aus und lassen sich die Collections ausgeben:

$ mongo > use pizzadb > show collections > db.toppings.find() > db.pizzas.find() > exit

Wenn Sie diese Hürden erklommen haben, können Sie endlich loslegen.

Überblick über die Implementierung

Die Implementierung ist aufgrund der Anforderungen etwas komplexer geworden und verteilt sich auf mehrere Module. Das Modul PizzaRead ist für die lesenden Zugriffe per REST-Schnittstelle zuständig, während das Modul PizzaChange für alle schreibenden Zugriffe per REST-Schnittstelle verantwortlich ist. Das Modul PizzaCli dient dazu, nach den schreibenden Zugriffen den Zustand der Daten in der MongoDB für die lesenden Zugriffe auf Kommandozeilenebene zu aktualisieren. Das Modul MongoDB ist für die Anbindung an die MongoDB-Datenbank zuständig, und das Modul CQRS wiederum enthält die grundlegende Klasse für die Umsetzung von Commands und Events im CQRS.

Die beiden Pizza-Module PizzaRead und PizzaChange können somit derzeit nicht über ein Web-Front­end bedient werden, sondern müssen per RESTful Web Service angesteuert werden. Somit enthalten sie keinerlei Viewskripte oder View-Helper. Es sollte wegen der REST-Schnittstellen jedoch kein großes Problem darstellen, ein entsprechendes Frontend, z. B. auf Basis von JavaScript, umzusetzen. Das Modul PizzaCli wird auf der Kommandozeile ausgeführt und eignet sich sehr gut dafür, mithilfe eines Cronjobs regelmäßig automatisch ausgeführt zu werden. Nach jeder schreibenden Operation sorgt das Modul PizzaChange dafür, dass der entsprechende Command in eine temporäre Queue gestellt wird, die dann von dem PizzaCli-Modul verarbeitet werden kann, um den Datenbestand der MongoDB zu aktualisieren.

Es wäre durchaus denkbar gewesen, diese drei Module auch zu einem großen Pizza-Modul zusammenzuführen. Ich habe mich jedoch bewusst für diese Trennung entschieden. Zum einen folgt diese Trennung auch dem Motto „Separation of Concerns“, dass CQRS zugrunde liegt. Zum anderen macht es die Trennung auch dem Entwickler leichter, sich schneller zurechtzufinden. Alle drei Module arbeiten z. B. mit einem Pizza Repository, das je nach Modul anders funktioniert. Somit hätten wir die Trennung der drei Aspekte zum Daten lesen, Daten schreiben und Daten aktualisieren ansonsten in einer tiefere Ebene verlagern müssen.

MongoDB-Modul

Zuerst möchten wir das Modul MongoDB etwas genauer betrachten, da es als Basis für das PizzaRead-Modul zum Lesen der Daten dient. Das Modul enthält eine Factory zum Erstellen des Adapters für MongoDB sowie eine abstrakte Collection-Klasse für die Repräsentation einer Collection aus der MongoDB. Diese Adapter-Factory ist in Listing 1 zu sehen. Sie holt die Konfigurationsdaten für die MongoDB, erstellt eine Instanz von MongoClient und wählt darin dann den Datenbanknamen pizzadb aus. Die Konfigurationsdaten sowie der Einsatz der Factory sind inklusive der Konfiguration für die MySQL-Datenbank in der Datei /config/autoload/global.php zu sehen (Listing 2).

Für das Lesen der Daten aus den Collections in der MongoDB dient die abstrakte Klasse MongoDB\Col­lection\AbstractCollection, die in Listing 3 zu sehen ist und das entsprechende CollectionInterface implementiert. Der Konstruktor erwartet eine Instanz des MongoDB-Adapters und legt damit dann mithilfe des Namens der Collection die Collection in der MongoDB fest, die über entsprechende Getter- und Setter-Methoden nutzbar ist. Die Methode fetchList() liest alle Daten der Collection, während fetchSingleById() nur einen Datensatz anhand der ID einliest. An dieser Stelle müssen Sie beachten, dass jeder Datensatz in der Collection über zwei ID-Felder verfügt. Das Feld id enthält die ID aus der MySQL-Datenbank und damit deren Primärschlüssel. Das Feld _id wiederum enthält die interne MongoId, die wir jeweils vor dem Zurückgeb...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang