Domain-Driven Design und Model View Controller (PHP)

In diesem Abschnitt beschäftigen wir uns mit Prinzipien und Methoden des Software Engineerings für Webanwendungen. Dazu blicken wir zunächst in Modellierungsmethoden wie das Domain-Driven Design und Entwurfsmuster wie Model View Controller, das bei webbasierter Software sehr häufig Anwendung findet. Diese eher theoretischen Ansätze werden in der Praxis oft mit Hilfe des Programmierparadigmas der objektorientierten Programmierung umgesetzt, das wir von der funktionalen Programmierung abzugrenzen versuchen.

Die STURM-Webanwendung soll beim Seitenabruf aktuelle Inhalte bzw. Daten aus der Persistenzschicht holen (in diesem Fall eine API statt einer klassischen Datenbank) und diese anzeigen können. Wir benötigen dafür zunächst die serverseitige Skriptsprache PHP und ein Framework mit einigen Grundfunktionen für Webanwendungen. Später ergänzen wir noch ein System aus Vorlagen bzw. Ansichten (Views), für die wir die Datenmodelle (Models) und Steuerungseinheiten (Controller) bereitstellen müssen.

Modellierung von Webanwendungen

Software versucht einen Ausschnitt der realen Welt mit ihren Zusammenhängen, Funktionsweisen und Bezügen abzubilden. Hierfür werden informatische Modelle erstellt. Diese Modelle versuchen möglichst genau eine spezifische Anwendungsdomäne zu beschreiben. Die Digital Humanities beschäftigen sich sehr häufig mit Anwendungsdomänen, die um kulturelle und/oder geisteswissenschaftliche Zusammenhänge und Forschungsdaten kreisen. Eng mit einer Anwendungsdomäne in Beziehung steht die jeweilige Fachsprache, mittels derer die Wissensobjekte der Domäne beschrieben werden. Man kann bis zu einem gewissen Grad davon sprechen, dass in den Digital Humanities zusammen mit der Anwendungsdomäne häufig auch eine Wissensdomäne modelliert wird.

Diese Grundlagen gelten prinzipiell für alle Arten der Softwareentwicklung, also natürlich auch für die Entwicklung von Webanwendungen im geistes- und kulturwissenschaftlichen Umfeld. Für die Modellierung von Software haben sich verschiedene Methodologien herausgebildet. Teilweise stehen diese in Bezug zu einer bestimmten Software-Entwicklungsphilosophie, womit häufig ein Vorgehensmodell zur Entwicklung von Software gemeint ist. Insgesamt lässt sich Softwareentwicklung in folgende, nicht immer trennscharfe Bereiche unterteilen:

  1. Übergreifende Vorgehensmodelle („Entwicklungphilosopie“ bzw. Entwicklungsprinzip),
  2. die damit in Beziehung stehenden Modellierungsmethoden,
  3. die Entwurfs- und Architekturmuster für eine Software, die mit den Modellen in Beziehung stehen,
  4. die bei der Entwicklung verwendeten Programmierparadigmen,
  5. die Entwicklungsmethoden, die im Entwicklungsprozess Anwendung finden,
  6. sowie die verwendeten Entwicklungswerkzeuge und Instrumente.

Die nachfolgenden Abschnitte stellen beispielhaft einige Modellierungsansätze, Entwurfsmuster, Programmierparadigmen, Entwicklungsmethoden und Werkzeuge vor, die im Umfeld der weborientierten Anwendungsentwicklung häufig anzutreffen sind.

Modellierungsmethoden

Die Modellierung der Anwendungsdomäne einer Software ist unabhängig von spezifischen Programmiersprachen, Tools oder Frameworks. Es gibt jedoch zahlreiche Software-Frameworks, mit denen sich eine solche Modellierung recht einfach in Programmcode überführen lässt. Beispiele für unterschiedliche Modellierungsansätze von Software sind:

Domain-Driven Design

Domain-Driven Design (DDD) ist eine konzeptionelle Herangehens- und Denkweise an die Modellierung von Software. Der Ansatz wurde 2003 von Eric Evans in seinem gleichnamigen Buch geprägt. Das Hauptaugenmerk bei DDD fällt auf die Einführung einer ubiquitären (allgemein verständlichen) Sprache, die auf allen Stufen der Softwareentwicklung, von der Konzeption bis auf die Ebene des Programmcodes, verwendet wird. Durch regelmäßige Iterationen nach dem DDD-Prinzip ist es möglich, die Software genauer an die sich kontinuierlich verändernde Projektrealität anzupassen. Die Codebasis bleibt dabei immer im Einklang mit der Modellierungsebene. Domain-Driven Design steht daher agilen Vorgehensmodellen nahe.

