Kommentar-Funktion
Diese Seite beinhaltet veraltete Komponenten, die nicht mehr im
Release 1.11 enthalten sind. Die Seite
befindet sich deshalb bis zum Entfernen dieser Meldung in einem Bearbeitungszustand. Sollten
Fragen auftauchen, können diese gerne im
Forum gestellt werden.
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.
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:
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.
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 O/R-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.
Das
Domain-Object-Pattern besagt vereinfacht gesprochen, 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 gesamte 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.
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.
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.
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.
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.
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
Code
/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.
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:
PHP-Code
class ArticleComment extends APFObject {
protected $__ID = null;
protected $__Name;
protected $__EMail;
protected $__Comment;
protected $__Date;
protected $__Time;
protected $__CategoryKey;
}
Da die Klasse von
APFObject erbt müssen keine get()- bzw. set()-Methoden
implementiert werden, da die Klasse
APFObject 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
PHP-Code
$AC = new ArticleComment();
$AC->set('Name','Max Mustermann');
echo $AC->get('Name');
die Ausgabe
APF-Template
Max Mustermann
erzeugt werden.
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:
PHP-Code
import('modules::comments::biz','ArticleComment');
import('core::database','ConnectionManager');
class commentMapper extends APFObject {
function loadArticleCommentByID($articleCommentID){
// Datenbank-Verbindung aufbauen
$SQL = &$this->__getConnection();
// Eintrag selektieren
$select = 'SELECT ArticleCommentID, Name, EMail, Comment, Date, Time
FROM article_comments
WHERE ArticleCommentID = \''.$ArticleCommentID.'\';';
$result = $SQL->executeTextStatement($select);
// Ergebnis in ein Domain-Objekt mappen
return $this->__mapArticleComment2DomainObject($SQL->fetchData($result));
}
function __mapArticleComment2DomainObject($ResultSet){
$ArticleComment = new ArticleComment();
if(isset($ResultSet['ArticleCommentID'])){
$ArticleComment->set('ID',$ResultSet['ArticleCommentID']);
}
if(isset($ResultSet['Name'])){
$ArticleComment->set('Name',$ResultSet['Name']);
}
if(isset($ResultSet['EMail'])){
$ArticleComment->set('EMail',$ResultSet['EMail']);
}
if(isset($ResultSet['Comment'])){
$ArticleComment->set('Comment',$ResultSet['Comment']);
}
if(isset($ResultSet['Date'])){
$ArticleComment->set('Date',$ResultSet['Date']);
}
if(isset($ResultSet['Time'])){
$ArticleComment->set('Time',$ResultSet['Time']);
}
return $ArticleComment;
}
private function &__getConnection(){
$cM = &$this->__getServiceObject('core::database','ConnectionManager');
$config = $this->__getConfiguration('modules::comments','comments');
$connectionKey = $config->getValue('Default','Database.ConnectionKey');
return $cM->getConnection($connectionKey);
}
}
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.
-
Die Funktion __getConnection() liefert die für das Modul konfigurierte
Datenbank-Verbindung zurück.
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.
Die Konfigurationsdatei
{ENVIRONMENT}_comments.ini beinhaltet eine Referenz auf eine
Datenbank-Verbindung. Letztere findet sich in der globalen Datenbank-Konfiguration im Namespace
core::database. Details zur Konfiguration von Datenbank-Verbindungen können dem Kapitel
ConnectionManager
entnommen werden.
Der hier abgedruckte Quellcode enthält keine Sicherungen für SQL-Injections. Diese
sollten zur eigenen Sicherheit in echten Applikationen enthalten sein. Hierzu bietet die Klasse
MySQLxHandler die Methode
escapeValue(). Diese maskiert Steuerzeichen und
verhindert so in Verbindung mit den
Input-Filtern eine
mögliche Sicherheitslücke. Mehr zum Thema Sicherheit und den im Framework eingebauten
Maßnahmen findet sich unter
Security.
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:
PHP-Code
import('modules::pager::biz','PagerManagerFabric');
import('modules::comments::data','commentMapper');
class commentManager extends APFObject {
private $__CategoryKey;
function init($CategoryKey){
$this->__CategoryKey = $CategoryKey;
}
function loadEntries(){
}
}
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:
PHP-Code
// Beziehen einer Singleton-Instanz der Fabric
$pMF = &$this->__getServiceObject('modules::pager::biz','PagerManagerFabric');
// Beziehen und initialisieren des PagerManagers
$pM = &$pMF->getPagerManager('ArticleComments');
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:
APF-Konfiguration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ArticleComments ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[ArticleComments]
; Datenbank ConnectionKey
Pager.DatabaseConnection = "..."
; 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:
Code
SELECT COUNT(*) AS EntriesCount
FROM article_comments
WHERE CategoryKey = '[CategoryKey]'
GROUP BY CategoryKey;
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:
Code
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:
PHP-Code
function loadEntries(){
// pagerManager holen
$pMF = &$this->__getServiceObject('modules::pager::biz','PagerManagerFabric');
$pM = &$pMF->getPagerManager('ArticleComments');
// Kommentare laden
$M = &$this->__getServiceObject('modules::comments::data','commentMapper');
return $pM->loadEntriesByAppDataComponent($M,'loadArticleCommentByID',array('CategoryKey' => $this->__CategoryKey));
}
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:
PHP-Code
function loadEntries(){
// pagerManager holen
$pMF = &$this->__getServiceObject('modules::pager::biz','PagerManagerFabric');
$pM = &$pMF->getPagerManager('ArticleComments');
// Ids laden
$EntryIDs = $pM->loadEntries(array('CategoryKey' => $this->__CategoryKey));
// Kommentare laden
$M = &$this->__getServiceObject('modules::comments::data','commentMapper');
$Entries = array();
for($i = 0; $i < count($EntryIDs); $i++){
$Entries[] = $M->loadArticleCommentByID($EntryIDs[$i]);
}
return $Entries;
}
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.
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
APF-Template
<core:importdesign
namespace="modules::comments::pres::templates"
template="comment"
categorykey="****"
/>
erledigt werden. Im dort genannten Template wird nun der Kopf des Moduls durch
APF-Template
<a name="comments" /><h2>Kommentare</h2>
beschrieben und mit
APF-Template
<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:
APF-Template
<@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" />
<div class="cm--list">
<div class="cm--list-head">
<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" />
</div>
<div class="cm--list-pager">
<html:placeholder name="Pager" />
</div>
<div class="cm--list-items">
<html:placeholder name="Content" />
</div>
</div>
<html:template name="ArticleComment">
<div class="cm--list-item">
<div class="cm--list-item-head">
<div class="cm--list-item-head-num"><template:placeholder name="Number" /></div>
<div class="cm--list-item-head-date">
<span><template:placeholder name="Name" /></span>
<em><template:placeholder name="Date" />, <template:placeholder name="Time" /></em>
</div>
</div>
<div class="cm--list-item-body">
<template:placeholder name="Comment" />
</div>
</div>
</html:template>
<html:template name="NoEntries">
<template:addtaglib namespace="tools::html::taglib" prefix="template" class="getstring" />
<div class="cm--list-noentries">
<template:getstring namespace="modules::comments" config="language" entry="noentries.text" />
</div>
</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:
PHP-Code
$DocParent = &$this->__Document->getParentObject();
$this->__CategoryKey = $DocParent->getAttribute('categorykey');
Um bei fehlenden Attribut keine Fehlermeldung zu bekommen wird der Code noch um
PHP-Code
if($CategoryKey == null){
$this->__CategoryKey = 'standard';
}
else{
$this->__CategoryKey = $CategoryKey;
}
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:
PHP-Code
class comment_listing_v1_controller extends commentBaseController {
function transformContent(){
$this->__loadCategoryKey();
$M = &$this->__getAndInitServiceObject('modules::comments::biz','commentManager',$this->__CategoryKey);
// load the entries using the business component
$entries = $M->loadEntries();
$buffer = (string)'';
$template = &$this->__getTemplate('ArticleComment');
// init bb code parser (remove some provider, that we don't need configuration files)
$bP = &$this->__getServiceObject('tools::string','AdvancedBBCodeParser');
$bP->removeProvider('standard.font.color');
$bP->removeProvider('standard.font.size');
for($i = 0; $i < count($entries); $i++){
$template->setPlaceHolder('Number',$i + 1);
$template->setPlaceHolder('Name',$entries[$i]->get('Name'));
$template->setPlaceHolder('Date',dateTimeManager::convertDate2Normal($entries[$i]->get('Date')));
$template->setPlaceHolder('Time',$entries[$i]->get('Time'));
$template->setPlaceHolder('Comment',$bP->parseCode($entries[$i]->get('Comment')));
$buffer .= $template->transformTemplate();
}
// display hint, if no entries are to display
if(count($entries) < 1){
$Template__NoEntries = &$this->__getTemplate('NoEntries');
$buffer = $Template__NoEntries->transformTemplate();
}
// display the list
$this->setPlaceHolder('Content',$buffer);
// display the pager
$this->setPlaceHolder('Pager',$M->getPager('comments'));
// get the pager url params from the business component
// to be able to delete them from the url.
$urlParams = $M->getURLParameter();
// generate the add comment link
$this->setPlaceHolder(
'Link',
FrontcontrollerLinkHandler::generateLink(
$_SERVER['REQUEST_URI'],
array(
$urlParams['StartName'] => '',
$urlParams['CountName'] => '',
'coview' => 'form'
)
)
);
}
}
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.
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.
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:
APF-Template
<html:form name="AddComment" method="post">
<span>
<form:getstring namespace="modules::comments" config="language" entry="form.name" />*
</span>
<form:text
maxlength="100"
name="Name"
class="cm--create-element-name"
validate="true"
button="Save"
validator="Text"
/>
<br />
<span>
<form:getstring namespace="modules::comments" config="language" entry="form.email" />*
</span>
<form:text
maxlength="100"
name="EMail"
class="cm--create-element-email"
validate="true"
button="Save"
validator="EMail"
/>
<br />
<br />
<form:getstring namespace="modules::comments" config="language" entry="form.comment" />
<br />
<form:area
name="Comment"
class="cm--create-element-comment"
validate="true"
button="Save"
validator="Text"
/>
<br />
<br />
<span>
<form:getstring namespace="modules::comments" config="language" entry="form.confirm" />*
</span>
<br />
<br />
<form:addtaglib namespace="modules::captcha::pres::taglib" prefix="form" class="captcha" />
<form:captcha
text_class="cm--create-element-captcha"
validate="true"
button="Save"
clearonerror="true"
/>
<br />
<br />
<form:button name="Save" class="cm--create-element-button" />
</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:
APF-Template
<@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" />
<div class="cm--create">
<div class="cm--create-head">
<html:getstring namespace="modules::comments" config="language" entry="formhint.text.1" /> <a href="<html:placeholder name="back" />#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" />
</div>
<div class="cm--create-form">
<html:form name="AddComment" method="post">
<span>
<form:getstring namespace="modules::comments" config="language" entry="form.name" />*
</span>
<form:text
maxlength="100"
name="Name"
class="cm--create-element-name"
validate="true"
button="Save"
validator="Text"
/>
<br />
<span>
<form:getstring namespace="modules::comments" config="language" entry="form.email" />*
</span>
<form:text
maxlength="100"
name="EMail"
class="cm--create-element-email"
validate="true"
button="Save"
validator="EMail"
/>
<br />
<br />
<form:getstring namespace="modules::comments" config="language" entry="form.comment" />
<br />
<form:area
name="Comment"
class="cm--create-element-comment"
validate="true"
button="Save"
validator="Text"
/>
<br />
<br />
<span>
<form:getstring namespace="modules::comments" config="language" entry="form.confirm" />*
</span>
<br />
<br />
<form:addtaglib namespace="modules::captcha::pres::taglib" prefix="form" class="captcha" />
<form:captcha
text_class="cm--create-element-captcha"
validate="true"
button="Save"
clearonerror="true"
/>
<br />
<br />
<form:button name="Save" class="cm--create-element-button" />
</html:form>
</div>
</div>
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:
PHP-Code
class comment_form_v1_controller extends commentBaseController {
function transformContent(){
$form = &$this->__getForm('AddComment');
if($form->get('isSent') == true){
$this->__loadCategoryKey();
$M = &$this->__getAndInitServiceObject('modules::comments::biz','commentManager',$this->__CategoryKey);
if($form->get('isValid') == true){
$articleComment = new ArticleComment();
$name = &$form->getFormElementByName('Name');
$articleComment->set('Name',$name->getAttribute('value'));
$email = &$form->getFormElementByName('EMail');
$articleComment->set('EMail',$email->getAttribute('value'));
$comment = &$form->getFormElementByName('Comment');
$articleComment->set('Comment',$comment->getContent());
$M->saveEntry($articleComment);
}
else{
$this->__buildForm();
}
}
else{
$this->__buildForm();
}
}
private function __buildForm(){
$form = &$this->__getForm('AddComment');
$form->setAttribute('action',$_SERVER['REQUEST_URI'].'#comments');
$config = &$this->__getConfiguration('modules::comments','language');
$button = &$form->getFormElementByName('Save');
$button->setAttribute('value',$config->getValue($this->__Language,'form.button'));
$form->transformOnPlace();
$link = FrontcontrollerLinkHandler::generateLink($_SERVER['REQUEST_URI'],array('coview' => 'listing'));
$this->setPlaceHolder('back',$link);
}
}
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.
PHP-Code
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');
}
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.
Die Datenschicht muss nun noch die Methode
saveArticleComment() implementieren.
PHP-Code
function saveArticleComment($ArticleComment){
$SQL = &$this->__getConnection();
// 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);
}
}
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.
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.