Objektorientierte Implementierung eines Gästebuchs

Objektorientierte Implementierung eines Gästebuchs

Aufbauend auf dem Artikel Objektorientiertes Design eines Gästebuchs soll dieser Teil zeigen, wie die Implementierung des Moduls mit dem Adventure PHP Framework (APF) umgesetzt werden kann. Es wird dabei nicht nur auf die Möglichkeiten des Frameworks eingegangen, sondern auch das im ersten Teil erarbeitete, Technologie-unabhängige Design erweitert.

1. Einleitung

Im ersten Teil dieser Serie wurden die Anforderungen an das Gästebuch aufgezeigt, analysiert und die Planung der Umsetzung mit Hilfe von Use Case- und Klassen-Diagramme beschrieben. Bei den UML-Diagrammen wurde auf eine Technologie-unabhängige Darstellung geachtet um die Möglichkeit zu geben, das beschriebene Design in unterschiedlichen Programmier-Sprachen und mit beliebigen Werkzeugen umzusetzen. Dies ist in vielen Fällen – etwa für ein Grob-Konzept oder zur genaueren Abschätzung der Aufwende eines Projekts – auch nützlich und ausreichend, für die konkrete Umsetzung einer Anwendung, muss dieses jedoch noch verfeinert werden.

In diesem Teil soll daher nicht nur das Design konkretisiert werden, sondern es soll vor allem auf die praktische Umsetzung eingegangen werden. Als Hilfsmittel zur Umsetzung soll das Adventure PHP Framework (kurz: APF) zum Einsatz kommen. Dieses verfügt mit dem Page-Controller über eine Möglichkeit, beliebig komplexe GUI-Strukturen ohne Einschränkung des Komplexitäts-Grades aufzubauen und Views bzw. Module einfach per XML-Tags einzuklinken. Durch den Einsatz von aus dem JAVA-Bereich bekannten Taglibs ist es zudem auf sehr einfache Weise möglich, widerverwendbare GUI-Elemente (Widgets) zu schaffen.

Der fertige Quellcode ist sowohl im SVN-Repository des APF als auch im aktuellen Release 1.11 enthalten. Es wird empfohlen, diesen auf der Downloads-Seite herunter zu laden und begleitend zum Artikel zu lesen.

2. Einführung in das APF

Das Adventure PHP Framework versteht sich als Hilfsmittel zur Implementierung von objektorientierten, generischen und widerverwendbaren PHP-Web-Applikationen. Der Entwickler wird dabei unterstützt Anwendungen nach anerkannten OO-Design-Pattern zu implementieren und das Framework bietet bereits eine Vielzahl von Lösungen für bekannte Problemstellungen.

Es versteht sich bewusst nicht als Applikation, die lediglich konfiguriert werden muss, sondern als technische Basis und Design-Guide für den Entwurf von Anwendungen.

Das Herzstück des Applikations-Frameworks bildet der Page-Controller. Seine Funktion besteht darin, Anfragen entgegen zu nehmen und einen allgemeingültigen Rahmen für Applikationen zu bieten, die nach dem HMVC-Muster erstellt werden. Er verfügt daher über Mechanismen zum Laden und Verarbeiten von Templates mit Hilfe der Funktionen von Taglibs, zur Ausführung von (MVC-)Controllern und zum Handling des HMVC-DOM-Baumes, der mit Hilfe der Taglibs erzeugt wird.

Da der Page-Controller selbst einen Funktions-Rahmen bereitstellt, kann die Applikations-Logik durch eigene Taglibs und (MVC-)Controller direkt vom Entwickler beeinflusst werden. Die Komponente selbst handelt dabei unabhängig von der in Taglibs definierter Logik. Auf diese Weise ist es möglich, Tags mit sehr unterschiedlichen Funktionen wie beispielsweise Platzhalter oder Formulare zu erstellen.

In den folgenden Abschnitten soll dieser Mechanismus dazu genutzt werden, das Template-Layout des Gästebuchs so granular zu gestalten, dass die einzelnen Bereiche klein und übersichtlich bleiben. So sind Erweiterungen später einfacher zu bewerkstelligen.

3. Und ... Action!

Für den Einstieg in das APF lohnt es sich, einen Blick auf das Hallo Welt!-Tutorial unter Hallo Welt! zu werfen. Dieses beschreibt, wie eine einfache Webseite erstellt werden kann. Um die Entwicklung des Gästebuches einfacher zugestalten, legen wir zunächst ein solches Projekt an. Hierzu beziehen wir die Bibliotheken (apf-codepack-1.11*) des Frameworks und erstellen einen neuen Ordner oder VHOST in unserem lokalen Webserver. Um im Weiteren eine einheitliche Sprachregelung hinsichtlich der Ordner-Struktur zu haben, soll das Projekt unter

Code
/var/www/html/guestbook

angelegt werden. Dort erstellen wir nun einen Ordner apps und entpacken darin die Bibliotheken.

Parallel zu apps legen wir zusätzlich eine index.php an. Diese dient als so genannte Bootstrap-Datei, da das APF dafür ausgelegt ist, eine komplette Webseite mit einer einzigen PHP-Datei auszuliefern. Für ein erstes Erfolgserlebnis erstellen wir ein Template, das später zur Einbindung des Gästebuchs dient.

Für das Template zur Ausgabe des Gästebuchs legen wir dann den Ordner

Code
/var/www/html/guestbook/EXAMPLE/gb/templates

an und erstellen darin eine Datei mit dem Namen

Code
guestbook.html

Diese füllen wir zunächst mit einem beliebigen Text und widmen uns der Bootstrap-Datei. Um das Anlegen einfach zu gestalten, kopieren wir den Code aus dem oben genannten Hallo-Welt!-Tutorial und passen die Parameter entsprechend an:

PHP-Code
include('./APF/core/bootstrap.php'); use APF\core\singleton\Singleton:; use APF\core\frontcontroller\Frontcontroller; RootClassLoader::addLoader(new StandardClassLoader('EXAMPLE', '/var/www/html/guestbook')); $fC = Singleton::getInstance(Frontcontroller::class); echo $fC->start('EXAMPLE\gb\templates', 'guestbook');

Das Code-Snippet zeigt bereits wichtige Konventionen des APF: Klassen und Template-Dateien werden immer relativ zum Installations-Ordner der Basis-Bibliotheken adressiert. Verzeichnisse (Namespaces) werden mit ”\“ statt ”/“ getrennt und Datei-Namen ohne Endung notiert.Template-Dateien tragen die Endung ”.html“, PHP-Klassen ”.php“.

Rufen wir nun die Datei index.php im Browser auf, sehen wir den im Template abgespeicherten Text. Damit ist unser erste APF-Webseite fertig!

4. Domain-Model

Das im ersten Teil beschriebe Domänen-Modell ist für alle Schichten der Software relevant und soll daher zuerst implementiert werden.

Da die Instanz eines Gästebuchs zu einem Zeitpunkt in exakt einer Sprache angezeigt wird, haben wir uns beim Design dazu entschieden ein einfaches Objektmodell ohne Sprach-Differenzierung zu erstellen. Hierzu zählen die Klassen Guestbook, User und Entry. Diese sind einfache Daten-Objekte, wodurch sich die Implementierung relativ einfach gestaltet. Die Klassen besitzen für jede (private) Eigenschaft jeweils eine get()- und eine set()-Methode. Um einen Eintrag (Entry) vollständig anzeigen zu können ist der zugehörige Benutzer (User) notwendig. Aus diesem Grund besitzt die Klasse Entry eine komplexe Eigenschaft editor, die den Autor des Eintrags beinhaltet.

PHP-Code
namespace APF\modules\guestbook2009\biz; final class Entry { private $title; private $text; private $creationTimestamp; private $modificationTimestamp; private $user; private $id; public function getEditor(){ return $this->user; } public function setEditor($user){ $this->user = $user; } ... }

Die Klasse Guestbook kennt zudem alle Einträge des Gästebuchs, erhält deshalb eine komplexe Eigenschaft entries:

PHP-Code
namespace APF\modules\guestbook2009\biz; final class Guestbook { private $title; private $description; private $entries = array(); public function getEntries(){ return $this->entries; } public function setEntries($entries){ $this->entries = $entries; } public function addEntry($entry){ $this->entries[] = $entry; } ... }

Die Klasse User repräsentiert sowohl den Autor eines Eintrags als auch den Administrator eines Gästebuchs. Diese hat folgende Definition:

PHP-Code
final class User { private $username; private $password; private $name; private $email; private $website; private $id; public function setName($name){ $this->name = $name; } public function getName(){ return $this->name; } ... }

In den folgenden Abschnitten werden die Klassen als Schnittstellen-Objekte für den Datenaustausch zwischen Präsentations- und Business-Schicht sowie Business- und Datenschicht verwendet werden. Dadurch wird der Code lesbarer und die API ist typisiert.

Um die einzelnen Komponenten unserer Software einfacher wieder zu finden, legen wir eine Ordner-Struktur gemäß der Schichten der Applikation im apps-Ordner an. Da das Gästebuch ein widerverwendbares Modul sein soll, wird dieses im Ordner modules abgelegt und mit einem eindeutigen Kennzeichner versehen. Die Ordner-Struktur von apps gestaltet sich dann wie folgt:

Code
/APF/modules/guestbook2009/ pres/ biz/ data/

Der Ordner pres dient der Ablage der Bestandteile der Präsentations-Schicht, unter biz werden alle Inhalte der Business-Schicht der Applikation abgelegt und data beinhaltet die Datenschicht-Komponenten.

Die Klassen Guestbook, User und Entry werden, da sie zur Business-Schicht zählen, im Ordner biz unter den Namen Guestbook.php, User.php und Entry.php abgelegt. Das APF sieht dabei ebenfalls die Namenskonvention Klasse=Datei-Name vor. Später kann dann die Klasse User daher via

PHP-Code
use APF\modules\guestbook2009\biz\User;

eingebunden werden.

5. Use Cases und Views

Das Kapitel Use Cases des ersten Teils beschreibt die Interaktionen eines Benutzers mir dem Gästebuch. Werden diese möglichst realitätsnah im UML-Diagramm abgebildet, so lassen sich daraus üblicherweise direkt die Views der Applikation ableiten. Als View wird ein Bereich einer Applikation oder einer Webseite bezeichnet, dem eine definierte Aufgabe zugeschrieben werden kann. Dabei ist es sehr wohl möglich, einen View durch mehrere weitere Views zu beschreiben.

Gästebuch Use Cases

Aus dem Diagramm, das im ersten Teil das Ergebnis des Designs war, ergeben sich folgende Views für den Besucher und den Webmaster einer Webseite:

  • Anzeige der Einträge des Gästebuchs
  • Anzeige eines einzelnen Eintrags
  • Anzeige eines Pagers
  • Anzeige eines Formulars
  • Anzeige einer Login-Maske
  • Anzeige eines Auswahlmenüs

Es bietet sich daher an, diese logischen Views auch in View-Templates oder Template-Fragmente abzubilden. Das erleichtert nicht nur die Implementierung sondern garantiert auch, dass der Code im Falle einer Erweiterung einfacher lesbar ist.

6. Gestaltung der Präsentationsschicht