DDD legt besonders großen Wert auf die Fachlichkeit einer Anwendungsdomäne, die mittels der ubiquitären Sprache abgebildet werden soll. Daher steht die Kommunikation zwischen Softwareentwickler:innen und Domänenexpert:innen im Zentrum der Methode. Ein Domänenmodell basiert auf Objekten, die für die einzelnen Bestandteile der zu modellierenden Fachdomäne stehen und unterschiedliche Eigenschaften haben. Dadurch steht DDD dem objektorientierten Programmierparadigma nahe.

Ein Domänenmodell kann aus folgenden Bestandteilen bestehen:

Begriff Erklärung / Beispiel aus der STURM-Webanwendung
Entitäten (entities) Objekte mit eigener Identität: Briefe, Personen, Orte, Werke etc.
Wertobjekte (value objects) Objekte, die über ihre Eigenschaften definiert sind: Geokoordinaten, Datierungen
Assoziationen (associations) Beziehungen der Objekte untereinander: Personen und Briefe, Sende- und Empfangsvorgang
Aggregate (aggregates) Zusammenschlüsse von Entitäten, Wertobjekten und Assoziationen zu logischen Einheiten. Aggregate gewährleisen die Konsistenz und funktionale Integrität der Fachdomäne: Brief, Datierung, Sende- und Empfangsort gehören zum Aggregat „Briefe“
Serviceobjekte (services) Dienste für Objekte der Fachdomäne: Normdatenresolver für Personen und Orte
Ereignisse (events) Ereignisse, die eine Zustandsveränderung in der Fachdomäne bzw. bei ihren Objekten auslösen: ein Suchereigniss, ein Publikationsereignis für neu edierte Briefe
Fabriken (factories) In der objektorientierten Programmierung wird die Erstellung von Objekten gerne sogenannten „Fabriken“ übergeben. Dies sind spezielle Objekte, die anhand eines Entwurfsmusters Objekte erzeugen. Dadurch wird vermieden, dass die Herstellungslogik am konkreten Objekt selbst implementiert wird. Das Fabrikobjekt hingegen kann ausgetauscht werden: Ein Briefobjekt benötigt ein neues Personenobjekt. Das Briefobjekt stellt das Personenobjekt aber nicht selbst her (mittels „new“), sondern fordert es bei einer Fabrik an.
Repositorien (repositories) Repositorien abstrahieren das Speichern (Persistieren) und Laden (Rekonstituieren) von Domänenobjekten. Sie sind zwischen die Infrastrukturschicht (bspw. eine Datenbank) und die Schicht mit der Anwendungslogik geschaltet. Dadurch kann die Infrastrukturschicht ausgetauscht werden, ohne dass die Schicht mit der Anwendungslogik geändert werden muss.

Folgende Grafik zeigt einzelne Bestandteile von DDD in Beziehung zueinander:

Grafik: Elemene des Domain-Driven Design

Quelle: Moritz Graf, freies Bild

Praxis mit Domain-Driven Design

Den praktischen Nutzen des Domain-Driven Design können wir anhand der STURM-Edition erproben. Dazu überlegen wir zunächst, wie deren Anwendungsdomäne auf DDD-Basis aussehen könnte. Bei Bedarf nutzen wir das freie Online-Whiteboard Excalidraw.

Folgende Leitfragen können bei der Ausarbeitung helfen:

  • Welche Begrifflichkeiten sollten im Sinne der ubiquitären Sprache für die STURM-Webanwendung in Form eines Glossars angelegt werden?
  • Was sind die Entitäten, was die Wertobjekte in der STURM-Fachdomäne?
  • Welche Aggregate lassen sich für die Objekte der STURM-Fachdomäne bilden?
  • Welche weiteren Objekttypen gibt es (bspw. Serviceobjekte, Ereignisse etc.)?
  • Aus welcher/n Quelle/n (Persistenzschicht) werden die Daten bezogen?
  • Welches Datenformat liegt vor und welche Auswirkungen hat das Datenformat auf die Erstellung der Domänenobjekte?
  • Welche Repositories, Services und Factories lassen sich für die STURM-Anwendungsdomäne definieren?

Beispiellösung als Grafik

Entwurfsmuster

Wie in jedem Bereich der professionellen Softwareentwicklung spielen auch im Feld der Webanwendungsentwicklung Entwurfsmuster, also Lösungsmuster für wiederkehrende Problemstellungen eine große Rolle. Insbesondere aktuelle Web Frameworks verwenden (unabhängig von der eingesetzten Programmiersprache) häufig Entwurfsmuster. Eine Beschäftigung mit und eine Kenntnis von Software-Entwurfsmustern ist daher sowohl aus der Projektmanagement- als auch der Entwicklungsperspektive angeraten.

