Quicknavi |
|
Kommentar-Funktion
1. Einleitung
Das Tutorial soll zeigen, wie das Kommentar-Modul am Ende der Dokumentations-Seiten aufgebaut ist und
welche Komponenten zur Funktionsweise beitragen. Es soll nochmals verdeutlicht werden, welche Richtlinen
die im Manual genannten Design-Pattern wirklich geben und welche Hintergründe diese haben. Aus
diesem Grund soll das Tutorial wie folgt aufgebaut sein:
- Beschreibung des Aufbaus der Applikation,
- Verwendung von Modulen zur Unterstützung und
- Erläuterung der Programm-Dateien.
Dabei werden die unter Grundlagen bereits vermittelten
Design-Pattern praktisch für diesen Anwendungsfall aufbereitet. Dieses Tutorial sollte deshalb
als Einstiegs-Tutorial vor den beiden Tutorials Kontaktformular
und Gästebuch zum Studium herangezogen
werden.
2. Aufbau der Applikation
Die Kommentar-Funktion besteht aus Sicht eines Anwenders aus zwei Teilen: Ausgabe und Formular. Aus
Sicht eines Entwicklers aus der Ausgabe, dem Formular, einer Datenbank und der zugehörigen Logik,
die das Auslesen der Daten und das Füllen der Datenbank organisiert. Folgt man der Lehre des
objektorientierten Applikations-Entwurfs, so gibt es dort eine Vielzahl an Entwurfsmuster (Pattern),
die zum Entwurf der Kommentar-Funktion herangezogen werden können. Die in diesem Entwurf
eingeschlossenen Muster und deren Auswirkungen seien hier erläutert:
2.1. 3-Schicht-Architektur
Das Pattern der "3-Schicht-Architektur" geht davon aus, dass es von Vorteil ist, ein Programm - hier
ein Teil einer Gesamt-Applikation - in die Schichten Daten-Schicht, Business-Schicht
und Präsentations-Schicht zu unterteilen, da so eine größere
Transparenz des Programm-Codes garantiert werden kann. Dies erzeugt im ersten Wurf zwar einen
Mehraufwand, der sich jedoch mit dem Betrieb und der Weiterentwicklung der Software auszahlt. Ein
weiterer Vorteil ist, dass mit Einführung des Schichten-Modells beliebige Schichten einfach
ausgetauscht werden können, ohne dass die Funktion der verbleibenen Schichten geändert
werden muss. Für Kommentar-Funktion gilt dann:
-
Daten-Schicht:
Liest die Daten aus der Datenbank aus und stellt sie der nächst höheren Schicht, bzw.
nimmt Daten entgegen und speichert sie in der Datenbank.
-
Business-Schicht:
Sie kontrolliert die Funktion der Software, und steuert die Ein- und Ausgabe und den Ablauf der
Software.
-
Präsentations-Schicht:
Die Präsentations-Schicht kümmert sich um die Präsentation der Applikation zum
Kunden. Hierzu zählt die Ausgabe einer GUI (im Web-Umfeld HTML) und die damit verbundenen
Funktionen, jedoch nicht mehr.
Im objektorientierten Anwendungsdesign hat sich zu diesen Bereichen in der Vergangenheit ein einiger
Sprachgebrauch "eingebürgert", der auch hier verwendet werden soll. So wird bei der Datenschicht-
Komponente von einem Mapper gesprochen (siehe 2.2), bei der Business-Schicht von einem
Manager und in der Präsentations-Schicht je nach Art dieser von
View- bzw. hier von DocumentControllern gesprochen. Für diese
Begriffe existiert in der Regel eine jeweils eigene Klasse. In der Präsentations-Schicht kann
dies jedoch je nach Anzahl der Views und Anzeige-Module einer Applikation variieren. Im Fall der
Kommentar-Funktion wird die Software zwei Views mit jeweils einem Template und einem Controller
besitzen. Dieser Verfahrensweise begegnen Sie auch in den folgenden Tutorials.
2.2. (OR-)Data-Mapper
Ein Data-Mapper ist ein Vermittler zwischen der "Welt" der Datenbank und der "Welt"
der Anwendung. Das bedeutet, dass diese Komponente - wie bereits oben angedeutet - sich mit den
Spezifika der Datenbank beschäftigt und der Software eine einfache Schnittstelle bietet um auf
dieser Daten zu speichern und anschließend wieder auszulesen. Konkret wird der Mapper - eine
Komponente der Datenschicht - die zur Darstellung benöätigten Daten auslesen und im Format
der Anwendung bereitstellen, bzw. umgekehrt, einen neuen Kommentar speichern. Das Präfix
OR erhält der Mapper in diesem Fall deshalb, weil eine relationale Datenbank
zur Speicherung der Daten verwendet wird (MySQL) und die Anwendung jedoch objektorientiert
entworfen wird. Eine klassische damit verbundene Aufgabe des OR-Mappers ist es ein Ergebnis-Array
in ein Objekt der Anwendung (Domain-Object) zu mappen - zu übersetzen. Üblicherweise
implementiert der Mapper dazu eine Methode map{ObjectName}2DomainObject(),
wobei {ObjectName} jeweils durch den Namen des Domain-Objekts bzw. der Domain-Objekte (siehe 2.3)
ersetzt wird. Als Parameter trägt die Funktion ein Array und der Rückgabe-Wert ist immer
vom Typ Object {ObjectName} - das Domänen-Objekt. Darüber hinaus besitzt der
Mapper üblicherweise Methoden zum Lesen von Objekten per ID, bzw. per in der Anwendung benötigten
Schlüsseln und zum Schreiben dieser.
2.3. Domain-Object-Pattern
Das Domain-Object-Pattern besagt vereinfacht gesürochen, dass eine Anwendung
immer nur den Teil der in der Datenhaltung vorhandenen Objekte nutzen sollte, die für die Applikation
auch bestimmt ist. In einem großen Datenhaltungs-Konzept werden in der Regel erheblich mehr
Daten abgespeichert, als eine einzelne Anwendung benötigt. Nehmen wir an, in der für die
Kommentar-Funktion genutzen Datenbank werden neben Kommentaren noch Termine, Orte, Länder und
deren Beziehungen gespeichert, dann benötigt die Kommentar-Funktion nur den (kleinen) Teil der
Kommentare aus diesen Daten-Topf und es wäre überflüssig, wenn sich die Anwendung auch
mit den übrigen Objekten beschäftigen würde. Aus diesem Grund beschäftigt sich
jede Domäne - hier Kommentar-Funktion - nur mit den Domänen-Objekten ihres Interesses.
Die gesammte Applikation "kennt" damit ausschließlich diese Menge von Objekten und kann mit
diesen umgehen. Domain-Objekte sind üblicherweise Objekte der Business-Schicht und werden auch
in dem dafür vorgesehenen Ordner abgelegt. Wichtig ist hier jedoch, dass alle Schichten diese
Objekte "kennen". Die Datenschicht "schneidet" diese aus dem großen Pool an Daten "heraus" um
später den Extrakt wieder "einpflanzen" zu können, die Business-Schicht bildet an Hand der
Objekte ihren internen Ablauf ab und die Präsentations-Schicht nutzt diese zur Darstellung der
Benutzer-GUI nutzt.
2.4. MVC
Das Model-View-Controller-Pattern ist ein Entwurfsmuster der Präsentations-Schicht
und ist im Sinne der 3-Schicht-Architektur lediglich eine "Verfeinerung" des Präsentations-
Schicht-Designs. Es beschreibt in Zusammenarbeit mit der 3-Schicht-Architektur - wie auch
bereits unter Grundlagen aufgeführt wurde -
die Trennung zwischen dem dargestellten Inhalt und der Definition der Applikations-Workflows (Model),
der Beschreibung der Darstellung und des Aussehens der Applikation (View) und der eigentlichen
Darstellungs-Logik (Controller). Genau wie das 3-Schicht-Architektur-Pattern verspricht sich
der Entwickler auch hier mehr Flexibilität bei Erweiterung und Wartung der Applikation.
Im Fall der Kommentar-Funktion wird eine gemeinsame Business-Komponente (Model) verwendet, die aus
dem Manager und dem Domain-Objekt besteht. In diesen sind zum einen der Ablauf der
Applikation und zum anderen die Daten zur Ausgabe gespeichert. Zur weiteren Strukturierung der
Präsentations-Schicht werden zwei Unterordner angelegt, die später die Controller- und die
View-Dateien aufnehmen. So kann auf einen Blick bereits beurteilt werden, zu welchem Programmteil
welche Datei gehört.
MVC ist jedoch zunächst ein Pattern, das weitere Tools benötigt um dem Entwickler eine
sinnvolle Möglichkeit zu bieten Anwendungen zu verfassen ohne mit jeder Anwendung das Rad neu
erfinden zu müssen. Aus diesem Grund wurdem im Adventure-PHP-Framework weitere Pattern zum Design
der Präsentations-Schicht und zur Unterstützung des Entwicklers herangezogen.
2.5. PageController
Der PageController ist in Zusammenarbeit mit dem Composite-Pattern ein generisches Hilfmittel
für den Anwender, MVC-Applikationen zu entwickeln ohne den dazu notwendigen Rahmen bei jeder
Applikation neu erstellen zu müssen. Zudem bietet der PageController einen Mechanismus, wie der
Entwickler Module auf GUI-Ebene in eine bereits bestehende Applikation einklinken kann, die für
die Kommentar-Funktion von Wichtigkeit ist, da diese "einfach per Tag" in eine bereits bestehende
Artikel-Seite eingebunden wird.
3. Entwurf der Software
Im aktuellen Turorial soll auf einen detaillierten Entwurf der Software via UML verzichtet werden,
denn eine Kommentar-Funktion besitzt keine so hohe Komplexität, dass ein schriftliches Design
von Nöten wäre. Zudem erreicht man mit der unter Kapitel 2 erläuterten Struktur
bereits ein sehr generisches Anwendungsdesign, das sehr einfach erweitert werden kann.
4. Implementierung der Software
Für das Vorgehen bei der Implementierung der Software kann der Autor kein allgemeingültiges
Rezept ausstellen. Oft schickt es sich eine Applikation von oben nach unten, oft umgekehrt zu
entwickeln, in manchen Fällen zunächst den rein lesenden und anschließend erst den
schreibenden Teil zu erstellen. In diesem Fall zieht der Autor letztere vor um schnell ein anzeigbares
Ergebnis zu erhalten.
4.1. Struktur des Moduls
Zunächst soll die Ordner- bzw. Namespace-Struktur der Software angelegt werden. Da es sich bei
der Kommentar-Funktion gemäß Grundlagen
um ein Stück Software handelt, das in mehreren Projekten (=Webseiten) eingesetzt werden kann,
handelt es sich um ein Modul, das im Ordner /apps/modules abgelegt
wird. Dem Modul wird nun der Name comments gegeben, was gleichzeitig auch der
Ordner-Name sein muss. Für die oben genannten Schichten werden jeweils eigene Ordner (data,
biz, pres) angelegt und im Ordner pres nochmals weitere Unterordner
für die Controller (documentcontroller) und die Views (templates). Es ergibt
sich somit eine Ordnerstruktur von
/apps
/config
/core
/modules
/comments
/biz
/data
/pres
/documentcontroller
/templates
/sites
/tools
Da die Business-Schicht die Pager-Komponente nutzt müssen später weitere Strukturen unter
config angelegt werden, was aber zu gegebener Zeit erklärt wird.
4.2. Domain-Objekt
Als erste Klasse legen wir das von allen Schichten genutzte Domain-Objekt ArticleComment
an. Dazu wird die Datei ArticleComment.php im Ordner /apps/modules/comments/biz
erstellt un mit folgenden Inhalt gefüllt:
class ArticleComment extends coreObject {
var $__ID = null; var $__Name; var $__EMail; var $__Comment; var $__Date; var $__Time; var $__CategoryKey;
function ArticleComment(){ }
// end class }
Da die Klasse von coreObject erbt müssen keine get()- bzw. set()-Methoden
implementiert werden, da die Klasse coreObject bereits über abstrakte Methoden für
das Lesen und Setzen von privaten Klassen-Variablen besitzt, die später auch dafür genutzt
werden können. Um diese jedoch nutzen zu können muss die Namens-Konvention eingehalten
werden, die besagt, dass jede private und durch die Methoden get()- bzw. set() manipulierten
Klassen-Variablen mit $__{Name} benannt sein müssen, wobei {Name}
durch den jeweils erwünschten Namen ersetzt werden muss. Anschließend kann per
$AC = new ArticleComment(); $AC->set('Name','Max Mustermann'); echo $AC->get('Name');
die Ausgabe
Max Mustermann
erzeugt werden.
4.3. Datenschicht
Die Datenschicht besteht gemäß den Ausführungen unter Kapitel 2 als einem
DataMapper, der zunächst nur eine Lese-Funktion erhält. An dieser Stelle muss
vorweggenommen werden, dass innerhalb der Anwednung die Komponente "Pager" zum Einsatz kommt.
Diese läd an Hand von URL-Parametern IDs der gewünschten Datensätze aus der Datenbank
und erwartet, dass der Data-Mapper lediglich eine Funktion bereitstellt, die an Hand von IDs
Domänen-Objekte läd. Dazu erstellen wir eine Datei mit dem Namen der Klasse - plus Endung -
(commentMapper.php) unter /apps/modules/comments/data. Diese trägt
zunächst folgenden Inhalt:
import('modules::comments::biz','ArticleComment'); import('core::database','MySQLHandler');
class commentMapper extends coreObject {
function commentMapper(){ }
function loadArticleCommentByID($ArticleCommentID){
// SQL-Handler holen $SQL = &$this->__getServiceObject('core::database','MySQLHandler');
// Eintrag selektieren $select = 'SELECT ArticleCommentID, Name, EMail, Comment, Date, Time FROM article_comments WHERE ArticleCommentID = \''.$ArticleCommentID.'\';'; $result = $SQL->executeTextStatement($select);
// Objekt zurückgeben return $this->__mapArticleComment2DomainObject($SQL->fetchData($result));
// end function }
function __mapArticleComment2DomainObject($ResultSet){
// Neues Objekt erstellen $ArticleComment = new ArticleComment();
// ArticleCommentID if(isset($ResultSet['ArticleCommentID'])){ $ArticleComment->set('ID',$ResultSet['ArticleCommentID']); // end if }
// Name if(isset($ResultSet['Name'])){ $ArticleComment->set('Name',$ResultSet['Name']); // end if }
// EMail if(isset($ResultSet['EMail'])){ $ArticleComment->set('EMail',$ResultSet['EMail']); // end if }
// Comment if(isset($ResultSet['Comment'])){ $ArticleComment->set('Comment',$ResultSet['Comment']); // end if }
// Date if(isset($ResultSet['Date'])){ $ArticleComment->set('Date',$ResultSet['Date']); // end if }
// Time if(isset($ResultSet['Time'])){ $ArticleComment->set('Time',$ResultSet['Time']); // end if }
// Gefülltes Objekt zurückgeben return $ArticleComment;
// end function }
// end class }
Der Quellcode hat dabei folgende Bedeutung:
-
Die beiden import()-Befehle binden die benötigten Klassen ein. Hierzu gehört
das Domain-Objekt ArticleComment und die MySQL-Schnittstellen-Klasse MySQLHandler.
-
Die Methode loadArticleCommentByID() läd einen Kommentar an Hand einer ID aus der
Datenbank. Es wird hier auf eine Konfiguration für Feldnamen und Datenbank-Tabelle verzichtet,
da der commentMapper ohnehin eine konkrete Implementierung eines DataMappers für die
vorliegende Applikation ist. Die beschleunigt nicht nur das Lade-Verhalten, es ist zudem einfacher
zu entwickeln. Innerhalb dieser Methode wird vom MySQL-Wrapper gebrauch gemacht, über den eine
Result gezogen wird. Anschließend wird das Ergebnis abgeholt und der privaten Methode
__mapArticleComment2DomainObject() übergeben und ds Ergebnis an den Aufrufenden
zurückgegeben.
-
__mapArticleComment2DomainObject() ist die unter Kapitel 2 angesprochene Implementierung
einer Mapper-Methode, die ein Array in ein Domain-Objekt mappt.
Um die für dieses Modul notwenige Datenbank-Layout anlegen zu können, muss das im Ordner
/apps/modules/comments/data/scripts vorhandene SQL-Script init_comments.sql
ausgeführt werden. Für weitere, von Ihnen erstellte Anwendungen schickt es sich ebenso,
die notwenigen Datenbank-Initialisations-Skripte direkt in einem Ordner der Datenschicht abzulegen
um später nachvollziehen zu können, welche Anwendung, welche Tabellen der Datenbank nutzt.
4.4. Business-Schicht
Zur Business-Schicht zählt eine Manager-Klasse, die den Ablauf der Software regelt. Dazu legen
wir eine Datei mit dem Namen commentManager.php im Ordner /apps/modules/comments/biz
an. Im Fall der Kommentar-Funktion (lesender Zugriff) muss diese lediglich die Daten aus der Datenschicht beziehen
und diese zurück an die Präsentations-Schicht geben. Wie bereits oben angedeutet verwendet
die Kommentar-Funktion den Pager. Aus diesem Grund ist es notwendig weitere Mechanismen vorzusehen und
den Pager entsprechend zu konfigurieren. Das Grundgerüst der Klasse sieht jedoch wie folgt aus:
import('modules::pager::biz','pagerManager'); import('modules::comments::data','commentMapper'); import('modules::pager::biz','pagerManager'); import('tools::link','frontcontrollerLinkHandler');
class commentManager extends coreObject {
var $__CategoryKey;
function commentManager(){ }
function init($CategoryKey){ $this->__CategoryKey = $CategoryKey; // end function }
function loadEntries(){ }
// end class }
Der Quellcode hat dabei folgende Bedeutung:
-
Die import()-Befehle binden die benötigten Klassen ein. Hierzu gehört das
Domain-Objekt ArticleComment, die Datenschicht-Komponente commentMapper, der
PagerManager und der frontcontrollerLinkHandler, der später zur Generierung
eines Links für die Weiterleitung während des Speicher-Vorgangs genutzt wird.
-
Die Klassenvariable $__CategoryKey speichert die Katorgorie, dessen Kommentare
geladen werden sollen.
-
Um die Business-Komponente mit der Methode getAndInitServiceObject() verwenden
zu können muss diese eine init()-Methode implementieren, die die Klasse
initialisiert. In diesem Fall soll die Klasse mit der entsprechenden Kommentar-Kategorie bestückt
werden.
-
loadEntries() ist der Prototyp der Lade-Methode, die die Einträge an die
Präsentations-Schicht zurückgibt.
Kümmern wir uns nun um die zuletzt genannte Funktion. Die Komponente pagerManager hat
gemäß API-Dokumentation die Methoden
- setAnchorName() (Name des Ankers setzen)
- loadEntries() (IDs von Einträgen laden)
- getPager() (HTML-Ausgabe des Pagers erzeugen)
- getPagerURLParameters() (Ausgabe der URL-Parameter der aktuellen Pager-Konfiguration)
und eine Referenz auf einen Pager wird mit Hilfe der pagerManagerFabric bezogen. Der
Fabric muss dazu der Abschnitt der zu verwendenden Konfiguration und ein Initialisierungs-Parameter
mitgegeben werden. In PHP-Code funktioniert das wie folgt:
// Beziehen einer Singleton-Instanz der Fabric $pMF = &$this->__getServiceObject('modules::pager::biz','pagerManagerFabric');
// Beziehen und initialisieren des PagerManagers $pM = &$pMF->getPagerManager('ArticleComments',array('CategoryKey' => $this->__CategoryKey));
Damit wird ein PagerManager erzeugt, die Konfiguration aus dem Abschnitt ArticleComments
geladen und dieser mit dem Parameter CategoryKey und dem Wert aus der lokalen
Klassen-Variable $__CategoryKey gespeist. Das zweite Argument der Methode
getPagerManager() initialisiert die zusätzlichen SQL-Statement-Parameter,
die einem ID- oder Count-Statement mitgegeben werden können.
Die Konfiguration des pagerManager's ist unter dessen Namespace (modules::pager) und dem
aktuellen Context der Applikation abgelegt. Im Fall der Dokumentationsseite ist das sites::demosite.
Daraus ergibt sich der Ordner-Pfad /apps/config/modules/pager/sites/demosite. Dort
befindet sich eine Datei mit dem Namen DEFAULT_pager.ini mit ungefähr folgendem
Inhalt:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ArticleComments ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[ArticleComments]
; Anzahl Einträge pro Seite
Pager.EntriesPerPage = "5"
; Namen der URI-Parameter für Startpunkt und Anzahl/Seite
Pager.ParameterStartName = "PgrStr"
Pager.ParameterCountName = "PgrAnz"
; Namespace und Statements für Selektionen inkl. Konfiguration der Statements
Pager.StatementNamespace = "modules::comments"
Pager.CountStatement = "load_entries_count"
Pager.CountStatement.Params = "CategoryKey:standard"
Pager.EntriesStatement = "load_entry_ids"
Pager.EntriesStatement.Params = "CategoryKey:standard"
; Pager-Ausgabe Design
Pager.DesignNamespace = "modules::pager::pres::templates"
Pager.DesignTemplate = "pager_2"
Mit diesem Satz von Parametern kann der Pager vollständig konfiguriert werden. Einge davon
können für die meißten Anwendungen ohne Änderung belassen werden. Was jedoch
von Anwendungsfall zu Anwendungsfall variiert sind die Definition der Statements. Der Pager verlangt -
um seine Arbeit verrichten zu können - nach zwei SQL-Statements, die er zum Laden der Anzahl
der in der Datenbank befindlichen Datenstätze und zum Laden der für eine Seite relevanten
IDs verwendet. Diese Statements werden durch ihren Namespace und durch ihren Namen bestimmt, wobei
der Namespace der Ordner-Pfad - getrennt durch "::" statt "/" - nur demjenigen Pfad entspricht, der
den Context der Applikation nicht enthält. Der Context wird dem Pager dadurch übergeben,
dass dieser mit der PagerManagerFabric erzeugt wird. Die Statements-Dateien befinden sich im aktuellen
Anwendungsfall in einem Unterordner des Moduls "comments", da die Statements immer unter dem zugehörigen
Modul-Namespace abgelegt werden, nicht unter dem Pager-Namespace. Übersetzt man die oben
konfigurierten Ordner-Pfade, müssen die Statement-Dateien unter dem Ordner
/apps/config/modules/comments/sites/demosite/statements liegent. Der Ordner
statements wird vom Datenbank-Wrapper MySQLHandler verlangt. Da
Statements einer Konfiguration gleich kommen werden diese nicht nur unter dem config-Namespace
abgelegt, sondern auch so benannt. Wie unter Konfiguration
beschrieben, muss dem Dateinamen, der Wert der Umgebungsvariable vorangestellt und eine Endung
angehängt werden. Im Fall der SQL-Statements ist die Endung .sql. Die Dateien
müssen deshalb
- DEFAULT_load_entries_count.sql
- DEFAULT_load_entry_ids.sql
lauten. Der PagerManager erwartet eine weitere Konvention: die Benennung der Rückgabe-Variablen
und das Einfügen von Parametern. Am Beispiel des Entries-Count-Statement lässt sich das
einfach zeigen:
SELECT COUNT(*) AS EntriesCount
FROM article_comments
WHERE CategoryKey = '[CategoryKey]'
GROUP BY ArticleCommentID;
Der PagerManager erwartet intern immer ein Ergebnis in der Variable EntriesCount,
weswegen das Ergebnis des Select-Statements auch in diesen Alias geschrieben werden muss. Weitere,
der PagerManagerFaric zuvor übergebene Parameter können im Statement mit eckigen Klammern
eingeschlossen verwendet werden. Für das Load-Statement gelten folgende Regeln:
SELECT ArticleCommentID AS DB_ID
FROM article_comments
WHERE CategoryKey = '[CategoryKey]'
ORDER BY Date DESC, Time DESC
LIMIT [Start],[EntriesCount];
Auch hier erwartet der PagerManager die Ergebnis-IDs in der Variable DB_ID - ein Alias
ist notwenig. Weitere Statement-Parameter können einfach wie oben beschrieben verwendet werden.
Eine weitere Besonderheit ist die LIMIT-Anweisung. Diese muss die Parameter
enthalten, da diese mit internen Pager-Variablen gefüllt werden um jeweils nur diejenigen
Einträge auszugeben, die für die aktuelle Seite vorgesehen sind. Die restlichen Konstrukte
der Statements können beliebig komplex gewählt werden, solange die genannten Struktur-Elemente
eingehalten werden.
Kommen wir nun nach der Konfiguration des Pagers zu dessen Verwendung. Die Methode
loadEntries() des Managers gestaltet sich damit wie folgt:
function loadEntries(){
// pagerManager holen $pMF = &$this->__getServiceObject('modules::pager::biz','pagerManagerFabric'); $pM = &$pMF->getPagerManager('ArticleComments',array('CategoryKey' => $this->__CategoryKey));
// Kommentare laden $M = &$this->__getServiceObject('modules::comments::data','commentMapper'); return $pM->loadEntriesByAppDataComponent($M,'loadArticleCommentByID');
// end function }
Im ersten Schritt wird die gewünschte Pager-Instanz über die PagerManagerFabric
geholt, im zweiten Schritt wird eine Instanz des DataMappers der Applikation erzeugt. Diese wird dann
in darauf folgenden Schritt als Data-Provider dem PagerManager übergeben, damit dieser die
gewünschten Domänen-Objekte läd. Eine weitere Möglichkeit den Pager anzuwenden
ist sich zunächst die IDs der auf der aktuell anzuzeigenden Seite zu ziehen und dann im Manager
die Objekte per Schleife mit Hilfe des Mappers zu laden. Das sieht in PHP-Code wie folgt aus:
function loadEntries(){
// pagerManager holen $pMF = &$this->__getServiceObject('modules::pager::biz','pagerManagerFabric'); $pM = &$pMF->getPagerManager('ArticleComments',array('CategoryKey' => $this->__CategoryKey));
// Ids laden $EntryIDs = $pM->loadEntries();
// Kommentare laden $M = &$this->__getServiceObject('modules::comments::data','commentMapper');
$Entries = array();
for($i = 0; $i < count($EntryIDs); $i++){ $Entries[] = $M->loadArticleCommentByID($EntryIDs[$i]); // end for }
// Liste zurückgeben return $Entries;
// end function }
Beide Möglichkeiten werden angeboten, letztere ist für denjenigen Anwendungsfall gedacht,
bei dem die Business-Komponente noch Einfluss auf die Sortierung oder den Inhalt der Domain-Objects
haben möchte oder muss. Innerhalb der Kommentar-Funktion wird die zu erst genannte Variante
eingesetzt in den folgenden Tutorials die zuletzt genannte. Damit wäre der Teil der lesenden
Anwendung für die Business-Schicht abgeschlossen.
4.5. Präsentations-Schicht
Die Ausgabe der Kommentare in einer blätterbaren Seite beinhaltet neben der Einbindung des Moduls
lediglich einen View - die Ausgabe-Liste. Widmen wir uns zunächst der Einbindung, da diese mit
einer Besonderheit ausgestattet werden soll. Wie bereits weiter oben angesprochen, soll es möglich
sein, die Kommentar-Funktion in eine bestehende Seite per XML-Tag einbindbar zu machen. Dazu nutzen
wir in diesem Anwendungsfall das <core:importdesign />-Tag. Dieses trägt
laut Standard-TagLibs die XML-Parameter
namespace um den Namespace des Templates zu deklarieren, template
für die Benennung des Templates und den optionalen Parameter incparam um den
URL-Parameter, der die Einbindung steuert zu benennen. Letzterer soll auch hier zum Einsatz kommen,
da der Standard-Parameter "pagepart" unter Umständen bereits in Gebrauch sein könnte
und die Ausführung der Funktion stören könnte. Weiterhin soll der Template-Bauer, der
die Funktion einbaut entscheiden können, welche Kommentare angezeigt werden. Dazu führen
wir einen neuen Parameter categorykey ein, der beschreiben soll, welche Kategorie
von Kommentaren hier eingetragen werden soll und angezeigt werden kann. Dieser Mechanismus wurde oben
bereits ohne Kommentar vorgesehen, ist aber - mit dem Wissen der hier verfassten Zeilen - notwenig
um eine Unterscheidung der Kommentare vorzusehen. Datenbank-technisch ist sicher auch eine andere
Möglichkeit der Kategorisierung denkbar, es handelt sich jedoch um eine einfaches Beispiel, bei
dem von einem komplexen Datenbank-Design abgesehen wird.
Die Einbindung in ein bestehendes Template kann damit mit einem
<core:importdesign
namespace="modules::comments::pres::templates"
template="comment"
categorykey="****"
/>
erledigt werden. Im dort genannten Template wird nun der Kopf des Moduls durch
<a name="comments" /><h2>Kommentare</h2>
beschrieben und mit
<core:importdesign
namespace="modules::comments::pres::templates"
template="[coview = listing]"
incparam="coview"
/>
anschließend der gerade angefragte View (Liste oder Formular) an Hand des URL-Parameters
coview (coview für CommentView) eingebunden. Für die Ausgabe der Liste
soll nun Template listing erstellt werden:
<@controller namespace="modules::comments::pres::documentcontroller" file="comment_listing_v1_controller" class="comment_listing_v1_controller" @>
<core:addtaglib namespace="tools::html::taglib" prefix="html" class="getstring" />
<html:getstring namespace="modules::comments" config="language" entry="listing.text.1" /> <a href="<html:placeholder name="Link" />#comments" title="<html:getstring namespace="modules::comments" config="language" entry="listing.text.2.title" />"><strong><html:getstring namespace="modules::comments" config="language" entry="listing.text.2" /></strong></a> <html:getstring namespace="modules::comments" config="language" entry="listing.text.3" />
<br />
<br />
<html:placeholder name="Pager" />
<html:placeholder name="Content" />
<html:template name="ArticleComment">
<table cellspacing="0" cellpadding="0" style="border: 0px solid black; width: 100%;">
<tr>
<td style="width: 60px; text-align: left; padding: 2px;">
<font style="font-size: 36px; color: #777BB4; font-style: italic;">
<template:placeholder name="Number" />
</font>
</td>
<td style="padding: 2px; font-size: 12px; font-family: Arial, Helvetica, sans-serif;">
<strong><template:placeholder name="Name" /></strong>
<br />
<em><template:placeholder name="Date" />, <template:placeholder name="Time" /></em>
</td>
</tr>
</table>
<div style="background-color: #eeeeee; width: 100%; padding: 2px;">
<template:placeholder name="Comment" />
</div>
<br />
</html:template>
<html:template name="NoEntries">
<template:addtaglib namespace="tools::html::taglib" prefix="template" class="getstring" />
<br />
<span style="color: #777BB4; font-style: italic; margin-left: 30px; font-weight: bold;"><template:getstring namespace="modules::comments" config="language" entry="noentries.text" /></span>
<br />
<br />
<br />
</html:template>
<html:template name="Deactivated">
<template:addtaglib namespace="tools::html::taglib" prefix="template" class="getstring" />
<br />
<span style="color: #777BB4; font-style: italic; margin-left: 30px; font-weight: bold;"><template:getstring namespace="modules::comments" config="language" entry="deactivated.text" /></span>
<br />
<br />
<br />
</html:template>
Dieses sieht die Definition des zu verwendenden Controllers, einen Einleitungstext, Platz für
die Ausgabe eines Pagers und die Ausgabe der Liste und ein Templates für einen Eintrag und die
Anzeige der Meldung, dass keine Einträge vorhanden sind. Die Beschreibung der verwendeten Tags
kann unter Standard-TagLibs
nachgelesen werden. Interessanter ist nun die Gestaltung des DocumentControllers für die Ausgabe
der Liste.
An dieser Stelle soll jedoch zunächst ein kleiner Exkurs in die Struktur des GUI-Designs des
Adventure-PHP-Frameworks eingeschoben werden um die Funktion der Tags am Beispiel zu erläutern.
Der PageController erzeugt aus jedem Template einen eigenen DOM-Knoten im Objektbaum der Oberfläche.
Durch Referenzen eines Knotens auf seinen Vater-Knoten können ausgehend von einem beliebigen
DOM-Objekt die Eigenschaften des Vater- und/oder Kind-Objekts ausgelesen werden. Diese Eigenschaft
machen wir uns bei der Ausgabe der Liste und später dem Eintragen eines Kommentars zu Nutze.
Die aktuelle Struktur sieht vor, dass in einem beliebigen Template die Kommentar-Funktion durch ein
parametrisiertes <core:importdesign />-Tag eingebunden wird. Dieses wiederum
bindet ein weiteres Template ein, das für Ausgabe der Liste oder Anzeige des Menüs
zuständig ist. Letztere benötigen jedoch Zugriff auf das im "Haupt-Template" gesetzte
Tag-Attribut um die richtigen Kommentare ausgeben, oder in die richtige Kategorie eintragen zu
können. Gemäß dem DOM-Modell ist das innerhalb eines DocumentControllers, der wiederum
eine Referenz auf das ihm zugewiesene Document-Objekt besitzt, durch den folgenden PHP-Code
möglich:
$DocParent = &$this->__Document->getByReference('ParentObject'); $this->__CategoryKey = $DocParent->getAttribute('categorykey');
Um bei fehlenden Attribut keine Fehlermeldung zu bekommen wird der Code noch um
if($CategoryKey == null){ $this->__CategoryKey = 'standard'; // end if } else{ $this->__CategoryKey = $CategoryKey; // end else }
ergänzt. Da diese Funktion in beiden DocumentControllern wichtig ist (Erzeugen der Liste, Eintragen
eines Kommentars) wird diese Funktion in einen gemeinsamen Basis-DocumentController verpackt. Dieser
wird mit commentBaseController benannt und unter
/apps/modules/coments/pres/documentcontroller abgelegt. Der abstrakte Controller wird
zudem für die Einbindung des Domain-Objekts und des Managers verwendet. Alle konkreten Controller
erhalten diese Funktion nun automatisch dadurch, dass diese vom commentBaseController erben.
Der konkrete DocumentController comment_listing_v1_controller (siehe Template) wird
nun mit den Aufgaben betraut, die gewünschten Einträge und den Pager, bzw. ohne Kommentare
eine Meldung auszugeben. In Quellcode gegossen kann das so gelöst werden:
import('modules::comments::pres::documentcontroller','commentBaseController'); import('tools::datetime','dateTimeManager'); import('tools::string','bbCodeParser'); import('tools::link','frontcontrollerLinkHandler');
class comment_listing_v1_controller extends commentBaseController {
function comment_listing_v1_controller(){ }
function transformContent(){
// get category key $this->__loadCategoryKey();
// get data layer component $M = &$this->__getAndInitServiceObject('modules::comments::biz','commentManager',$this->__CategoryKey);
// load entries $Entries = $M->loadEntries();
$Buffer = (string)''; $Template__ArticleComment = &$this->__getTemplate('ArticleComment'); $bbCP = &$this->__getServiceObject('tools::string','bbCodeParser');
for($i = 0; $i < count($Entries); $i++){
// fill template $Template__ArticleComment->setPlaceHolder('Number',$i + 1); $Template__ArticleComment->setPlaceHolder('Name',$Entries[$i]->get('Name')); $Template__ArticleComment->setPlaceHolder('Date',dateTimeManager::convertDate2Normal($Entries[$i]->get('Date'))); $Template__ArticleComment->setPlaceHolder('Time',$Entries[$i]->get('Time')); $Template__ArticleComment->setPlaceHolder('Comment',$bbCP->parseText($Entries[$i]->get('Comment')));
// add template to list $Buffer .= $Template__ArticleComment->transformTemplate();
// end for }
// display hint in cas of no entries if(count($Entries) < 1){ $Template__NoEntries = &$this->__getTemplate('NoEntries'); $Buffer = $Template__NoEntries->transformTemplate(); // end if }
// display list $this->setPlaceHolder('Content',$Buffer);
// display pager $this->setPlaceHolder('Pager',$M->getPager('comments'));
// get pager url params $URLParameter = $M->getURLParameter();
// generate create entry link $this->setPlaceHolder( 'Link', frontcontrollerLinkHandler::generateLink( $_SERVER['REQUEST_URI'], array( $URLParameter['StartName'] => '', $URLParameter['CountName'] => '', 'coview' => 'form' ) ) );
// end function }
// end function }
Besonderheiten der Umsetzung sind das Holen des Kategorie-Schlüssels zu Beginn der Methode
transformContent() und die dynamische Generierung des Eintragen-Links, da das Modul selbst
nicht weiß, in welchem Bereich der Applikation es eingebunden ist. Für letztere Aufgabe
wird die Business-Komponente um die Rückgabe der URL-Parameter gebeten, die in der Konfiguration
unter 4.3. / Datei DEFAULT_pager.ini definiert worden sind. Die Parameter werden dann aus
kosmetischen Gründen zurückgesetzt, damit der Pager beim Aufruf der Listing-Seite nach dem
Eintragen immer auf die erste Seite springt. Dies könnte auch innerhalb des Managers abgebildet
werden, wurde jedoch in den DocumentController verlagert, da hier ohnehin ein dynamischer Link zum
Formular platziert werden muss. Im Fall der Kommentar-Funktion wird präventiv der
frontcontrollerLinkHandler verwendet, obwohl die Funktion des linkHandler
ausreichend wäre, da nicht mit Sicherheit davon ausgegangen werden kann, dass die aktuell generierte
Seite nicht mit Hilfe von FrontController-Actions generiert wurde. Zur Formatierung der Ausgabe
werden der bbCodeParser und der dateTimeManager eingesetzt.
5. Erweiterung der Software
Die Software, wie sie bis Ende des vierten Kapitels beschrieben wurde, könnte nun alle per
PHPMyAdmin erzeugte Kommentare einer Kategorie auf einer Seite ausgeben, in die das Modul eingebunden
ist. Ein Eintragen ist jedoch nicht möglich. Aus diesem Grund soll die Software um das Eintragen
von Kommentaren schrittweise erweitert werden. Dazu gehen wir nun in der umgekehrten Reihenfolge
vor und Beginnen mit der Präsentations-Schicht.
5.1. Präsentations-Schicht
Unter 4.4. haben wir bereits die Möglichkeit vorgesehen einen durch den URL-Parameter
coview gesteuerten View einzublenden. Der neu erstellte View für das Formular
soll form heißen. Wie das Domänen-Objekt bereits vorgibt sollen vom
Benutzer die Eingaben
abgefragt werden. Das Formular gestaltet sich dann in XML-Tags ausgedrückt wie folgt:
<html:form name="AddComment" method="post">
<span style="margin-right: 10px;">Name:</span><form:text maxlength="100"
name="Name" value="" class="eingabe_feld" style="width: 390px;" validate="true"
button="Speichern" validator="Text" />
<br />
<span style="margin-right: 10px;">E-Mail:</span><form:text maxlength="100"
name="EMail" value="" class="eingabe_feld" style="width: 390px;" validate="true"
button="Speichern" validator="EMail" />
<br />
<br />
Kommentar:
<br />
<form:area name="Comment" class="eingabe_feld" style="width: 438px; height: 120px; overflow: auto;"
validate="true" button="Speichern" validator="Text" />
<br />
<br />
<form:button name="Speichern" value="Speichern" class="eingabe_feld" />
</html:form>
Mit den Attributen validate="true" und button="Speichern" wird
die Formular-Validierung für dieses Feld aktiviert und mit validator="Text" bzw.
validator="EMail" wird die Art und Weise festgelegt, mit der das Feld validiert werden
soll. Mit ein wenig Text versehen hat das Template form damit folgenden Inhalt:
<@controller namespace="modules::comments::pres::documentcontroller" file="comment_form_v1_controller" class="comment_form_v1_controller" @>
<core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" />
<core:addtaglib namespace="tools::html::taglib" prefix="html" class="getstring" />
<html:getstring namespace="modules::comments" config="language" entry="formhint.text.1" /> <a href="<html:placeholder name="Zurueck" />#comments" title="<html:getstring namespace="modules::comments" config="language" entry="formhint.text.2.title" />"><strong><html:getstring namespace="modules::comments" config="language" entry="formhint.text.2" /></strong></a><html:getstring namespace="modules::comments" config="language" entry="formhint.text.3" />
<br />
<br />
<html:placeholder name="Form"/>
<html:form name="AddComment" method="post">
<span style="margin-right: 10px;"><form:getstring namespace="modules::comments" config="language" entry="form.name" />*</span><form:text maxlength="100" name="Name" value="" class="eingabe_feld" style="width: 390px;" validate="true" button="Save" validator="Text" />
<br />
<span style="margin-right: 10px;"><form:getstring namespace="modules::comments" config="language" entry="form.email" />*</span><form:text maxlength="100" name="EMail" value="" class="eingabe_feld" style="width: 390px;" validate="true" button="Save" validator="EMail" />
<br />
<br />
<form:getstring namespace="modules::comments" config="language" entry="form.comment" />
<br />
<form:area name="Comment" class="eingabe_feld" style="width: 441px; height: 120px; overflow: auto;" validate="true" button="Save" validator="Text" />
<br />
<br />
<span style="margin-right: 10px;"><form:getstring namespace="modules::comments" config="language" entry="form.confirm" />*</span>
<br />
<br />
<img src="<form:placeholder name="CaptchaImage" />" border="0" align="absmiddle" />
<form:text name="CaptchaString" class="eingabe_feld" style="margin-left: 10px; width: 60px;" validate="true" button="Save" validator="Text" maxlength="5" />
<br />
<br />
<form:button name="Save" class="eingabe_feld" />
</html:form>
Der zugehörige DocumentController (comment_form_v1_controller) hat dabei die
Aufgabe das Formular in den dafür vorgesehenen Platzhalter einzusetzen und im Fall eines
abgeschickten und validen Formulars den Eintrag mit Hilfe der Business-Komponente zu speichern. Dazu
implementiert dieser eine neue Methode saveEntry(), dem ein Domain-Objekt übergeben
werden muss. Hier der Controller in der Übersicht:
import('modules::comments::pres::documentcontroller','commentBaseController'); import('tools::variablen','variablenHandler'); import('modules::comments::biz','commentManager'); import('tools::link','frontcontrollerLinkHandler'); import('tools::string','stringAssistant');
class comment_form_v1_controller extends commentBaseController {
var $_LOCALS = array();
function comment_form_v1_controller(){ $this->_LOCALS = variablenHandler::registerLocal(array('Name','EMail','Comment','CaptchaString')); // end function }
function transformContent(){
// Referenz auf das Formular holen $Form__AddComment = &$this->__getForm('AddComment');
// Prüfen, ob Formular abgesendet und erforlgreich validiert wurde if($Form__AddComment->get('isSent') == true){
// Kategorie-Schlüssel laden $this->__loadCategoryKey();
// Mapper holen $M = &$this->__getAndInitServiceObject('modules::comments::biz','commentManager',$this->__CategoryKey);
// Validieren des Captchas $CaptchaString = $M->get('CaptchaString');
if($CaptchaString != $this->_LOCALS['CaptchaString']){ $Captcha = &$Form__AddComment->getFormElementByName('CaptchaString'); $Captcha->set('isValid',false); $Form__AddComment->set('isValid',false); // end if }
// Prüfen, ob Formular korrekt ausgefüllt wurde if($Form__AddComment->get('isValid') == true){
// Eintrag erstellen $ArticleComment = new ArticleComment(); $ArticleComment->set('Name',$this->_LOCALS['Name']); $ArticleComment->set('EMail',$this->_LOCALS['EMail']); $ArticleComment->set('Comment',$this->_LOCALS['Comment']);
// Eintrag speichern $M->saveEntry($ArticleComment);
// end if } else{ $this->__buildForm(); // end else }
// end if } else{ $this->__buildForm(); // end else }
// end function }
function __buildForm(){
// Referenz auf das Formular holen $Form__AddComment = &$this->__getForm('AddComment');
// action setzen $Form__AddComment->setAttribute('action',$_SERVER['REQUEST_URI'].'#comments');
// Button beschriften $Config = &$this->__getConfiguration('modules::comments','language'); $Button = &$Form__AddComment->getFormElementByName('Save'); $Button->setAttribute('value',$Config->getValue($this->__Language,'form.button'));
// CaptchaImage füllen $Reg = &Singleton::getInstance('Registry'); $URLRewriting = $Reg->retrieve('apf::core','URLRewriting'); if($URLRewriting === true){ $Form__AddComment->setPlaceHolder('CaptchaImage','/~/modules_comments-action/showCaptcha'); // end if } else{ $Form__AddComment->setPlaceHolder('CaptchaImage','./?modules_comments-action:showCaptcha'); // end else }
// Formular darstellen $this->setPlaceHolder('Form',$Form__AddComment->transformForm());
// Zurücklink darstellen $Link = frontcontrollerLinkHandler::generateLink($_SERVER['REQUEST_URI'],array('coview' => 'listing')); $this->setPlaceHolder('Zurueck',$Link);
// end function }
// end class }
5.2. Business-Schicht
In diesem Kapitel stellt sich nun die Aufgabe, die zuvor beschriebene Business-Schicht-Methode
saveEntry() mit Leben zu füllen. Im Wesentlichen besteht die Aufgabe darin, den neuen
Datensatz zu speichern und den Ausgabe-View anzuzeigen. Wie auch beim Laden der Daten muss dazu die
Datenschicht-Komponente herangezogen werden. Dieser schreiben wir nun - ohne diese bereits
implementiert zu haben - eine Methode saveArticleComment() zu, die wir in der
Business-Schicht zur Speicherung des neuen Kommentars nutzen. Die Weiterleitung erledigt eine einfache
Weiterleitung auf den Anzeigen-View. Hier muss natürlich darauf geachtet werden, dass die erzeugte
Seite auch wieder korrekt angezeigt wird.
function saveEntry($ArticleComment){
// Mapper holen $M = &$this->__getServiceObject('modules::comments::data','commentMapper');
// Artikel speichern $ArticleComment->set('CategoryKey',$this->__CategoryKey); $M->saveArticleComment($ArticleComment);
// Auf die Ausgabe weiterleiten $Link = frontcontrollerLinkHandler::generateLink($_SERVER['REQUEST_URI'],array('coview' => 'listing')); header('Location: '.$Link.'#comments');
// end function }
Wie Zeile 6 ($ArticleComment->set('CategoryKey'..) zeigt manipuliert die Business-Schicht
das Domain-Objekt, damit dieses mir der korrekten Kategorie gespeichert wird. Anschließend
wird - wie bereits für die Generierung des Links für das Formular - der
frontcontrollerLinkHandler für die Zusammensetzung der Weiterleitungs-URL verwendet.
5.3. Datenschicht
Die Datenschicht muss nun noch die Methode saveArticleComment() implementieren.
function saveArticleComment($ArticleComment){
// SQL-Handler holen $SQL = &$this->__getServiceObject('core::database','MySQLHandler');
// Prüfen, ob Artikel bereits existiert if($ArticleComment->get('ID') == null){
$insert = 'INSERT INTO article_comments (Name, EMail, Comment, Date, Time, CategoryKey) VALUES (\''.$ArticleComment->get('Name').'\', \''.$ArticleComment->get('EMail').'\', \''.$ArticleComment->get('Comment').'\', CURDATE(), CURTIME(), \''.$ArticleComment->get('CategoryKey').'\');'; $SQL->executeTextStatement($insert);
// end if }
// end function }
Die Bedeutung der Code-Zeilen lässt sich leicht erschließen. Zunächst wird eine
Singleton-Instanz des MySQLHandler über die Methode __getServiceObject()
bezogen und anschließend wird nach Prüfung ob es sich um einen neuen Kommentar handelt
wieder in seine flache relationale Strukur zerlegt und per SQL-Statament gespeichert.
6. Ausblick / Ergänzung
An dieser Stelle möchte der Autor nochmals darauf hinweisen, dass die Methoden
__getServiceObject() und __getAndInitServiceObject() immer dann
Anwendung finden müssen, wenn das damit erzeugte Service-Layer Context-abhängige Konfigurationen
läd oder ein Layer instanziiert, die weitere Context-abhängige Komponenten verwenden. Im
Datenbank-gestützten Applikations-Design sollten deshalb grundsätzlich die beiden Methoden
verwendet werden, da in letzter Konsequenz immer der MySQLHandler verwendet wird.
Kommentare
Möchten Sie den Artikel eine Anmerkung hinzufügen, oder haben Sie ergänzende Hinweise? Dann können Sie diese hier einfügen. Die bereits verfassten Anmerkungen und Kommentare finden Sie in der untenstehenden Liste.
Für diesen Artikel liegen aktuell keine Kommentare vor.
|