Für den Entwurf und die Implementierung einer Anwendung gibt es zwei verbreitete Ansätze: top-down- oder bottom-up-Methode. Die erste der beiden beschreibt die Idee, zunächst die Oberfläche einer Anwendung zu erstellen und dann sukzessive die Funktionen zu integrieren. Der bottom-up-Ansatz bedeutet, die Basis-Funktion vor der GUI bereitzustellen. Daneben existieren noch einige weitere Vorgehensweisen, die die genannten jedoch lediglich erweitern bzw. granulieren.

In diesem Workshop entscheiden wir uns für die erste Variante, da in Web-Anwendungen die grafische Präsentation der Anwendung im Vordergrund steht.

Zudem ist der Vorgehensweise dieser Vorgehensweise nicht zu vernachlässigen, dass für den Kunden schnell ein präsentierbares Resultat entsteht. Dieses kann dann beispielsweise als Prototyp für das look&feel der Anwendung oder als Gesprächsgrundlage für die Ausgestaltung der Funktionen dienen.

In der folgenden Skizze ist der Aufbau der Präsentations-Schicht des Gästebuch-Moduls schemenhaft dargestellt:

Perspektiven-Design des APF-Gästebuchs

Der grün umrandete Bereich beschreibt den View, der das komplette Gästebuch darstellt. Dieser beinhaltet zwei weitere Views (rot): den Header mit dem Titel und der Beschreibung des Gästebuchs und den Inhalt. Im Falle der Anzeige der Gästebuch-Einträge gliedert sich dieser wiederum in drei Bereiche (blau): dem Header, dem Pager und der Liste der Einträge.

Bei der technischen Umsetzung können wir uns die Funktion des Page-Controllers zu Nutze machen. Dieser strukturiert alle Elemente als (MVC-)View-Elemente innerhalb eines Baumes. Damit repräsentieren die genannten, fachlichen Views später auch die technischen Views. Hierauf wird im Abschnitt Einbindung des Gästebuchs näher eingegangen.

7. Formatierung per CSS

Um die Formatierung des Gästebuchs und damit den Einsatz auf einer bestehenden Seite zu erleichtern, soll beim HTML-Markup Wert auf ein konsequentes Container-Layout mit Hilfe von div-Elementen gelegt werden. Um Überschneidungen der Klassen-Benennung mit bestehenden Komponenten zu vermeiden, werden alle CSS-Klassen mit dem Präfix gb-- ausgestattet. Weiterhin wird pro Formatierungs-Bereich eine weitere ”Unter-Klasse“ eingeführt. Das folgende HTML-Fragment zeigt die Idee:

Html-Code
<div class="gb--main"> <div class="gb--main-heading"> ... </div> <div class="gb--main-text"> ... </div> <div class="gb--list-entries"> ... </div> </div>

Da das CSS-Design nicht im Fokus dieses Artikels steht, wird dieses Thema im Folgenden ausgespart. Ein fertiges CSS-Stylesheet kann jedoch aus dem SVN bzw. dem aktuellen APF-Release 1.10 bezogen werden.

8. Generische Einbindung von Modulen

In den Anforderungen wurde definiert, dass die Einbindung des Gästebuchs als eigenständiges Modul möglich sein soll. Nun stellt sich die Frage: ”Was ist ein Modul und wie muss die Schnittstelle zur Einbindung aussehen?“.

Unter einem Modul versteht der Autor ein abgeschlossenes Stück Software, das – mit einigen Umgebungs-Informationen versorgt – für sich alleine lauffähig ist. Bei der Gestaltung von mehrfach einsetzbaren Modulen ist es zudem wichtig, dass diese keine direkte Abhängigkeit zu bestehenden Funktionalitäten einer Webseite oder Applikation besitzen. Andernfalls ist es nicht möglich, diese ohne Änderungen des Quellcodes in andere Projekte einzubinden. Hier sieht sich der Anwender dann schnell damit konfrontiert, Abhängigkeiten zu URL-Parametern der umgebenden Software manuell auflösen zu müssen.

Wie angedeutet, benötigt ein Modul Informationen aus der Umgebung, in der es eingebunden ist. Zu diesen zählen:

  • die Sprache einer Webseite bzw. Applikation,
  • die Applikation, in der das Modul eingebettet ist und
  • die Umgebung auf der die Software läuft.

Stehen diese drei Rahmenbedingungen zur Verfügung ist es problemlos möglich ein abgegrenztes und widerverwendbares Modul zu erstellen.

Zur Einbindung eines Moduls – oder allgemeiner – eines neuen GUI-Elements, stellt das APF einen generischen Mechanismus zur Verfügung: den Page-Controller. Dieser injiziert die genannten Informationen in jeden neu erstellten DOM-Knoten.

Dieses Verfahren soll auch im Fall des Gästebuchs genutzt werden um das separat entwickelte Modul später in beliebige Projekte einbinden zu können. Die oben benannten Parameter werden im APF etwas anderes bezeichnet. Dies ist im Folgenden kurz beschrieben.

Die Sprache steht in jedem APF-DOM-Knoten in der Klassen-Variable $this->language zur Verfügung. Dies gilt insbesondere auch für Document-Controller. Sie wird bei der Initialisierung des Page- oder Front-Controllers gesetzt und gilt für den kompletten Baum. Sofern notwendig, kann die Sprache in einem Knoten auch manuell verändert werden. Dies kann notwendig sein, wenn in einer mehrsprachigen Anwendung ein Modul nur in englischer Sprache eingebunden werden soll.

Der Context kennzeichnet die Applikation, in der ein Modul oder eine Funktion eingebunden ist. Diese steht in jedem Knoten in der Klassen-Variable $this->context zur Verfügung. Für die Initialisierung und die Veränderbarkeit gelten dieselben Regeln wie bei der Sprache.

Die Umgebung, auf der die Anwendung ausgeführt wird, ist in der zentralen Registry-Instanz abgelegt. Hierzu steht im Namespace apf::core die Direktive Environment zur Verfügung. Dieser Wert wird nicht, wie die zuvor genannten, direkt im DOM-Knoten vorgehalten. Grund dafür ist, dass es sich hierbei um eine für alle Knoten identische und nicht veränderliche Information handelt. Sie beschreibt eine physikalische Maschine oder ein Cluster, auf dem die Applikation gehostet wird. Sofern notwendig, kann die Umgebung via

PHP-Code
Registry::retrieve('APF\core', 'Environment');

von der Registry bezogen werden.

9. Einbindung des Gästebuchs

Zur Unterstützung des Entwicklers enthält das APF den <core:importdesign />-Tag. Dieser ermöglicht es, ein Modul in eine bestehende Anwendung basierend auf der im vorangegangenen Abschnitt beschriebenen Schnittstelle einzubinden.

Um die Integration des Gästebuchs für den Anwender noch komfortabler zu gestalten und innerhalb der Applikation eine einfache Möglichkeit zu schaffen auf die Id des anzuzeigenden Gästebuchs zuzugreifen, soll die Funktion des Tags erweitert und die Gästebuch-Id in einem Model abgelegt werden.

Hierzu erstellen wir zunächst eine Klasse, die die Gästebuch-Id verwaltet:

PHP-Code
class GuestbookModel extends APFObject { protected $guestbookId; }

Da ein Model im Sinne der eingesetzten 3-Schicht-Architektur zur Business-Schicht zählt, legen wird diese Klasse unter dem Namespace (=Ordner)

Code
/APF/modules/guestbook2009/biz

ab. Durch die Vererbung besitzt das Objekt bereits eine get()- und set()-Methode zur Manipulation der GuestbookId.

Anschließend erweitern wir die Funktion der oben genannten Taglib um das Füllen des Models:

PHP-Code
class GuestbookImportTemplateTag extends ImportTemplateTag { public function onParseTime() { $model = & $this->getServiceObject('APF\modules\guestbook2009\biz\GuestbookModel'); $guestbookId = $this->getAttribute('gbid'); // do not include the guestbook, if gbid is not set/existent if ($guestbookId == null || ((int) $guestbookId) == 0) { throw new InvalidArgumentException('[GuestbookImportTemplateTag::onParseTime()] The attribute ' . '"gbid" is empty or not present or the value is not an id. Please specify the ' . 'attribute correctly in order to include the guestbook module!'); } $model->setGuestbookId($guestbookId); $this->setAttribute('namespace', 'APF\modules\guestbook2009\pres\templates'); $this->setAttribute('template', 'guestbook'); parent::onParseTime(); } }

Wie das Code-Listing zeigt, wird das Model mit Hilfe der Methode getServiceObject() erzeugt. Diese ist ein Proxy auf die Klasse ServiceManager, die im APF dafür verantwortlich ist, Objekte zu erzeugen und gleichzeitig zu initialisieren. In der oben verwendeten Form liefert die Methode ein Singleton-Objekt zurück. Greifen die übrigen Komponenten auf dieselbe Weise auf das Objekt zu, steht die Informationen allen gleichermaßen zur Verfügung.

Verglichen mit der ursprünglichen Implementierung der Taglib, werden die für die Einbindung des Gästebuchs notwendigen Parameter bereits in der erweiterten Taglib gesetzt werden. Dadurch kann das Gästebuch wie folgt im Template guestbook.html eingebunden werden:

APF-Template
<core:addtaglib class="APF\modules\guestbook2009\pres\taglib\GuestbookImportTemplateTag" prefix="gb" name="import" /> <gb:import gbid=“1“ />

Details zur Implementierung von Taglibs können unter Implementierung von Tags nachgelesen werden.

10. Der Basis-View

Rufen wir nun die URL in unserem Browser auf, erscheint eine Fehlermeldung, die darauf hinweist, dass das Template guestbook(.html) aus dem Namespace APF\modules\guestbook2009\pres\templates nicht geladen werden kann. Hintergrund ist, dass im letzten Abschnitt der Ablage-Ort und der Name des Basis-Templates bereits implizit definiert wurden. Innerhalb des pres-Ordners wurde zur besseren Gliederung folgende Struktur aufgebaut:

Code
/APF/guestbook2009/pres/ controller/ taglib/ templates/

Der Ordner controller dient später zur Ablage der MVC-Document-Controller-Klassen, im Ordner taglib wurde die oben besprochene Klasse GuestbookImportTemplateTag abgelegt und templates ist der Ordner, in dem alle Templates gespeichert werden. Diese Konvention hat sich in der Vergangenheit als sehr angenehm erwiesen.

Wie im Abschnitt Gestaltung der Präsentationsschicht beschrieben, soll die Ausgabe des Gästebuchs in mehrere View-Templates aufgeteilt werden. Das Haupt-Template beinhaltet demnach den Header des Gästebuchs und die Einbindung eines Content-Views:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\GuestbookController" @> <div class="gb--main"> ... <html:placeholder name="title" /> ... <html:placeholder name="desc" /> ... <core:importdesign namespace="APF\modules\guestbook2009\pres\templates" template="[gbview=list]" incparam="gbview" /> </div>

An dieser Stelle wird eine Besonderheit des <core:importdesign />-Tags genutzt, der neben einer ”statischen“ Einbindung ermöglicht, einen View abhängig von einem definierten URL-Parameter einzubinden. In diesem Anwendungsfall wird der Inhalt des Request-Parameters gbview dazu verwendet, zu entscheiden, welches Template eingebunden wird. Ist der Parameter nicht im Request vorhanden, wird das Template list.html geladen. Enthält dieser den Wert ”create“, so wird das Template create.html verwendet.

Wie im Abschnitt Einführung in das APF angesprochen, bietet das APF neben den Taglibs eine zweite Möglichkeit für die Generierung von dynamischen Inhalten: den Document-Controller.

Die ersten Zeilen des oben abgedruckten Templates definieren, welcher Controller bei der des Templates zum Einsatz kommen soll. Die Deklaration besagt, dass in diesem Fall die Klasse GuestbookController aus dem Namespace APF\modules\guestbook2009\pres\controller verwendet werden soll. Der Page-Controller erwartet, dass die Controller-Klasse implizit oder explizit von BaseDocumentController erbt und die Methode transformContent() implementieren. Die übrige Gestaltung einer Controller-Klasse obliegt dabei dem Entwickler. Das folgende Listing zeigt den zur Ausgabe des Titels und der Beschreibung notwendigen Code:

PHP-Code
class GuestbookController extends BaseDocumentController { public function transformContent(){ $gS = &$this->getDIServiceObject( 'APF\modules\guestbook2009\biz', 'GuestbookService' ); $guestbook = $gS->loadGuestbook(); $this->setPlaceHolder( 'title', $guestbook->getTitle() ); $this->setPlaceHolder( 'description', $guestbook->getDescription() ); } }

In den ersten Zeilen wird die Instanz des aktuell anzuzeigenden Gästebuchs geladen und anschließend werden die im Template definierten Platzhalter gefüllt. Zur Beschaffung der Daten wird der GuestbookService genutzt. Dieser hat Zugriff auf das zuvor beschriebene GuestbookModel und liefert deshalb die gewünschten Inhalte ohne Übergabe der Gästebuch-ID.

11. Der Ausgabe-View

Das im Abschnitt Gestaltung der Präsentationsschicht gezeigte Schaubild skizziert die im Ausgabe-View relevanten Bereiche in blauer Farbe. Zu diesen zählt der Header, mit der Möglichkeit einen neuen Eintrag zu erstellen, der Pager-Ausgabe zum Blättern der Einträge und die Liste der Einträge auf der gewählten Seite.

Auch in diesem Template stehen dem Entwickler alle Möglichkeiten zur Gestaltung der Inhalte frei und er kann beliebige weiter Templates einbinden. Hier ist es jedoch nicht notwendig, da die drei darzustellenden Bereiche sehr gut abgrenzbar sind. Für den Header- und Pager-Bereich können einfache Platzhalter verwendet werden, für die Ausgabe der Inhalte kommt eine weitere Taglib zum Einsatz: <html:template />. Diese bietet die Möglichkeit, wiederverwendbare, parametrisierte HTML-Fragmente innerhalb einer Template-Datei zu definieren. Im Controller kann diese dann genutzt werden um alle Einträge in einer Schleife auszugeben:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\GuestbookListController" @> ... <html:placeholder name="link" /> ... <html:placeholder name="pager" /> ... <html:placeholder name="content" /> <html:template name="entry"> ... <html:placeholder name="title" /> ... <html:placeholder name="name" />, <html:placeholder name="web" /> ... <html:placeholder name="text"/> ... Datum: <html:placeholder name="date" />, Uhrzeit: <html:placeholder name="time" /> ... </html:template>

Für die Ausgabe der Liste wurde das <html:template /> mit Platzhaltern ausgestattet, die die Inhalte eines Entry-Objekts repräsentieren. Die fertigen Liste wird dann dem Platzhalter <html:placeholder name="content" /> mitgegeben.

Dass ein Document-Controller von der Klasse BaseDocumentController erben muss ist nicht nur reine Konvention, sondern bietet auch eine Reihe von Vorteilen. Die Klasse beinhaltet eine Vielzahl von Methoden, die das Erzeugen von dynamischem Inhalt erleichtern. Zu diesen zählt die Funktion getTemplate($tmplName), die eine Referenz auf die mit $tmplName benannte Instanz der Taglib <html:placeholder /> zurück liefert. So kann in einem Controller bequem auf die im DOM-Baum umliegenden Objekte zugegriffen werden.

In diesem Fall handelt es sich dabei um ein Kind-Objekt des DOM-Knotens, der mit Hilfe der Template-Datei list.html erzeugt wurde. Die Instanz der Taglib <html:placeholder /> ist wiederum ein Kind der Instanz der Taglib <html:template />. Da über die Methode $this->getDocument() eine Referenz auf das aktuelle DOM-Objekt bezogen werden kann, kann auch in einem Controller auf die umliegenden Objekte direkt zugegriffen werden:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller; use APF\tools\link\LinkGenerator; use APF\tools\link\Url; class GuestbookListController extends GuestbookBaseController { public function transformContent() { $gS = & $this->getGuestbookService(); $entryList = $gS->loadPagedEntryList(); $tmpl_entry = & $this->getTemplate('entry'); $buffer = (string) ''; foreach ($entryList as $entry) { $editor = $entry->getEditor(); $tmpl_entry->setPlaceHolder('name', $editor->getName()); $tmpl_entry->setPlaceHolder('website', $editor->getWebsite()); $tmpl_entry->setPlaceHolder('title', $entry->getTitle()); $tmpl_entry->setPlaceHolder('text', $entry->getText()); $creationTimestamp = $entry->getCreationTimestamp(); $tmpl_entry->setPlaceHolder('time', date('H:i:s', strtotime($creationTimestamp))); $tmpl_entry->setPlaceHolder('date', date('d.m.Y', strtotime($creationTimestamp))); $buffer .= $tmpl_entry->transformTemplate(); } $this->setPlaceHolder('content', $buffer); // add the pager $this->setPlaceHolder('pager', $gS->getPagerOutput()); // add dynamic link $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array('gbview' => 'create'))); $this->setPlaceHolder('createlink', $link); } }

Die Funktion des Controllers besteht darin, die aktuell anzuzeigenden Einträge von der Business-Schicht entgegen zu nehmen, in einer Schleife die Instanz des Templates zu befüllen und das Ergebnis in einen Ausgabe-Puffer zu schreiben. Anschließend wird dieser zusammen mit der fertig generierten Ausgabe des Pagers und des Links in die jeweiligen Platzhalter-Tags eingesetzt.

Auch bei diesem Document-Controller lässt sich erkennen, dass die Granulierung der Funktionalität die Logik im Controller übersichtlich erscheinen lässt.

12. MOCK-Testing

In den beiden Controller-Klassen wurde stillschweigend davon ausgegangen, dass die Business-Komponente GuestbookService bereits implementiert und zum Einsatz bereit ist. Da wir jedoch den top-down-Ansatz für die Erstellung der Applikation gewählt haben, ist das natürlich nicht der Fall. Möchten wir trotzdem jetzt schon die Applikation testen um dem Kunden beispielsweise das look&feel zu präsentieren, bietet sich das mock'en (siehe Mock-Objekte) der Business-Schicht an.

Das Konzept von Mock-Testing besteht darin, notwendige Komponenten durch Klassen zu ersetzen, die die Funktionen statisch implementieren und so genannte Mock-Objekte – Test-Daten – zurückliefern. In unserem Fall ist das die Liste der Einträge – eine Liste von Entry-Objekten –, eine Instanz eines Gästebuchs und die Ausgabe des Pagers. Letztere können wir für einen ersten Test ignorieren und statt der HTML-Ausgabe einfach einen Leer-String zurückgeben. Die Liste der Inhalte und das Gästebuch erstellen wir in den Methoden des Mock-Gästebuch-Services manuell:

PHP-Code
class MockGuestbookService extends APFObject { public function loadGuestbook() { $gb = new Guestbook(); $gb->setTitle('Mein Gästebuch'); $gb->setDescription('Dies ist mein erster Test des neuen Gästebuchs. Die Einträge werden von einer Mock-Schicht zur Verfügung gestellt.'); return $gb; } public function loadPagedEntryList() { $entry = new Entry(); $entry->setTitle('MOCK-Eintrag'); $entry->setText( 'Dies ist ein MOCK-Text ...' ); $entry->setCreationTimestamp( date('Y-m-d H:i:s') ); $editor = new User(); $editor->setName('MOCK-Benutzer'); $editor->setEmail('test@test.de'); $editor->setWebsite( 'http://test.de' ); $entry->setEditor($editor); $entries = array(); for($i = 0; $i < 10; $i+){ $entries[] = $entry; } return $entries; } public function getPagerOutput() { return ''; } }

Die Klasse legen wir im Ordner biz unter dem Namen MockGuestbookService.php ab.

13. Kopplung von Schichten

In den Beispielen zuvor tauchte immer wieder die Methode getDIServiceObject() auf. Diese stellt eine Facade für den Aufruf des DIServiceManager (siehe Services) dar und liefert einen per dependency injection konfigurierten Service zurück. Ein Service zeichnet sich dabei durch eine Sektion in einer Konfigurationsdatei aus. Diese besitzt folgendes Schema:

APF-Konfiguration
[GuestbookService] servicetype = "" namespace = "" class = "" [init.{INITKEY}.method = "" init.{INITKEY}.namespace = "" init.{INITKEY}.name = ""] [conf.{CONFKEY}.method = "" conf. {CONFKEY}.value = ""]

Für den Service-Typ gibt es drei mögliche Werte:

  • NORMAL: Service wird ”normal“ mit Hilfe des new-Operators instanziiert,
  • SINGLETON: es wird pro Request exakt ein Service erzeugt,
  • SESSIONSINGLETON: es wird pro Session exakt ein Service erzeugt.

Die Parameter namespace und class beschreiben die Implementierung eines Services – eine PHP-Klasse –, mit den übrigen Parametern kann die Konfiguration des Services vorgenommen werden. Alle mit conf beginnenden Parameter enthalten eine statische Konfiguration, alle init Definitionen nehmen eine dynamische Konfiguration mit Hilfe von weiteren Service-Objekten vor.

Durch die Einführung des DIServiceManager wurde bei der vorliegenden Applikation bereits implizit die Möglichkeit geschaffen, MOCK-Objekte einzusetzen. So können wir ohne Änderung des Codes den im letzten Abschnitt implementierten Mock-Service einsetzen. Dazu muss dieser lediglich in der Konfigurationsdatei unter der Sektion GuestbookService eingetragen werden. Da die Mock-Schicht keinerlei Konfiguration benötigt, besteht die Konfigurationsdatei aus folgenden Zeilen:

APF-Konfiguration
[GuestbookService] servicetype = "SINGLETON" class = "APF\modules\guestbook2009\biz\MockGuestbookService"

14. Konfiguration

Um unser MOCK-up einem ersten Test unterziehen zu können, muss die Komponente GuestbookService noch zur Verwendung konfiguriert werden. Der vollständige Pfad einer Konfigurationsdatei setzt sich beim APF aus fünf verschiedenen Bestandteilen zusammen:

Basis-Ordner config: alle Konfigurationen werden in einem eigenen Verzeichnis verwaltet um Code und Konfiguration zu trennen.

Der Namespace: dieser beschreibt den zweiten Pfad-Abschnitt. Er ist üblicherweise identisch zum Namespace des verfassten Moduls/Tools. Dadurch wird die die Zugehörigkeit der Konfigurationen zum betreffenden Modul/Tool gekennzeichnet.

Der Context: Dieser fügt dem Pfad noch eine individuelle Unterscheidung hinzu. Das APF ist hinsichtlich der Konfiguration so ausgelegt, dass ein Modul/Tool in mehreren Applikationen eingebunden werden kann. Hierzu ist eine Unterscheidung notwendig, in welchem Umfeld dieses gerade läuft, um dafür spezialisierte Konfigurationen laden zu können. So kann mit einer Code-Basis eine beliebige Anzahl an Applikationen betrieben werden.

Die Umgebung (=Environment) ist Teil des Datei-Namens und bestimmt innerhalb eines Einsatzgebietes die Umgebung, auf der das Modul/Tool eingesetzt wird. Damit kann ohne Änderung der Applikation und der Konfiguration ein Modul auf unterschiedlichen Umgebungen (Dev/Test/Live) deployed werden.

Der Name der Datei bildet den Rumpf der Konfigurations-Datei. Als Endung wird .ini verwendet.

Zusammengefasst wird der Name einer Konfigurations-Datei durch folgendes Schema definiert:

Code
/VENDOR/config/{NAMESPACE}/{CONTEXT}/{ENVIRONMENT}__{NAME}.ini

Mit diesen Parametern ist es möglich Software für den Einsatz in unterschiedlichen Applikationen und Webseiten zu konfigurieren.

15. Ein erster Test!

Vor einem ersten Test müssen wir nun noch die Konfiguration für die Verwendung des DIServiceManager in den beiden Document-Controllern anlegen. Da die aktuell angelegte index.php den Context der Anwendung noch nicht definiert, müssen wir dies nun nachholen. Aus diesem Grund fügen in der index.php wir zwischen

PHP-Code
$fC = Singleton::getInstance(Frontcontroller::class);

und

PHP-Code
echo $fC->start('EXAMPLE\gb\templates', 'guestbook');

ein

PHP-Code
$fC->setContext('testproject');

hinzu. Damit wird der Context des Page-Controllers auf den Wert testproject gesetzt.

Wie der Dokumentation zu entnehmen ist, erwartet der Aufruf der Methode getDIServiceObject() zwei Parameter: den Namespace, unter dem die Konfiguration abgelegt ist und den Namen der Sektion, in der der Service definiert ist. In diesem Fall lautet der Namespace APF\modules\guestbook2009\biz. Daraus resultiert der folgende Konfigurations-Pfad:

Code
/var/www/html/guestbook/APF/config/modules/guestbook2009/biz/testproject/DEFAULT_serviceobjects.ini

Diese Datei füllen wir nun mit der Konfiguration des MOCK-Services (siehe Abschnitt Kopplung von Schichten). Beim Aufruf der zu Anfang angelegten index.php sehen wir nun die Ausgabe des Gästebuchs mit 10 Einträgen. Voila!

16. Sprachabhängigkeit

Die Templates zur Ausgabe enthalten an den relevanten Stellen explizite Texte wie z.B. Datum. Um das Gästebuch in mehreren Sprachen einsetzbar zu gestalten, könnten pro Sprache eigene Templates angelegt oder die Werte durch Platzhalter ersetzt werden. Die erste Möglichkeit erzeugt redundanten Code, der Pflege und Erweiterung des Moduls erschwert, die zweite Variante bedingt, dass die Platzhalter-Werte für jede Sprache gepflegt werden müssen.

Das APF bietet für diese Aufgabenstellung eine Lösung für die Umsetzungs-Variante 2 in Form von Taglibs an. Die sprachabhängigen Werte werden dabei aus einer Konfigurations-Datei bezogen. Die aktuell gewählte Sprache wird entnimmt die Taglib direkt aus dem Attribut $this->language, das durch den Page-Controller in jeden DOM-Knoten injiziert wird. Die Signatur des Tags ist laut Dokumentation die folgende:

APF-Template
<html:getstring namespace="" config="" entry="" />

Mit dem Attribut namespace wird der Ablage-Ort der Sprach-Konfiguration definiert, config enthält den Namen der Konfiguration und entry referenziert den Wert, der statt des Platzhalter-Tags ausgegeben werden soll. Damit kann das fest codierte Datum im Ausgabe-View folgendermaßen ersetzt werden:

APF-Template
<html:getstring namespace="APF\modules\guestbook2009\pres" config="language.ini" entry="list.date" />: <html:placeholder name="date" />, ...

Die korrespondierende Konfigurations-Datei ist wie im nachfolgenden Listing beschrieben aufgebaut:

APF-Konfiguration
[de] list.date = "Datum" ... [en] list.date = "Date" ...

Da sich die Signaturen der Tags in den Templates nur um das Attribut entry unterscheiden, kann die Einbindung nochmals durch die Implementierung einer eigenen Taglib vereinfacht werden. In der verküzten Form wird die Beschriftung des Datums dann per

APF-Template
<html:langlabel entry="form.label.email" />: <html:placeholder name="date" />, ...

erledigt. Die Implementierung der Taglib <form:langlabel /> nutzt dabei die im APF enthaltene Klasse LanguageLabelTag und erweitert diese um die Definition der feststehenden Attribute. Da diese Funktion nicht nur innerhalb eines Formulars, sondern auch bei der Ausgabe der Einträge verwendet wird, leitet die Klasse GuestbookFormLanguageLabelTag nochmals von einer Basis-Klasse ab:

PHP-Code
namespace APF\modules\guestbook2009\pres\taglib; use APF\core\pagecontroller\LanguageLabelTag; class GuestbookLanguageLabelTag extends LanguageLabelTag { public function __construct() { $this->setAttribute('namespace', 'APF\modules\guestbook2009\pres'); $this->setAttribute('config', 'language.ini'); } }

17. Formulare

Eine Besonderheit des APF ist die Formular-Unterstützung. Auf Basis des Taglib-Konzeptes wurden alle in HTML verfügbaren Formular-Elemente in Taglibs gekapselt. Damit können diese wie bei der Erstellung von HTML-Templates frei definiert werden, verfügen jedoch über Features wie Auto-Ausfüllen, Validierung und Filterung. Ein Formular lässt sich so vollständig mit Hilfe von Taglibs definieren ohne eine Zeile PHP-Code verfassen zu müssen. Hieraus ergibt sich zudem eine aus objektorientierter Sicht sehr elegante Möglichkeit Formulare zu kapseln.

Innerhalb eines Document-Controllers steht ein Formular als eigenständiges DOM-Objekt zur Verfügung, das alle seine Elemente als Kind-Knoten beinhaltet. Es besitzt Methoden um auf Formular-Elemente zugreifen und dynamische Formulare erstellen zu können. Ein typisches APF-Formular gestaltet sich wie folgt:

APF-Template
<html:form name="search" method="post"> <html:text name="searchterm" /> <form:button name="send" value="GO"/> </html:form>

In diesem Formular wird das Attribut action selbst gefüllt, so dass mit dem gezeigten Code bereits ohne Zutun ein Postback auf die aktuelle Seite möglich ist. Die notwendigen HTML-Attribute werden von jedem Tag selbst erzeugt. Wie bei den übrigen Taglibs des APF hat der Entwickler die Möglichkeit generische Attribute wie beispielsweise class oder id hinzuzufügen um Formatierungen vorzunehmen oder eine Adressierung per JavaScript zu erleichtern.

Um das oben gezeigte Formular darzustellen wird ein Document-Controller benötigt. Dies ist notwendig, da ein Formular immer an dynamische Aktionen gebunden ist, die vom Entwickler gemeint definiert werden müssen. Um das Such-Formular immer auf der Seite darzustellen genügt der folgende Code:

PHP-Code
class SearchController extends BaseDocumentController { public function transformContent(){ $form = &$this->getForm('search'); $form->transformOnPlace(); } }

18. Der Create-View

Um einen Eintrag verfassen zu können, benötigen wir ein Formular. Hierzu definieren wir ein neues Template, das wegen der Definition des <core:importdesign />-Tags durch Ändern des URL-Paramaters gbwiew aufgerufen werden kann. Das Template trägt den Namen create.html und muss im selben Namespace wie auch list.html liegen.

Der View selbst definiert nun das Formular mit den in den Anforderungen gewünschten Feldern:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\GuestbookCreateEntryController" @> ... <html:form name="create_entry" ...> Name: <form:text maxlength="100" name="name" /> <br /> E-Mail: <form:text maxlength="100" name="email" /> <br /> ... Text: <br /> <form:area name="text" /> ... <br /> <form:button name="send" /> ... </html:form>

Zur Sicherung der Eingabe wird eine Zeichen-Begrenzung via HTML definiert und Validierung und Filterung aktiviert. Zur Konfiguration der Validierung sind die Tag-Attribute validate, validator und button definiert. Sobald validate auf den Wert true eingestellt ist, wird ein Formular-Element auf das Klick-Event des angegebenen Buttons validiert. Die Art der Validierung kann mit dem Attribut validator bestimmt werden.

Die Filterung basiert auf einer ähnlichen Konfiguration, jedoch wird hierfür lediglich das Attribut filter hinzugefügt. Dieses referenziert entweder einen mitgelieferten Filter oder unter Angabe einer Filter-Klasse (Attribute: filterclass) einen eigenen Filter.

Aufgabe des Controllers ist es nun, das Formular solange darzustellen, bis dieses gemäß der Validator-Definition korrekt ausgefüllt ist um anschließend die Daten zur Speicherung an den GuestbookService zu übergeben:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller; use APF\modules\guestbook2009\biz\Entry; use APF\modules\guestbook2009\biz\User; use APF\tools\link\LinkGenerator; use APF\tools\link\Url; class GuestbookCreateEntryController extends GuestbookBaseController { public function transformContent() { $form = & $this->getForm('create_entry'); if ($form->isSent() && $form->isValid()) { // Fill domain objects by extracting the values // from the form elements directly. $name = & $form->getFormElementByName('name'); $email = & $form->getFormElementByName('email'); $website = & $form->getFormElementByName('website'); $user = new User(); $user->setName($name->getAttribute('value')); $user->setEmail($email->getAttribute('value')); $user->setWebsite($website->getAttribute('value')); $title = & $form->getFormElementByName('title'); $text = & $form->getFormElementByName('text'); $entry = new Entry(); $entry->setTitle($title->getAttribute('value')); $entry->setText($text->getContent()); $entry->setEditor($user); // Save the entry using the business component. $this->getGuestbookService()->saveEntry($entry); } ... $form->transformOnPlace(); ... } }

Ist das Formular weder abgeschickt noch valide, wird es per

PHP-Code
$form->transformOnPlace();

ausgegeben. Wurde das Formular abgeschickt und wunschgemäß ausgefüllt, so werden mit Hilfe der Methode

PHP-Code
$form->getFormElementByName('...');

die Formular-Elemente referenziert und die Werte wie von ”normalen“ HTML-Elementen eines Formulars ausgelesen. Da der GuestbookService aus Gründen einer konsistenten und typisierten API nur mit einem Domänen-Objekt versorgt werden darf, müssen die Informationen des Formulars in das Domänen-Modell überführt werden. Die Logik zur Speicherung des Eintrags und zur Anzeige des Ausgabe-Views wird dann vom GuestbookService übernommen.

19. Front-Controller

Im letzten Abschnitt wurde die Anforderung, das Formular durch ein CAPTCHA zu schützen ausgespart. Der Grund dafür ist, dass das APF dazu bereits ein fertiges Modul anbietet, das lediglich konfiguriert und in das Formular eingebaut werden muss. Das Modul besteht aus einer Taglib, die das CAPTCHA-Bild und ein Text-Feld anzeigt und sich selbständig um die Validierung der Eingabe kümmert. Die Anzeige des Bildes wird dabei von einer Front-Controller-Action übernommen. Hierbei wird der Umstand ausgenutzt, dass Actions vom Typ prepagecreate vor dem Aufbau der Seite ausgeführt werden. Dadurch kann ein dynamisch erzeugtes Bild mit derselben Bootstrap-Datei ausgeliefert werden, wie die Applikation selbst.

Um das unter CAPTCHA-Taglib (für Formulare) beschriebene Modul nutzen zu können, muss zunächst die Taglib in das Eingabe-Formular integriert werden:

APF-Template
<form:captcha clearonerror="true" />

Wie der Code-Box zu entnehmen ist, wird die verwendete Taglib zunächst dem Formular bekannt gegeben. Bauen wir die Zeilen in das Template des Create-Views ein, stellen wir zunächst fest, dass kein CAPTCHA-Bild ausgegeben wird.

Der Front-Controller ist eine Erweiterung des Page-Controllers, der vor dem Erstellen der Seite, nach dem Aufbau des DOM-Baumes und nach der Transformation so genannte Actions ausführt. Diese können für verschiedene Aufgaben innerhalb einer Applikation wie beispielsweise das Befüllen eines Models eingesetzt werden.

Der Front-Controller führt jedoch nur bekannte Actions aus. Diese können statisch in der index.php oder dynamisch über Anweisungen in der URL bekannt gegeben werden. Da in den URL-Anweisungen nicht alle Informationen zur Ausführung enthalten sind, muss ein URL-Mapping definiert sein.

Die für die Auslieferung des CAPTCHA-Bildes notwendige Konfiguration können wir aus dem apf-configpack-*-Release-Paket entnehmen. Hierzu befindet sich im Ordner modules/captcha/biz/actions/ eine Date mit dem Namen EXAMPLE_actionconfig.ini.

Für die Ablage der Konfigurations-Datei gilt auch hier das im Abschnitt Konfiguration beschrieben Schema. Die Action-Definition muss deshalb im Ordner

Code
/var/www/html/guestbook/APF/config/modules/captcha/biz/actions/testproject/

unter dem Namen

Code
DEFAULT_actionconfig.ini

abgelegt werden. Der folgende Code-Block zeigt den Inhalt der Action-Definition:

APF-Konfiguration
[showCaptcha] ActionClass = "APF\modules\captcha\biz\actions\ShowCaptchaImageAction"

Die ersten drei Parameter referenzieren die Klasse, die die Action im Code repräsentiert, die folgenden beschreiben die Klasse, die zur Kapselung der Parameter eingesetzt wird. Die Direktive FC.InputParams kann dazu verwendet werden, Standard-Parameter für eine Action zu definieren.

Rufen wir nun die Bootstrap-Datei via index.php?gbview=create auf, so wird das CAPTCHA-Bild angezeigt:

CAPTCHA-Taglib des APF

20. Login und Authentifizierung

Im Abschnitt Der Basis-View haben wir bereits die Möglichkeit geschaffen, im Inhaltsbereich des Gästebuchs verschiedene Views über den URL-Parameter gbview einblenden zu können. Der Login-View kann deshalb parallel zu den bereits implementierten View-Templates angelegt werden.

Zur Authentifizierung eines Gästebuch-Administrators soll gemäß Design Benutzer-Name und Passwort verwendet werden. Aus diesem Grund präsentiert der Login-View ein Formular zur Eingaben der relevanten Daten. Aus Gründen der Einfachheit wird das Backend nur in englischer Sprache angeboten:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\GuestbookLoginController" @> Please log in to administrate the guest book. <html:form name="login" method="post"> <html:placeholder name="error" /> <label>Username:</label> <form:text maxlength="100" name="username" class="gb--login-field-username" /> ... <label>Password:</label> <form:password maxlength="100" name="password" /> ... <form:button name="login" value="Login" /> </html:form> <html:template name="error"> ... The given user credentials are wrong! ... </html:template>

Zur Ausgabe der Fehlermeldung wurde ein Template angelegt, das die relevante Meldung beinhaltet. Dieses wird im Fehlerfall in den Formular-Platzhalter error injiziert um den Benutzer auf falsche Authentifizierungs-Daten hinzuweisen.

Der Controller übernimmt dabei die Aufgabe, die Inhalte des Formulars in ein Domänen-Objekt zu übersetzten und dieses dem GuestbookService zur Prüfung zu übergeben. Schlägt diese fehl, so wird die zuvor erwähnte Fehlermeldung angezeigt:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller; use APF\modules\guestbook2009\biz\User; class GuestbookLoginController extends GuestbookBaseController { public function transformContent() { $form = & $this->getForm('login'); if ($form->isSent() && $form->isValid()) { $fieldUser = & $form->getFormElementByName('username'); $fieldPass = & $form->getFormElementByName('password'); $user = new User(); $user->setUsername($fieldUser->getAttribute('value')); $user->setPassword($fieldPass->getAttribute('value')); if (!$this->getGuestbookService()->validateCredentials($user)) { $error = & $this->getTemplate('error'); $form->setPlaceHolder('error', $error->transformTemplate()); } } $form->transformOnPlace(); } }

Wird der Benutzer erfolgreich authentifiziert, sorgt der GuestbookService dafür, dass der entsprechende View des Backends angezeigt wird.

21. Die Administration

Das Backend des Gästebuchs bietet die Möglichkeit, Einträge zu editieren und zu löschen. Damit für das Backend keine weitere Installation des Frameworks und des Gästebuch-Moduls notwendig ist, soll der dieses direkt in das Modul integriert werden. Hierzu nutzen wir den Inhalts-View, der auch schon die Ausgabe, das Formular und das Login darstellt.

Der Admin-View – den wir parallel zu den bisher erstellten Templates anlegen – beinhaltet zwei weitere Views: das Menü zur Steuerung der Funktionen und den Inhaltsbereich, in dem die administrativen Aktionen vorgenommen werden können. Die folgende Abbildung zeigt den Aufbau:

Die Admin-Ansicht des APF-Gästebuch

Implementierungs-technisch bedeutet ein weiterer View dabei lediglich die Verwendung eines weiteren <core:importdesign />-Tags im Admin-View-Template. Da der Inhaltsbereich bereits über den URL-Parameter gbview gesteuert wird, muss an dieser Stelle darauf geachtet werden, dass der Steuer-Parameter einen anderen Namen erhält: adminview. Der Übersichtlichkeit halber werden die Dateien des Backends im Ordner admin abgelegt, der sowohl unter pres/templates/ als auch unter pres/controller/ angelegt wird. Gemäß des zuvor gezeigten Aufbaus beinhaltet das Template admin.html folgenden Quellcode:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\GuestbookAdminController" @> <a href="<html:placeholder name="editLink" />"Edit entry</a> <a href="<html:placeholder name="deleteLink" />">Delete entry</a> <a href="<html:placeholder name="logoutLink" />">Logout</a> ... <core:importdesign namespace="apf\modules\guestbook2009\pres\templates\admin" template="[adminview=start]" incparam="adminview" />

Wie auch im Haupt-Template des Gästebuchs, wurde auch hier der statische Teil – das Admin-Menü – nicht in ein eigenes Template ausgelagert. Sofern notwendig, kann dies jedoch im Rahmen einer Weiterentwicklung ohne weiteres durchgeführt werden.

Das <core:importdesign />-Tag inkludiert je nach Wert des Parameters adminview das Template, das die Aktionen des jeweiligen Menü-Punktes der Administration darstellt.

Der zugehörige Controller delegiert dabei die Prüfung, ob das Administrations-Interface aufgerufen werden darf, an den GuestbookService und füllt die Platzhalter mit den gewünschten Links:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller; use APF\tools\link\LinkGenerator; use APF\tools\link\Url; class GuestbookAdminController extends GuestbookBaseController { public function transformContent() { // invoke the service to check, if the current user may request this page $this->getGuestbookService()->checkAccessAllowed(); // generate the admin menu links to be able to include the module in either page. $editLink = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'admin', 'adminview' => 'edit' ))); $this->setPlaceHolder('editLink', $editLink); $deleteLink = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'admin', 'adminview' => 'delete' ))); $this->setPlaceHolder('deleteLink', $deleteLink); $logoutLink = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'admin', 'adminview' => 'logout' ))); $this->setPlaceHolder('logoutLink', $logoutLink); } }

Zur Link-Generierung wurde die auch bereits zuvor genutzte Klasse LinkGenerator verwendet. Diese bietet gegenüber der manuellen Link-Definition den Vorteil, dass nur diejenigen Parameter der aktuellen URL beeinflusst werden, die für das aktuelle Modul wirklich relevant sind. So können verschiedene Module sehr einfach auf einer Seite platziert werden, ohne dass Abhängigkeiten manuell auflösen zu müssen. Die URL-Parameter anderer Komponenten bleiben einfach unverändert in der URL enthalten.

22. Auslagerung von Templates

Die Templates zum Bearbeiten und Löschen eines Eintrags beinhalten gemäß der zuvor festgelegten Struktur beide ein Auswahl-Menü für das zu behandelnde Entry-Objekt. Um keinen redundanten Code zu erzeugen, kann für das Auswahl-Menü ein eigenes Template und ein eigener Controller erstellt werden.

Die DOM-Struktur des APF erlaubt es dann – ähnlich einer Prä-Compiler-Direktive – ein ausgelagertes Template an die gewünschte Stelle einzubinden. Im Document-Controller können die darin definierten Elemente dann wie direkt im Template vorhandene Objekte behandelt werden.

Um zu verhindern, dass der PHP-Code zum Befüllen und Ausgeben des Formulars redundant vorhanden ist, erstellen wir eine abstrakte Basis-Klasse, von der die konkreten Controller ableiten. Diese beinhaltet die Funktion, das Formular darzustellen:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller\admin; use APF\modules\guestbook2009\biz\Entry; use APF\modules\guestbook2009\pres\controller\GuestbookBaseController; use APF\tools\form\taglib\SelectBoxTag; use APF\tools\link\LinkGenerator; use APF\tools\link\Url; abstract class GuestbookBackendBaseController extends GuestbookBaseController { protected function displayEntrySelection($adminView) { // fill the select list $form = & $this->getForm('selectentry'); /* @var $select SelectBoxTag */ $select = & $form->getFormElementByName('entryid'); $entriesList = $this->getGuestbookService()->loadEntryListForSelection(); foreach ($entriesList as $entry) { /* @var $entry Entry */ $select->addOption($entry->getTitle() . ' (#' . $entry->getId() . ')', $entry->getId()); } // define form action url concerning the view it is rendered in $action = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'admin', 'adminview' => $adminView ))); $form->setAttribute('action', $action); $form->transformOnPlace(); } }

Das Einbinden des widerverwendbaren Template-Fragments gestaltet sich ähnlich dem Einbinden eines View-Templates:

APF-Template
<core:appendnode namespace="APF\modules\guestbook2009\pres\templates\admin" template="choose" />

23. Bearbeiten eines Eintrags

Das Bearbeiten eines Eintrags verhält sich weitestgehend identisch zur Erstellung. Unter Die Administration haben wir durch die Menü-Struktur und die Definition der Views bereits implizit vorgegeben, dass das Template edit.html die Darstellung für die Bearbeitung eines Entry-Objektes übernimmt. Dies umfasst das Menü zur Auswahl des gewünschten Eintrags und ein Formular zur Manipulation der Werte:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\admin\GuestbookEditEntryController" @> Please select an entry to edit! <core:appendnode namespace="APF\modules\guestbook2009\pres\templates\admin" template="choose" /> <html:form name="edit_entry" method="post"> ... <form:button name="send" value="Save" /> <form:hidden name="entryid" /> <form:hidden name="editorid" /> </html:form>

Da auf den Aufbau des Formulars bereits im Abschnitt Der Create-View besprochen wurde, ist die Darstellung dieses Templates auf das Wesentliche reduziert. Neben dem Formular beinhaltet es noch die Definition des zuständigen Controllers und die Einbindung des Auswahl-Menüs mit Hilfe des <core:appendnode />-Tags. Das Template choose.html selbst beinhaltet dabei die Definition des Auswahl-Formulars:

APF-Template
<html:form name="selectentry" method="post"> <label>Select entry:</label> <form:select name="entryid" /> <form:button name="choose" value="OK" /> </html:form>

Der Controller ist nun dafür zuständig, im ersten Schritt das Auswahl-Menü anzuzeigen und im zweiten Schritt die Bearbeitung des Eintrags zu ermöglichen:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller\admin; use APF\modules\guestbook2009\biz\Entry; use APF\modules\guestbook2009\biz\User; class GuestbookEditEntryController extends GuestbookBackendBaseController { public function transformContent() { $entryId = $this->getRequest()->getParameter('entryid'); if ($entryId === null) { $this->displayEntrySelection('edit'); } else { // pre-fill edit form by directly accessing the APF form objects $gS = & $this->getGuestbookService(); $form = & $this->getForm('edit_entry'); if ($form->isSent() === false) { $entry = $gS->loadEntry($entryId); $editor = $entry->getEditor(); $name = & $form->getFormElementByName('name'); $name->setAttribute('value', $editor->getName()); $email = & $form->getFormElementByName('email'); $email->setAttribute('value', $editor->getEmail()); $website = & $form->getFormElementByName('website'); $website->setAttribute('value', $editor->getWebsite()); $title = & $form->getFormElementByName('title'); $title->setAttribute('value', $entry->getTitle()); $text = & $form->getFormElementByName('text'); $text->setContent($entry->getText()); $hiddenEntryId = & $form->getFormElementByName('entryid'); $hiddenEntryId->setAttribute('value', $entry->getId()); $hiddenEditorId = & $form->getFormElementByName('editorid'); $hiddenEditorId->setAttribute('value', $editor->getId()); } else { // save entry if ($form->isValid() === true) { $entry = new Entry(); $editor = new User(); $name = & $form->getFormElementByName('name'); $editor->setName($name->getAttribute('value')); $email = & $form->getFormElementByName('email'); $editor->setEmail($email->getAttribute('value')); $website = & $form->getFormElementByName('website'); $editor->setWebsite($website->getAttribute('value')); $title = & $form->getFormElementByName('title'); $entry->setTitle($title->getAttribute('value')); $text = & $form->getFormElementByName('text'); $entry->setText($text->getContent()); // retrieve the entry id from the hidden field $hiddenEntryId = & $form->getFormElementByName('entryid'); $entry->setId($hiddenEntryId->getAttribute('value')); // retrieve the editor id from the hidden field $hiddenEditorId = & $form->getFormElementByName('editorid'); $editor->setId($hiddenEditorId->getAttribute('value')); $entry->setEditor($editor); $gS->saveEntry($entry); } } $form->transformOnPlace(); } } }

Die Unterscheidung der Anzeige-Schritte wird dabei über die Id des zu bearbeitenden Eintrags vorgenommen. Ist diese noch nicht im Request vorhanden – das Formular wurde noch nicht abgesendet –, wird das Auswahl-Menü angezeigt, wurde ein Eintrag zur Bearbeitung ausgewählt, wird das Formular angezeigt. Sofern das Formular zum ersten Mal dargestellt wird, fragt der Document-Controller die Werte bei der Business-Schicht an und füllt die Felder mit den Werten des gewählten Beitrags. Wurde das Formular gemäß den Validator-Regeln ausgefüllt und abgeschickt, so liest der Controller die Inhalte des Formulars aus, befüllt die relevanten Domänen-Objekte und übergibt diese dem GuestbookService zur Speicherung.

24. Löschen eines Eintrags

Möchte ein Administrator einen Eintrag löschen, so muss dieser zunächst den gewünschten Eintrag auswählen. Anschließend wird ein Bestätigungs-Dialog angezeigt, damit Beiträge nicht versehentlich gelöscht werden können. Da die Definition des Auswahlmenüs bereits besteht, beschreibt das Template delete.html lediglich einen Bestätigungs-Dialog in Form von zwei Formularen:

APF-Template
<@controller class="APF\modules\guestbook2009\pres\controller\admin\GuestbookDeleteEntryController" @> ... Please select an entry to delete! <core:appendnode namespace="APF\modules\guestbook2009\pres\templates\admin" template="choose" /> <html:template name="confirm_text"> Do you really want do delete this entry? </html:template> <html:form name="delete_yes" method="post"> <form:button name="yes" value="Yes" /> <form:hidden name="entryid" /> </html:form> <html:form name="delete_no" method="post"> <form:button name="no" value="No" /> </html:form>

Der zugehörige Controller bietet – ähnlich wie im letzten Abschnitt – die Liste der Einträge zur Auswahl an und kümmert sich um die Verarbeitung der Eingabe:

PHP-Code
namespace APF\modules\guestbook2009\pres\controller\admin; use APF\modules\guestbook2009\biz\Entry; class GuestbookDeleteEntryController extends GuestbookBackendBaseController { public function transformContent() { $entryId = $this->getRequest()->getParameter('entryid'); if ($entryId === null) { $this->displayEntrySelection('delete'); } else { $form_yes = & $this->getForm('delete_yes'); $form_no = & $this->getForm('delete_no'); if ($form_no->isSent() || $form_yes->isSent()) { $entry = null; if ($form_yes->isSent()) { $entry = new Entry(); $entry->setId($entryId); } $this->getGuestbookService()->deleteEntry($entry); } else { $hidden_yes_entryid = & $form_yes->getFormElementByName('entryid'); $hidden_yes_entryid->setAttribute('value', $entryId); $form_yes->transformOnPlace(); $hidden_no_entryid = & $form_no->getFormElementByName('entryid'); $hidden_no_entryid->setAttribute('value', $entryId); $form_no->transformOnPlace(); $template_confirm_text = $this->getTemplate('confirm_text'); $template_confirm_text->transformOnPlace(); } } } }

25. Die Business-Schicht

Nach dem wir die Funktion der Präsentations-Schicht nun vollständig implementiert haben, widmen wir uns der Business-Schicht. Dieser kommt die zentrale Steuerung der Anwendung zu. Sie sorgt dafür, dass ein Event mit dem Anzeigen eines dafür vorgesehenen Views quittiert wird und die in der Applikation behandelten Daten an die Präsentations- oder Daten-Schicht weitergegeben werden. Sie kümmert sich zudem um das Thema Paging und die Benutzer-Authentifizierung für das Backend.

Wie im Abschnitt Kopplung von Schichten beschrieben ist, wird der DIServiceManager (siehe [5]) zur Entkoppelung der Schichten genutzt. Dies ermöglicht nicht nur einen einfachen Austausch von Schichten, sondern kann ebenfalls dazu verwendet werden, eine Komponente zu konfigurieren. Zur Anzeige der blätterbaren Liste kommt das Pager-Modul (siehe Pager) des APF zum Einsatz. Dieses benötigt zur Initialisierung den Namen der zu verwendenden Konfiguration. Um diese Abhängigkeit für die Business-Schicht transparent zu halten und die Möglichkeit zu wahren, dies von außen steuern zu können, wird diese Information via dependency injection mitgegeben. Dazu fügen wir eine Konfigurations-Anweisung zur Service-Definition hinzu und ändern gleichzeitig den Service-Typ auf SINGLETON, damit die Business-Schicht nur einmal innerhalb der Applikation existiert:

APF-Konfiguration
[GuestbookService] servicetype = "SINGLETON" class = "APF\modules\guestbook2009\biz\GuestbookService" conf.pager.method = "setPagerConfigSection" conf.pager.value = "Guestbook2009"

Beim Erzeugen des GuestbookService wird der Methode setPagerConfigSection() der Wert Guestbook2009 mitgegeben. Damit sichergestellt ist, dass der Service immer korrekt konfiguriert wird muss die Implementierung der Klasse folgendes Grundgerüst aufweisen:

PHP-Code
final class GuestbookService extends APFObject { private $pagerConfigSection; public function setPagerConfigSection($section){ $this->pagerConfigSection = $section; } }

Da der Pager – wie auch die CAPTCHA-Taglib –fertige Komponenten sind, soll dieser hier nicht weiter betrachtet werden.

26. Laden des Gästebuchs

Beginnen wir bei der Implementierung des GuestbookService mit dem Laden des Gästebuchs. Für diese Aufgabe stellt die Business-Schicht die Methode loadGuestbook() zur Verfügung:

PHP-Code
final class GuestbookService extends APFObject { public function loadGuestbook(){ $mapper = &$this->getMapper(); return $mapper->loadGuestbook(); } private function &getMapper(){ return $this->getDIServiceObject('APF\modules\guestbook2009\data', 'GuestbookMapper'); } }

Wie dem Quellcode entnommen werden kann, wird das Laden des gewünschten Guestbook-Objektes an die Datenschicht delegiert. Dies erfolgt deshalb, da bei diesem Vorgang keine weitere Interaktion der Business-Schicht notwendig ist. Die Rückgabe des Mapper-Aufrufs enthält das gewünschte Gästebuch-Domänen-Objekt.

Auch in der Business-Schicht wird der DIServiceManager zur Adressierung eines Services – in diesem Fall der Datenschicht – genutzt. Damit ist es möglich, die Business-Schicht mit Hilfe einer MOCK-Daten-Schicht vollständig automatisiert zu testen und die Datenschicht mit der notwendigen Konfiguration zu versorgen.

27. Laden der Einträge

Wie bereits angesprochen, wird zum Laden der Einträge der Pager des APF eingesetzt. Im Gegensatz zu der im vorangegangenen Abschnitt besprochenen Aufgabe ist hier das Eingreifen der Business-Schicht notwendig.

Daher wird zunächst die Liste der Einträge über den Pager bezogen. Anschließend wird die Daten-Schicht-Komponente dazu genutzt um die Instanzen der Einträge zu laden. Die Methode __getPager() kümmert sich dabei um die Bereitstellung der konfigurierten Instanz des Pagers:

PHP-Code
public function loadPagedEntryList(){ $pager = & $this->getPager(); /* @var $model GuestbookModel */ $model = & $this->getServiceObject('APF\modules\guestbook2009\biz\GuestbookModel'); $entryIds = $pager->loadEntries(array('GuestbookID' => $model->getGuestbookId())); $entries = array(); $mapper = & $this->getMapper(); foreach ($entryIds as $entryId) { $entries[] = $mapper->loadEntry($entryId); } return $entries; } private function &getPager(){ if ($this->pager === null) { /* @var $pMF PagerManagerFabric */ $pMF = & $this->getServiceObject('APF\modules\pager\biz\PagerManagerFabric'); $this->pager = & $pMF->getPagerManager($this->pagerConfigSection); } return $this->pager; }

28. Authentifizierung

Wie in der Implementierung der Präsentations-Schicht bereits festgelegt, soll die Business-Schicht die Methode validateCredentials() bereitstellen, um die vom Nutzer eingegebenen Zugangsdaten zu validieren. Die Methode wird dabei gleichzeitig dazu verwendet, den Benutzer im Erfolgsfall in der Session als eingeloggt zu markieren und den Admin-View anzuzeigen:

PHP-Code
public function validateCredentials(User $user) { $mapper = & $this->getMapper(); if ($mapper->validateCredentials($user)) { // log user in /* @var $model GuestbookModel */ $model = & $this->getServiceObject('APF\modules\guestbook2009\biz\GuestbookModel'); $guestbookId = $model->getGuestbookId(); $session = $this->getRequest()->getSession('APF\modules\guestbook2009\biz\\' . $guestbookId); $session->save('LoggedIn', 'true'); // redirect to admin page $adminLink = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'admin', 'adminview' => null ))); $this->getResponse()->forward($adminLink); } return false; }

Am Aufruf des LinkGenerator lässt sich erkennen, dass das Konzept der View-Definition hinsichtlich der Adressierung deutliche Vorteile aufweist. So ist es bei der Generierung des Links lediglich notwendig, die View-Parameter ungeachtet der Hierarchie entsprechend dem gewünschten Szenario in der URL zu platzieren.

29. Speichern von Einträgen

Eine weitere Aufgabe besteht in der Speicherung von Einträgen. Hier ergeben sich zwei Anwendungsfälle: das Anlegen eines Eintrags und das Speichern des Eintrags nach erfolgter Administration. Die Speicherung des Eintrags selbst wird dabei über eine Methode des Mappers abgebildet, die beide Anwendungsfälle abdeckt. Die Anzeige des Views wird daran festgemacht, ob der Eintrag bereits eine Id besitzt oder nicht:
PHP-Code
public function saveEntry(Entry $entry) { $mapper = & $this->getMapper(); $mapper->saveEntry($entry); // Forward to the desired view to prevent F5-bugs. $entryId = $entry->getId(); if (!empty($entryId)) { $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'admin' ))); } else { $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( 'gbview' => 'list' ))); } $this->getResponse()->forward($link); }

Die Implementierung der übrigen Funktionalitäten der Business-Schicht können der Klasse GuestbookService unter [1] direkt entnommen werden.

30. Die Datenschicht

Die Datenschicht des Gästebuchs stellt den komplexesten Teil der Anwendung dar. Dies ist darin begründet, dass das Design des Gästebuchs vorsieht, das Mapping der in der Datenbank gespeicherten, Sprach-abhängigen, Objekte in die einsprachigen Domänen-Objekte in der Datenschicht vorzunehmen.

Um die Speicherung und Bearbeitung der Daten-Objekte zu erleichtern, wird bei der Implementierung auf den GenericORMapper (siehe Generischer O/R-Mapper) zurückgegriffen. Dieser übernimmt bereits einen Großteil der Aufgaben und stellt eine Schnittstelle zur Verwaltung von Objekten und deren Beziehungen in einer relationalen Datenbank bereit. Ein weiterer Vorteil des GenericORMapper ist, dass die Benennungen der Objekte und Beziehungen aus dem Design 1:1 in die Konfiguration des Mappers einfließen und sich im Code wieder finden.

Rufen wir uns zunächst noch einmal das UML-Diagramm der Objekte der Datenschicht in Erinnerung:

Das Datenmodell des APF-Gästebuch

Dieses sieht vor, dass die Sprach-abhängigen Werte eines Domänen-Objekts über Attribut-Objekte abgebildet werden, die zu einer Sprache assoziiert sind. Beim Laden eines Entry-Objektes müssen demnach alle Attribut-Objekte geladen werden, die sowohl unter dem Entry-Stellvertreter-Objekt komponiert als auch zur aktuell gewählten Sprache assoziiert sind. Anschließend kann damit das Entry-Domänen-Objekt gefüllt werden. Bei der Speicherung eines Domänen-Objekts muss der umgekehrte Weg beschritten und die einzelnen, Sprach-abhängigen Attribute in Attribute-Objekte mit einer Assoziation zur aktuell aktiven Sprache übersetzt werden. Dieselbe Vorgehensweise gilt für das Laden und die Speicherung von Guestbook-Domänen-Objekten.

31. Der O/R-Mapper

Zur Nutzung des GenericORMapper, müssen zunächst die im UML-Diagramm abgebildeten Objekte und deren Beziehung in je einer Konfiguration definiert werden. Dazu dienen zwei Konfigurations-Dateien: DEFAULT_*_objects.ini für die Definition der Objekte und DEFAULT_*_relations.ini für die Beschreibung der Beziehungen. Die Objekt-Definition hat gemäß UML folgenden Inhalt:

APF-Konfiguration
[Guestbook] [Entry] [Attribute] Name = "VARCHAR(100)" Value = "TEXT" [Language] DisplayName = "VARCHAR(100)" ISOCode = "VARCHAR(2)" [User] Name = "VARCHAR(100)" Email = "VARCHAR(100)" Website = "VARCHAR(100)" Username = "VARCHAR(100)" Password = "VARCHAR(100)"

Die Sektionen Guestbook und Entry beinhalten keine Objekt-Attribute, da es sich bei diesen nur um Stellvertreter-Objekte handelt. Die im UML beschriebenen Beziehungen werden wie folgt konfiguriert:

APF-Konfiguration
[Guestbook2LangDepValues] Type = "COMPOSITION" SourceObject = "Guestbook" TargetObject = "Attribute" [Entry2LangDepValues] Type = "COMPOSITION" SourceObject = "Entry" TargetObject = "Attribute" [Guestbook2Adminstrator] Type = "ASSOCIATION" SourceObject = "Guestbook" TargetObject = "User" [Editor2Entry] Type = "ASSOCIATION" SourceObject = "User" TargetObject = "Entry" [Guestbook2Entry] Type = "COMPOSITION" SourceObject = "Guestbook" TargetObject = "Entry" [Attribute2Language] Type = "ASSOCIATION" SourceObject = "Attribute" TargetObject = "Language"

Zur Verwaltung der Objekte bringt der O/R-Mapper die Klasse GenericDomainObject mit, die zur Repräsentation der gespeicherten Daten-Objekte verwendet wird. Nach dem Setup der Datenbank (siehe nachfolgender Abschnitt) kann ein User-Objekt mit Hilfe des Mappers wie folgt gespeichert werden:

PHP-Code
$user = new GenericDomainObject('User'); $user->setProperty('Name', ''); $user->setProperty('Email', ''); $orm->saveObject($user);

Die Variable $orm beinhaltet dabei eine Instanz des Mappers, der durch

PHP-Code
$ormFactory = &$this->getServiceObject( 'APF\modules\genericormapper\data\GenericORMapperFactory' ); $orm = &$ormFactory->getGenericORMapper( 'APF\modules\guestbook2009\data', 'guestbook2009', '...' );

erzeugt wurde.

32. Setup der Datenbank

Der GenericORMapper bietet eine komfortable Möglichkeit, aus der erstellten Konfiguration das Setup der Datenbank zu generieren. Hierzu liegen dem APF die Komponente GenericORMapperManagementTool und eine Anleitung zum Setup der Datenbank bei. Um die Datenbank direkt initialisieren zu können, muss jedoch eine gültige Datenbank-Konfiguration vorliegen. Da der GenericORMapper den ConnectionManager als Datenbank-Abstraktions-Schicht nutzt, muss dem Setup-Tool lediglich eine gültige Konfigurations-Sektion übergeben werden.

Datenbank-Konfigurationen werden dabei unter dem Namespace APF\core\database und dem jeweiligen Context der Applikation in der Datei DEFAULT_connections.ini erwartet. Diese folgt dem anschließend abgedruckten Schema:

APF-Konfiguration
[guestbook2009] Host = "localhost" User = "..." Pass = "..." Name = "phpjournal" Type = "MySQLi" [Charset = "utf8"] [Collation = "utf8_general_ci"]

Die Direktive Type definiert den Datenbank-Treiber, der für die Verbindung verwendet werden soll. In diesem Fall verwenden wir MySQLi, da die Daten in einer MySQL gespeichert werden sollen.

Um mit dem Setup beginnen zu können, müssen wir die oben beschrieben Konfigurations-Dateien im richtigen Verzeichnis ablegen. Um innerhalb eines Moduls mehrere Mapper-Konfigurationen verwenden zu können, wird jeder Satz an Konfigurations-Dateien nochmals mit einem eigenen Namens-Zusatz ausgestattet. Im Code-Beispiel des letzten Abschnitts wurde dieses bereits auf guestbook2009 festgelegt. Damit lautet der Pfad der Konfigurations-Dateien wie folgt:

Code
/APF/config/modules/guestbook2009/data/testproject/DEFAULT_guestbook2009_objects.ini /APF/config/modules/guestbook2009/data/testproject/DEFAULT_guestbook2009_relations.ini

Nach dem Anlegen können wir das Setup der Datenbank mit dem folgenden Script ausführen:

PHP-Code
include('./APF/core/bootstrap.php'); use APF\modules\genericormapper\data\tools\GenericORMapperManagementTool; $setup = new GenericORMapperManagementTool(); $setup->setContext('testproject'); $setup->addMappingConfiguration('APF\modules\guestbook2009\data', 'guestbook2009'); $setup->addRelationConfiguration('APF\modules\guestbook2009\data', 'guestbook2009'); $setup->setConnectionName('guestbook2009'); $setup->run(true);

Ein Blick in die Datenbank zeigt uns, welche Tabellen erstellt wurden:

Das Tabellen-Layout des APF-Gästebuch

Tabellen mit dem Präfix ass_ speichern die Zuordnung von zwei Objekten über eine Assoziation, Tabellen mit der Kennzeichnung cmp_ referenzieren zwei Objekte über eine Komposition und Tabellen mit ent_ zu Beginn des Namens sind für die Haltung der eigentlichen Objekt-Attribute reserviert. Diese Art des Tabellen-Designs wird auch als Teil-Normalisierung bezeichnet, da die Beziehungen im Gegensatz zur Vollständigen De-Normalisierung von den Objekt-Daten getrennt gespeichert werden.

33. Laden von Einträgen

Beginnen wir zunächst mit dem Laden einer Liste von Einträgen, für die Anzeigen der Auswahl im Backend:

PHP-Code
public function loadEntryListForSelection() { $sortCrit = new GenericCriterionObject(); $sortCrit->addOrderIndicator('CreationTimestamp', 'DESC'); $gb = $this->getCurrentGuestbook(); return $this->mapGenericEntries2DomainObjects( $gb->loadRelatedObjects('Guestbook2Entry', $sortCrit), false ); }

Der GenericORMapper bietet die Möglichkeit, Daten mit Hilfe eines GenericCriterionObjects zu laden. Über dieses können Limitierung, Sortierung, Beziehungs-Indikatoren und Einschränkungen über Attribute für die zu ladenen Objekte definiert werden. In diesem Fall werden die Einträge durch die Definition des gezeigten Such-Kriteriums nach Datum absteigend angeordnet.

Anschließend können mit Hilfe der Gästebuch-Instanz alle über die Beziehung Guestbook2Entry verknüpften Einträge geladen werden. Die Rückgabe der Methode enthält eine Liste von GenericDomainObject-Instanzen. Diese müssen später noch in die eigentlichen Domänen-Objekte übersetzt werden.

Die private Methode getCurrentGuestbook() liefert die Instanz des Guestbook-Objekts (GenericDomainObject). Dieses repräsentiert das aktuell angezeigte Gästebuch in der Datenbank. Hierzu wird die Methode loadObjectByID() des GenericORMapper genutzt:

PHP-Code
private function getCurrentGuestbook(){ /* @var $model GuestbookModel */ $model = & $this->getServiceObject('APF\modules\guestbook2009\biz\GuestbookModel'); return $this->orm->loadObjectByID('Guestbook', $model->getGuestbookId()); }

Da die Rückgabe an die Business-Schicht in der Form von Domänen-Objekten erfolgen soll, müssen die generischen Domänen-Objekte des O/R-Mappers übersetzt werden. Hierzu wird die Methode __mapGenericEntries2DomainObjects() verwendet, die im folgenden Kasten abgebildet ist:

PHP-Code
private function mapGenericEntries2DomainObjects($entries = array(), $addEditor = true){ // return empty array, because having no entries means nothing to do! if (count($entries) == 0) { return array(); } // invoke benchmarker to be able to monitor the performance /* @var $t BenchmarkTimer */ $t = & Singleton::getInstance('APF\core\benchmark\BenchmarkTimer'); $t->start('mapGenericEntries2DomainObjects()'); // load the language object for the current language to enable // language dependent mapping! $lang = $this->getCurrentLanguage(); // define the criterion $critEntries = new GenericCriterionObject(); $critEntries->addRelationIndicator('Attribute2Language', $lang); $gbEntries = array(); /* @var $current GenericDomainObject */ foreach ($entries as $current) { // Check, whether there are attributes related in the current language. // If not, do NOT add an entry, because it will be empty! $attributes = $this->orm->loadRelatedObjects($current, 'Entry2LangDepValues', $critEntries); if (count($attributes) > 0) { // load the entry itself $entry = new Entry(); $entry->setCreationTimestamp($current->getProperty('CreationTimestamp')); foreach ($attributes as $attribute) { if ($attribute->getProperty('Name') == 'title') { $entry->setTitle($attribute->getProperty('Value')); } if ($attribute->getProperty('Name') == 'text') { $entry->setText($attribute->getProperty('Value')); } } // add the editor's data if ($addEditor === true) { $editor = new User(); $user = $this->orm->loadRelatedObjects($current, 'Editor2Entry'); $editor->setName($user[0]->getProperty('Name')); $editor->setEmail($user[0]->getProperty('Email')); $editor->setWebsite($user[0]->getProperty('Website')); $editor->setId($user[0]->getProperty('UserID')); $entry->setEditor($editor); } $entry->setId($current->getProperty('EntryID')); $gbEntries[] = $entry; } } $t->stop('mapGenericEntries2DomainObjects()'); return $gbEntries; } private function getCurrentLanguage() { $crit = new GenericCriterionObject(); $crit->addPropertyIndicator('ISOCode', $this->language); return $this->orm->loadObjectByCriterion('Language', $crit); }

Diese nimmt eine Liste von generischen Domänen-Objekte entgegen und übersetzt sie in eine Liste von Anwendungs-Objekte.

Um die Attribute eines Eintrags zu laden, wird das Entry-Stellvertreter-Objekt auf der einen Seite und die aktuell angezeigte Sprache auf der anderen Seite als Kriterium genutzt, um die Menge der zu ladenden Attribute-Objekte einzuschränken. Sofern die Datenstruktur keinen Fehler enthält werden dann exakt die relevanten Attribute vom O/R-Mapper zurückgegeben.

Anschließend können die Werte der Attribute-Objekte in die Entry-Domänen-Objekte übernommen und die Beziehung zum Autor eines Eintrags aufgelöst werden. Auch in diesem Fall kann eine Beziehungen – Editor2Entry – als Kriterium für das Laden des User-Objekts aus der Datenbank genutzt werden.

34. Speichern eines Eintrags

Die Speicherung eines Eintrags gestaltet sich etwas umfangreicher als der reine Lesezugriff auf die Datenstruktur. Dies ist nicht nur der Tatsache geschuldet, dass die hierfür bereitgestellte Methode das Anlegen und das Bearbeiten eines Eintrags berücksichtigt muss, sondern, dass die Beziehungen zwischen Eintrag und Autor sowie den Sprach-abhängigen Attributen aufgelöst werden muss.

Die Idee ist auch hier, die Domänen-Objekt-Struktur zunächst in die generische Datenstruktur zu übersetzen und diese dann mit Hilfe des GenericORMapper zu speichern. Die spiegelt sich auch in der Signatur der Methode saveEntry() wieder:

PHP-Code
public function saveEntry($entry){ $genericEntry = $this->mapDomainObject2GenericEntry($entry); $this->orm->saveObject($genericEntry); }

Die Übersetzung der Struktur übernimmt nun die Methode mapDomainObject2GenericEntry(), die im folgenden Kasten abgebildet ist:

PHP-Code
private function mapDomainObject2GenericEntry($domEntry){ $lang = $this->getCurrentLanguage(); $domEditor = $domEntry->getEditor(); $editor = new GenericDomainObject('User'); $editor->setProperty('Name', $domEditor->getName()); $editor->setProperty('Email', $domEditor->getEmail()); $editor->setProperty('Website', $domEditor->getWebsite()); $editorId = $domEditor->getId(); if (!empty($editorId)) { $editor->setProperty('UserID', $editorId); } // try to load an existing title attribute to avoid new attributes // on updates and merge changes $title = $this->getGenericAttribute($domEntry, 'title'); $title->setProperty('Name', 'title'); $title->setProperty('Value', $domEntry->getTitle()); $title->addRelatedObject('Attribute2Language', $lang); $text = $this->getGenericAttribute($domEntry, 'text'); $text->setProperty('Name', 'text'); $text->setProperty('Value', $domEntry->getText()); $text->addRelatedObject('Attribute2Language', $lang); // setup generic domain object structure to preserve the relations $entry = new GenericDomainObject('Entry'); $entry->addRelatedObject('Entry2LangDepValues', $title); $entry->addRelatedObject('Entry2LangDepValues', $text); $entry->addRelatedObject('Editor2Entry', $editor); $gb = $this->getCurrentGuestbook(); $entry->addRelatedObject('Guestbook2Entry', $gb); $entryId = $domEntry->getId(); if (!empty($entryId)) { $entry->setProperty('EntryID', $entryId); } return $entry; } private function getGenericAttribute($domEntry, $name){ // try to load $entry = new GenericDomainObject('Entry'); $entry->setProperty('EntryID', $domEntry->getId()); $crit = new GenericCriterionObject(); $crit->addPropertyIndicator('Name', $name); $crit->addRelationIndicator('Attribute2Language', $this->getCurrentLanguage()); $attributes = $this->orm->loadRelatedObjects($entry, 'Entry2LangDepValues', $crit); if (isset($attributes[0])) { return $attributes[0]; } return new GenericDomainObject('Attribute'); }

Diese schreibt zunächst die Domänen-Objekte in die korrespondierenden GenericDomainObject-Instanzen um und löst anschließend in Sprach-abhängigen Attribute in Attribute-Objekte auf. Hierbei wird berücksichtigt, ob ein Attribut bereits in der Datenbank angelegt wurde, um mehrfache Speicherung verhindern. Sofern beim Anlegen eines Eintrags noch kein Attribut existiert, liefert die Methode getGenericAttribute() eine neue Instanz zurück.

Am Ende der Methode wird die Objekt-Struktur gemäß dem UML-Design zusammengefügt. Die Sprach-abhängigen Attribute werden unterhalb der Entry-Objekte komponiert, zur aktuellen Sprache assoziiert und selbst unterhalb des aktuellen Gästebuchs komponiert. Die Herstellung der Beziehungen erfolgt dabei durch Hinzufügen eines GenericDomainObject zu einem anderen über die Methode addRelatedObject() unter Angabe des Beziehungs-Schlüssels.

35. Löschen eines Eintrags

Das Löschen eines Eintrags gestaltet sich verhältnismäßig einfach. Bei der Ausführung muss lediglich die Besonderheit des GenericORMapper beachtet werden, dass Objekte, die weitere Objekte komponieren, nicht gelöscht werden können. Dieses Feature stellt sicher, dass keine Daten-Leichen durch versehentliches Löschen von Objekten entstehen.

Im aktuellen Anwendungsfall müssen deshalb zunächst die Attribute- und User-Objekte selektiert und eliminiert werden, bevor das eigentliche Entry-Objekt gelöscht werden darf:

PHP-Code
public function deleteEntry($domEntry){ $entry = new GenericDomainObject('Entry'); $entry->setProperty('EntryID', $domEntry->getId()); // Delete the attributes of the entry, so that the object // itself can be deleted. If we don't do this, the ORM // will not be glad, because the entry still has child objects! $attributes = $this->orm->loadRelatedObjects($entry, 'Entry2LangDepValues'); foreach ($attributes as $attribute) { $this->orm->deleteObject($attribute); } // delete associated users, because we don't need them anymore. $users = $this->orm->loadRelatedObjects($entry, 'Editor2Entry'); foreach ($users as $user) { $this->orm->deleteObject($user); } // now delete entry object (associations are deleted automatically) $this->orm->deleteObject($entry); }

Das manuelle Auflösen von Assoziationen ist nicht notwendig, diese werden vom O/R-Mapper automatisch gelöscht.

Die Implementierung der übrigen Funktionalität der Daten-Schicht können der Klasse GuestbookMapper unter [1] direkt entnommen werden.

36. That's it!

Lassen wir die im ersten Teil beschriebenen Anforderungen noch einmal Revue passieren, so bietet die vorliegende Implementierung eine gute Grundlage für zukünftige Erweiterungen. Sofern neue Ausgabe-Bereiche eingebunden werden sollen, kann dies sehr einfach in den jeweiligen View-Templates bewerkstelligt werden. Wünscht der Kunde die Möglichkeit, Einträge mit weiteren Attributen auszustatten, genügt ein neuer Eintrag in der O/R-Mapper-Konfiguration und ein paar Zeilen Mapping-Code.

Wie der Umfang des Artikels zeigt, ist die Implementierung des Gästebuchs keine zu unterschätzende Aufgabe. Der geneigte Leser mag an dieser Stelle sicher schmunzeln und ein ”Das hätte man aber einfacher haben können!“ auf den Lippen haben, es war jedoch Ziel dieses Artikels, eine Vorgehensweise aufzuzeigen, die bei komplexen Anwendungen notwendig ist. Einige Tools und Frameworks verfolgenden zwar den Ansatz, Applikationen an Hand der Konfigurationen vollständig zu generieren, dies scheitert jedoch bei höherer Komplexität und treibt den manuellen Anpassungsaufwand deutlich in die Höhe. Aus diesem Grund ist es ratsam, komplexere Applikationen – und das Gästebuch zähle ich bereits hinzu – weiterhin selbst zu implementieren und auf den Einsatz von bewährten Tools zu erstellen. Dabei – und vor allem dabei – gilt: ”Think before you start to write code!“.