Dabei gilt jedoch: Der Einsatz von Entwurfsmustern sollte mit Bedacht geschehen. Entwurfsmuster sind keine Garantie für gute Software. Werden sie falsch eingesetzt, können sie ein Antimuster bilden und die Softwarequalität negativ beeinträchtigen.

Entwurfsmuster werden in vier Kategorien unterschieden: Erzeugungsmuster, Strukturmuster, Verhaltensmuster und objektrelationale Muster. Daneben gibt es Muster, die sich keiner der vorherigen vier Kategorien zuordnen lassen (bspw. Analysemuster, Architekturmuster oder Integrationsmuster).

Model View Controller

Das Model View Controller (MVC) Entwurfsmuster wird (meistens) zu den Software-Architekturmustern gerechnet und ist bereits seit Ende der 1970er Jahre in Gebrauch. Es gehört heute zu den besonders verbreiteten Architekturmustern. Nahezu alle modernen Web Framweworks implementieren in der einen oder anderen Ausprägung eine MVC-Architektur.

Unterschieden werden muss in einer klassischen MVC-Architektur zwischen dem Modell (model), das sämtliche Daten der Anwendung enthält bzw. verwaltet, der Präsentationsschicht (view), die für die Darstellung der benötigten Daten zuständig ist und diese vom Modell anfordert, sowie der Steuerungseinheit (controller), die zwischen Modell und Präsentation für die Verwaltung von Aktionen zuständig ist. Der Controller ist auch für die Interaktion mit den Usern zuständig.

An welcher Stelle innerhalb der MVC-Architektur die Anwendungslogik implementiert wird ist dabei nicht festgelegt. Generell wird aus Gründen einer besseren Erweiterbarkeit aber empfohlen, auf ein „slim controller"-Prinzip zu setzen, die Logik der Steuerungseinheit also auf die Verarbeitung von Nutzerinteraktionen und die Verwaltung der Präsentationsschicht zu konzentrieren, während die eigentliche Anwendungslogik im Modell implementiert wird.

Grafik: Interaktionsdiagramm der MVC Architektur

Quelle: RegisFrey, Public Domain

Bei serverseitigen Webanwendungen („thin client“) wertet der Controller meistens die über HTTP eintreffenden Nutzerinteraktionen und Datenpakete aus. Meist kommuniziert der Controller dazu mit dem Model, das in einer bestimmten Programmiersprache implementiert ist und die darzustellenden Daten aus einer Persistenzschicht (bspw. eine relationale Datenbank) lädt. Als View wird häufig eine mit Controller und Model verbundene Template-Engine verstanden, die auf Basis von HTML-Vorlagen die Webseiten erzeugt, die dann vom Controller an den Webserver und von diesem an den Client (also den Browser) zurückgeliefert werden.

Bei clientseitigen Webanwendungen („fat client“) kommt es je nach gewählter Implementierung dazu, dass ein oder mehrere Komponenten der MVC-Architektur im Client implementiert sind (also bspw. der vollständige View oder auch das Modell oder Teile des Modells). In solchen Architekturen werden häufig lediglich Daten über HTTP aus der serverseitigen Persistenzschicht nachgeladen, während die Anwendungslogik vollständig im Client abläuft. Die Webanwendungen und Apps großer Social Media-Plattformen sind häufig clientseitig implementiert, um dieselbe Serverstruktur für verschiedene Clients nutzen zu können. Aus diesem Grund wurde ursprünglich bei Facebook das zur Zeit häufig verwendete JavaScript-Framework React entwickelt.

Programmierparadigmen

Je nach Art und Design einer Programmiersprache können bei der Entwicklung unterschiedliche Prinzipien zugrunde gelegt werden. Diese können entweder durch die Programmiersprache selbst, die verwendeten Bibliotheken und Frameworks oder durch eine im Team verabredete Konvention vorgegeben sein. Man spricht auch von Programmierstilen oder Programmierparadigmen. Hierbei gibt es weder einen „richtigen“ noch einen „falschen“ Programmierstil. Es gibt lediglich sinnvolle oder weniger sinnvolle Lösungsansätze für eine Problemstellung. Was hierbei sinnvoll und weniger sinnvoll ist, entscheidet sich aus den Anforderungen der zu realisierenden Anwendung.

Eine grundsätzliche Unterscheidung besteht zwischen dem imperativen und dem deklarativen Programmierparadigma. Eine Faustformel zur Unterscheidung beider Ansätze lautet, dass beim imperativen Programmierparadigma das Wie einer Berechnung (der Handlungsablauf), beim deklarativen das Was einer Berechnung (das Ergebnis) im Vordergrund steht (während der Handlungsablauf vom ausführenden Computer festgelegt wird).

Neben dieser grundsätzlichen Unterscheidung existiert eine Vielzahl weiterer Programmierparadigmen, beispielsweise die prozedurale Programmierung (imperativ), die funktionale Programmierung (deklarativ) und die objektorientierte Programmierung. Sofern Programmiersprachen mehrere Stile bzw. Paradigmen unterstüzten, werden sie multiparadigmatisch genannt.

Objektorientierte Programmierung

Die objektorientierte Programmierung versucht durch die Verwendung von Objekten, die im Rahmen einer Anwendung miteinander interagieren, innerhalb der Software eine Annäherung an die Gegebenheiten der realen Welt zu erreichen. Objekte enthalten dabei sowohl Daten (Eigenschaften, properties) als auch Aktionen auf diesen Daten (Methoden, methods). Hierbei wird in der Konzeptionsphase ein Objektmodell für die Anwendung entworfen (bspw. mittels Domain Driven Design). Für jedes konkrete Objekt, das zur Laufzeit einer Anwendung erzeugt wird, existiert eine abstrakte Klasse (ein Bauplan). Klassen definieren (gleichartige) Objekte. Die Datenstruktur eines Objektes wird durch die Eigenschaften bestimmt, das Verhalten eines Objektes durch die Methoden.

Wichtig ist, dass die objektorientierte Programmierung eine Herangehensweise ist. Dies wird manchmal verwechselt mit der Tatsache, dass es objektorientierte Programmiersprachen gibt. Java und Python sind zum Beispiel objektorientierte Programmiersprachen. Man kann mit ihnen jedoch sowohl objektorientiert als auch funktional programmieren. PHP ist eine prozedurale Programmiersprache. Auch in PHP ist es möglich sowohl objektorientiert als auch funktional zu programmieren. Heutige PHP-Frameworks sind aber zum größten Teil objektorientiert programmiert.

Beispiel (PHP):

<?php

class MyClass
{
    protected $property = 'Ich bin eine Eigenschaft';

    public function setProperty($value)
    {
        $this->property = $value;
    }

    public function getProperty()
    {
        return $this->property;
    }
}

// Create two objects
$objectOne = new MyClass;
$objectTwo = new MyClass;

// Get the value of $property from both objects
echo $objectOne->getProperty() . "\n" . $objectTwo->getProperty();

// Set new values for both objects
$objectOne->setProperty('Eigenschaft von 1');
$objectTwo->setProperty('Eigenschaft von 2');

// Output both objects
echo "\n" . $objectOne->getProperty() . "\n" . $objectTwo->getProperty();

Funktionale Programmierung

Bei der funktionalen Programmierung bestehen Programme aus Funktionsdefinitionen, die für eine Eingabe eine Ausgabe liefern. Hierbei können Funktionen miteinander verknüpft sowie als Eingabeparameter und auch als Berechnungsergebnis (Ausgabe) von anderen Funktionen verwendet werden. Die Grundlagen der funktionalen Programmierung stammen aus der Mathematik. Grundprinzip ist, dass Eingabedaten mittels Funktionen auf Ausgabedaten abgebildet (gemappt) werden. Im Vergleich zur imperativen Programmierung, die aus aufeinander folgenden Rechenanweisungen besteht und daher Zustandsänderungen (Seiteneffekte) erzeugt, wird in der funktionalen Programmierung hierauf verzichtet. Imperative Programme arbeiten zur Berechnung von Werten häufig mit Iteration (Schleifen), funktionale Programme arbeiten mit rekursiven Funktionsaufrufen (Rekursion). Funktionale Programmierung gilt als besonders effizient und spielt bei Webanwendungen zum Beispiel im Kontext von Rechenoperationen auf verteilten Infrastrukturen eine Rolle. Insgesamt haben die meisten weborientierten Programmiersprachen inzwischen Möglichkeiten zur funktionalen Programmierung integriert und diese findet zunehmend ihren Weg in aktuelle Web Frameworks (siehe das Beispiel unten).

Auch die funktionale Programmierung stellt zunächst eine Herangehensweise dar. Darüber hinaus existieren funktionale Programmiersprachen (wie bspw. Haskell oder auch XQuery/XSLT), die nach streng funktionalen Prinzipien aufgebaut sind und einen funktionalen Programmierstil „erzwingen“. Dennoch ist es auch in prozeduralen (PHP), objektorientierten (Java) und multiparadigmatischen Sprachen (JavaScript) möglich, funktional zu programmieren.

Beispiel (PHP):

// Imperative/iterative
foreach ($stringArray as $key => $value) {
  $result[$key] = strtoupper($value);
}

// Functional
$result = array_map('strtoupper', $stringArray);

Anwendung einer anonymen Funktion („Lambda-Funktion“) innerhalb des Slim PHP-Frameworks:

$app = new \Slim\App;
$app->get('/home', function () {
    // show home page
});

Info

Bevor wir mit PHP fortfahren, können wir uns nach Bedarf zunächst ein Gespür für funktionale und objektorientierte Programmierung erarbeiten.

  1. Mit David Peters Cube Composer erhalten wir einen spielerischen Einstieg in die funktionalen Programmierung.

  2. Zum besseren Verständnis kann es hilfreich sein, sich eine Programmieraufgabe zu stellen. Dann kann man überlegen, wie eine funktionale und eine objektorientierte Lösung dafür aussehen könnte, und diese Lösungen anschließend gegeneinander abwägen. Eine Beispielaufgabe könnte der Abruf und Umgang mit einer XML-Datei sein, so wie wir ihn auch in unserer STURM-Webanwendung benötigen werden.

PHP und von uns genutzte Komponenten

Im Vergleich zu JavaScript wird PHP nicht im Browser der Nutzer:innen ausgeführt sondern auf dem Server. Auf diese Weise kann man die Anforderungen an die Geräte der Nutzer:innen gering halten, da rechenintensive Operationen auf den Server ausgelagert werden. Darüber hinaus bietet eine serverseitige Programmiersprache wie PHP mehr Möglichkeiten, mit Datenbanken zu interagieren.

PHP ist nach wie vor die am weitesten verbreitete Programmiersprache für die serverseitige Webanwendungsentwicklung. Mit Slim nutzen wir ein schlankes und für den Einstieg sehr gut geeignetes PHP Framework, um bestimmte Grundfunktionen, die sehr viele Webanwendungen benötigen, wie das Routing eines Request-Pfads zum richtigen Template nicht unnötig von Grund auf schreiben zu müssen. Da wir ein objektorientiertes Paradigma für die Realisierung wählen, gehört auch die Planung und Umsetzung der benötigten Klassenstruktur zu unseren Aufgaben.

PHP Composer

Für die Arbeit mit mehreren fertigen Komponenten, von denen die eigene Webanwendung abhängen wird, hat sich die Nutzung eines Paketmanagers etabliert, durch den an zentraler Stelle alle Abhängigkeiten zu Programmbibliotheken (Packages) verwaltet, installiert und nach Bedarf aktualisiert werden. Composer ist der wichtigste Paketmanager für PHP.

Hierfür wird im Wurzelordner einer PHP-Anwendung eine JSON-Datei mit dem Namen composer.json angelegt, in der die zu installierenden PHP-Pakete mit Namen und Versionsnummer angegeben sind. Zum Bezug von Paketen können unterschiedliche Paketquellen definiert werden. Neben dem Hauptrepositorium Packagist können auch Git-Repositorien (bspw. bei GitHub oder GitLab) einbezogen werden.

PHP-Pakete werden von Composer immer in einem projektspezifischen Unterordner mit dem Namen vendor installiert. Neben der Verwaltung von Paketen gehört das standardkonforme Autoloading von PHP-Dateien und Klassen zu den wichtigsten Funktionalitäten von Composer.

Praxis zum Kennenlernen von PHP und PHP Composer

Für unsere STURM-Webanwendung haben wir mit OCI-Containern (Docker) bereits die Technik der lokalen Entwicklungsumgebung ausprobiert. Nun möchten wir erste Erfahrungen in der serverseitigen Programmierung sammeln.

  1. Wir stellen sicher, dass der für uns vorbereitete Container sturm-app läuft. Er enthält einen Webserver mit Unterstützung für die Skriptsprache PHP.

  2. Im Verzeichnis htdocs/public benennen wir die Datei index.php vorläufig in index-original.php um. Dann erstellen wir eine neue Datei index.php mit folgendem Inhalt und schauen uns das Ergebnis anschließend im Webbrowser an:

<?php

echo 'Hello world!';
  1. Die Datei besteht nun aus der Anweisung zur Ausführung von PHP (<?php, der eigentlich schließende Hinweis ?> wird in reinen PHP-Dateien weggelassen) und der Anweisung zur Ausgabe eines Textstrings (echo). Als nächstes experimentieren wird mit weiteren Sprachkonstrukten von PHP. Bei Bedarf schauen wir in eines von vielen PHP Tutorials:
  • Text vor und nach der Anweisung zum Ausführen von PHP schreiben.
  • Per echo phpversion(); eine Info zur PHP-Umgebung erhalten.
  • Mehrere Variablen setzen und per . verbunden ausgeben.
  • Einen Kommentar in den Code einfügen.
  • Eine Rechenoperation mit zwei oder mehr Variablen durchführen.
  • Eine Bedingung mit if/elseif/else und booleschen Variable (true/false) ausführen.
  • Ein Array konstruieren und mit einer foreach-Schleife abarbeiten.
  • Dasselbe mit einem assoziativen Array (also mit Key und Value) ausprobieren.
  • Eine Minifunktion schreiben und ausführen.
  1. Als nächstes probieren wir die objektorientierte Programmierung aus. Dazu deklarieren wir eine Klasse und instanziieren dann ein neues Objekt dieser Klasse.

  2. Optional probieren wir noch, die Pakektverwaltung PHP Composer zu bedienen. Sie wurde beim Bau des Containers bereits ausgeführt, um alle Abhängigkeiten zu installieren, doch für ein besseres Verständnis schauen wir uns an, was dabei passiert: Zuerst versuchen wir die im Container enthaltene PHP Composer-Konfigurationsdatei zu verstehen. Danach führen wir mit folgendem Befehl ein Update aus:

docker exec -it sturm-app composer update -o
Weitere Informationen

Slim PHP Framework

Um Grundfunktionen unserer Webanwendung nicht selbst schreiben zu müssen, setzen wir für die Realisierung der STURM-Webanwendung auf das Slim PHP Framework in Version 4. Slim ist ein Microframework für PHP, das sich auf die Erstellung webbasierter APIs, das URL-Routing sowie die standardkonforme Verarbeitung von HTTP Requests und Responses spezialisiert hat. Da die XML-Daten für die STURM-Briefedition lediglich aus einer externen XML-Datenbank über HTTP bezogen werden, eignet sich das leichtgewichtige Slim sehr gut zur Verabrbeitung von Requests und Responses.

Bei der Wahl eines Frameworks sollte man danach schauen, dass die installierten Komponenten noch lange nutzbar bleiben und von einer größeren Entwickler:innengemeinde genutzt werden. Slim ist ein modernes Framework, das aktiv entwickelt wird und gemäß der PHP Standard Recommendations aufgebaut ist. Einer der Hauptentwickler, Josh Lockhart, ist auch einer der führenden Köpfe hinter PHP - The Right Way und Autor des Buches Modern PHP.

Twig Template Engine

Um dynamisch Inhalte aus der API der richtigen STURM-Webseite anzeigen zu können, müssen wir außerdem modular zusammensetzbare Vorlagen-Fragmente bauen. Wir nutzen dafür Twig als schlanke und leistungsstarke „Template Engine“ für PHP. Template Engines ermöglichen es uns, verschiedene Ansichten der Webanwendung (wie Startseite, Personenliste oder Einzelansicht eines Briefes) zu definieren, in die von uns festgelegte Elemente an ebenfalls von uns festgelegte Stellen geschrieben werden. In der STURM-App sollen beispielsweise alle Ansichten auf einer gemeinsamen Basisvorlage aufbauen, die den Seitenkopf und den Seitenfuß enthält. Muss zu einem späteren Zeitpunkt eine Angabe im Seitenfuß geändert werden, muss man dies nur in einer einzigen Datei erledigen.

Twig wird in vielen PHP-Frameworks (z.B. auch Symfony) und Content-Management-Systemen (bspw. Drupal) eingesetzt und auch das Slim-Framework verfügt über eine out-of-the-box Unterstützung. Die Twig-Templates werden im MVC-Muster unserer STURM-Webanwendung die Rolle der Views übernehmen.

Praxis mit Slim

Slim und Twig wurden als Komponenten unserer Anwendung bereits mit installiert. Nun können wir versuchen, mit Slim eine serverseitige Anwendung zu schreiben. Dabei fokussieren wir uns zunächst auf unsere Testdatei.

  1. Wir schauen in die Slim-Dokumentation und versuchen, die dort angeführte Beispielanwendung in unserer index.php umzusetzen. Dabei ersetzen wir die Zeile zur autoload.php mit der für unsere Verzeichnisstruktur passenden Zeile require __DIR__ . '/../../vendor/autoload.php';

  2. Um den Code besser zu verstehen, ergänzen wir in der Adresszeile unseres Browsers, der die Testdatei zeigt, den URL-Schnipsel /hello/Jonatan und drücken Enter. Wie funktioniert diese Funktion im Beispielcode?

  3. Die zentrale Funktion von Slim ist das Empfangen und Weiterverarbeiten eines Requests. Dabei setzt Slim auf sogenannte Routen. Nun ergänzen wir neben /hello/{name} eine weitere Route und probieren sie aus, z.B. /letter/{identifier} mit einer entsprechenden Antwortfunktion.

  4. Zuletzt entfernen wir unsere Datei index.php wieder und benennen die vorige index-original.php wieder in index.php um. Was macht diese Datei anders als unsere Tests und warum? Zum besseren Verständnis können wir in die Slim-Doku zum Routing schauen.

Einzelteile der serverseitigen Anwendung

Basierend auf den oben genannten Komponenten kann man die serverseitige Logik einer Webanwendung bauen. Neben den Views, die wir in einem späteren Kapitel mit Leben füllen werden, bietet uns eine Standard-Ordnerstruktur einen guten Einblick in deren Einzelteile:

  • Controller: Annahme eines spezifischen Requests, Anfordern des Domänenobjekts und Weitergabe an einen View
  • Domain…
    • Model: Baupläne einzelner Domänenobjekte
    • Repository: Finden der richtigen Daten in der Persistenzschicht
  • Factory: Herstellen der einzelnen Domänenobjekte nach Bauplänen
  • Mapper: Vermittler zwischen verschiedenen Datentypen
  • Processor: Verarbeitungsklassen, z.B. um Werte in einer bestimmten Form zu produzieren
  • Service: Serviceklassen, z.B. zum Verarbeiten bestimmter Datenformate wie XML

Visualieren lässt sich dieser Aufbau wie folgt:

Grafik: Überblick über die Einzelteile der STURM-Webanwendung

Quelle: Torsten Schrade

Innerhalb dieser Struktur werden alle Klassen mit ihren Properties und Methods angelegt. Eine Klasse wird üblicherweise nach dem Prinzip Klassentyp/KlassenName.php abgelegt, in unserem Fall im Ordner htdocs/app. Damit das Autoloading unserer eigenen Klassen richtig funktioniert, muss dieser Pfad nach den extern installieren Komponenten (require) in der composer.json eingetragen werden. Wir verwenden für alle Klassen, die mit der STURM-Webanwendung in Beziehung stehen den PHP Namespace \Sturm\App:

{
  "require": {...},
  "autoload": {
    "psr-4": {
      "Sturm\\App\\": "htdocs/app/"
    }
  }
}

Info

Schauen Sie für den Rest dieses Kapitels gerne in den Code der Beispielimplementierung der STURM-Webanwendung, aus der auch der oben aufgelistete Ordnerbaum stammt.

Controller

Bei Webanwendungen nach MVC-Schema gibt es meist eine zentrale Stelle, die Requests annimmt und dann an die richtigen Controller weiterleitet. Dies übernimmt (mit etwas Konfiguration) Slim für uns.

Es gibt einen Controller pro anforderbarem Domänenobjekt. Der Controller soll dafür sorgen, dass mindestens zwei Aktionen möglich sind: Die Domänenobjekte aufzulisten (list) und sie einzeln anzuzeigen (show).

Domain Models und Repositories

Die nach den Prinzipien des Domain-Driven Design zentralen Klassen sind die Domain Models. Sie bilden den Bauplan für einzelne Domänenobjekte, also beispielsweise einen einzelnen Brief. Wenn die Repositories und Factories die eigentliche Geschäftslogik enthalten, können sich Domain Models darauf beschränken, definierte Eigenschaften und die dazugehörigen Getter und Setter bereitzustellen.

Die Repositories übernehmen die Kommunikation mit der Persistenzschicht (Daten holen, Daten transformieren) unter Zuhilfenahme bestimmter Serviceklassen. Die Repositories stellen auch das Bindeglied zu den im nächsten Schritt ausgeführten Factories dar. Die Factories wiederum werden anhand des Bauplanes unserer Domänenobjekte die eigentlichen Objektinstanzen erzeugen. Die fertigen Objekte einschließlich ihrer Daten werden vom zuständigen Repository an den Controller zurückgeliefert, der die Weitergabe an den View und die Template Engine organisiert.

Praxis mit Domain Models und Controllern

Beim Blick in die index.php der Beispielimplementierung der STURM-Webanwendung haben wir gesehen, dass fünf verschiedene Controller angesprochen werden:

  • PageController (für die Standardseiten wie bspw. die Startseite, die Projektbeschreibung etc.)
  • LetterController (für die Briefe)
  • PersonController (für das Personenregister)
  • PlacesController (für das Ortsregister)
  • WorkController (für das Werkregister)

Basierend darauf experimentieren wir nun mit dem Rest der Beispielimplementierung.

  1. Zunächst üben wir unsere PHP-Kentnisse indem wir testweise eines der Domänenmodelle programmieren, z.B. für die Entity „Person“. Wir beginnen dafür mit wenigen Properties, die wir gerne hätten, unabhängig von den Informationen, die die STURM-API tatsächlich liefert.

  2. Danach schauen wir uns das richtige Domain Model der Beispielimplementierung an. Welche Unterschiede finden wir?

  3. Im letzten Schritt blicken wir in den zugehörigen Controller. Welche Funktionen werden hier aufgerufen? Optional malen wir uns ein Schema auf, welche weiteren Teile der serverseitigen Anwendung angesprochen werden und warum.

Factories, Mapper und Processors

Ein Großteil der Logik zum Finden und Holen von Daten inklsive der Baupläne für Domänenobjekte ist damit bereits realisiert. Um Controller, Models und Repositories herum benötigen wir jedoch noch einige kleine Tools, für die sich im Feld der Webanwendungen nach MVC-Muster ebenfalls Standards entwickelt haben, damit sie nicht als unübersichtliche, funktional programmierte Dateien im Code verstreut werden, wodurch andere Entwickler:innen den Code nur noch schwer verstehen würden.

Bisher noch nicht erledigt ist ein Mapping der XML-Daten auf die konkreten Objekte zur Laufzeit. Gemäß den Prinzipien des Domain-Driven Design sollte die Erzeugung von Objekten nicht an unterschiedlichen Stellen im Code mittels new erfolgen, sondern idealerweise von Factory-Objekten übernommen werden. Die Repositories verknüpfen also Controller und Factory. Sie holen im Auftrag des jeweiligen Controllers die Daten und fordern mit den erhaltenen Daten bei den Factories die Objekte an. Die Factories verwenden die innerhalb der Domänenobjekte definierten Baupläne zur Herstellung der konkreten Objektinstanzen.

Zur Implementierung der Factories müssen wir zwischen zwei Typen unterscheiden: das eigentliche Factory-Objekt, das unter Angabe der zu bauenden Klasse konkrete Objektinstanzen erstellt, sowie ein weiteres Factory-Objekt, dass einen Storage von Objekten bereitstellen kann. Beides benötigen wir an unterschiedlichen Stellen der Applikation: wenn wir einzelne Objektinstanzen benötigen (bspw. in der Einzelansicht eines Briefes oder einer Seite) und wenn wir Mengen von Objekten benötigen (bspw. in den Listenansichten).

Da wir aus der Persistenzschicht XML-Daten erhalten, stellt sich bei der Umsetzung die Frage, wie die Werte aus den Daten den definierten Eigenschaften der Objekte zugeordnet werden können. An dieser Stelle werden wir einen Mapper einsetzen, der diese Aufgabe übernimmt. Bei der Verwendung relationaler Datenbanken kommen häufig objektrelationale Mapper wie bspw. Doctrine ORM zum Einsatz. In unserem Fall werden wir einen eigenen Mapper definieren, der für die Abbildung der XML-Daten auf die Objekte zuständig ist.

Es bleibt die Frage, wie wir die konkrete Zuordnung von Werten innerhalb des Mappers vornehmen. Inspiriert von der Vorgehensweise gängiger Mapper werden wir dies über Annotationen an den Eigenschaften unserer Domänenobjekte lösen. Grundidee ist, dass zu jeder definierten Eigenschaft eines Domänenobjektes mittels der Annotation @xpath ein XPath angegeben werden kann, der die zugehörigen Werte in den XML-Daten ausliest. Um die XML-Daten noch besser während des Mappings verarbeiten zu können, werden wir zudem noch zwei weitere Annotationen einführen: @transform zur Transformation von gemappten XML-Fragmenten mittels XSLT sowie @process.

Services

Zuletzt benötigen wir noch die Funktionalität zum Abruf und Einlesen der XML-Daten aus der „echten“ STURM-Anwendung. Diese wird in einer Serviceklasse untergebracht. Bei der @process-Annotation im Mapper implementieren wir dazu schon einmal die Möglichkeit, beliebige PHP Verarbeitungsklassen anzudocken, mit denen eine Prozessierung der Daten vorgenommen werden kann. Der DataMapper wird die Annotationen an den Eigenschaften der Domänenobjekte mit Hilfe von Reflexion auflösen. Bevor wir den XML-Service angehen können, brauchen wir jedoch mehr Informationen zur Funktion von Schnittstellen.