Implementierung von Tags

1. Einführung

Dieses Kapitel widmet sich den APF-Tags (a.k.a. Taglibs), deren Bedeutung und Implementierung.

Der Page-Controller basiert auf dem gleichnamigen Software-Entwurfsmuster und ist ein zentraler Bestandteil des APF. Er ermöglicht dem Entwickler über das Tag-Konzept, eigene Funktionalitäten zur Erzeugung und Transformation des HMVC-DOM-Baumes zu injizieren. Tags bilden damit nicht nur die Basis des APF-HMVC-Konzepts, sondern können auch zur Erweiterung desselben genutzt werden.

Des Weiteren können Tags dazu verwendet werden um darin enthaltene Funktionalitäten in unterschiedlichen Projekten wiederzuverwenden. APF-Templates verarbeiten - als Schutz vor unkontrollierter Verteilung von Logik - lediglich Tags und keinen PHP-Code. Damit ist automatisch sichergestellt, dass View-Logik lediglich in Tags oder Controller enthalten sein kann.

1.1. Page-Controller

Aufgabe des Page-Controller ist der Aufbau und die Transformation der internen Template-Struktur. Hierzu bietet er ein Tag-API, die einem definierten Timing-Modell folgend Tags einliest, daraus Dokumente im Baum erzeugt und aus dem Baum anschließend eine HTML-Ausgabe erzeugt. Andere Formate wie beispielsweise XML sind ebenfalls möglich. Der Parser ist kein "echter" XML-Parser, beherrscht jedoch die Auflösung von explizit- und selbst-schließenden Tags sowie die Schachtelung von Tags in beliebig tiefen Hierarchien.

Bitte beachten Sie die Hinweise im Kapitel Tag-Hierarchie zu den Restriktionen des Parsers.

Die mit dem APF ausgelieferten Standard TagLibs bieten bereits eine Reihe an Standard-Funktionalitäten wie das Einbinden einer weiteren Template-Datei, die Definition von wiederverwendbaren Template-Fragmenten und das Bekanntgeben von eigenen Tags an.

1.2. XML-Parser

Der XMLParser dient dem Page-Controller zur Analyse einer Tag-Definition im Template und wird bei der Erstellung von Document-Instanzen genutzt.

Der XMLParser verarbeitet aus Performance-Gründen nur durch Leerzeichen getrennte Attribute. Die Werte von Attributen müssen mit doppelten Anführungszeichen umschlossen sein.

2. Definition eines Tags

Ein APF-Tag definiert sich durch folgende Bestandteile:

  • Prefix: das Prefix des Tags dient im Allgemeinen zur Strukturierung der Tags in Gruppen (z.B. core für Tags, die mit dem APF mitgeliefert werden). Dieser Teil ist mit XML-Namespaces vergleichbar.
  • Name: der Name des Tags bezeichnet den Tag selbst. Dieser Teil ist mit XML-Tag-Namen vergleichbar.
  • Attribute: die Attribute des Tags können zur Konfiguration des Tags genutzt werden. Sie dürfen keine weiteren Tags enthalten.
  • Inhalt: der Inhaltsbereich des Tags kann weitere Tags oder Text enthalten. Der APF-Parser löst dort enthaltene Tags entsprechend auf und hängt diese als Kinder in die aktuelle Hierarchie ein. Einfacher Text ist später ebenfalls in der Tag-Instanz verfügbar.

Folgende Tag-Definition könnte zur Ausgabe des aktuellen Datums in einem Template genutzt werden:

APF-Template
<current:date format="H:i" />

Dabei ist current das Prefix des Tags, date der Name und das Attribut format beinhaltet das Ausgabe-Format. Der Tag definiert keinen Inhalt.

Zur Darstellung einer Bildergalerie könnte folgendes Tag genutzt werden:

APF-Template
<img:gallery> <h3>Mein Urlaub 2012</h3> <p> Diese Bilder stammen aus meinem Urlaub im Jahr 2012: </p> <gallery:datasource namspace="..." class="..." /> </img:gallery>

In diesem Beispiel beinhaltet der <img:gallery />-Tag statisches HTML, was später zur Formatierung der Ausgabe genutzt werden kann und einen weiteren Tag zur Angabe der Datenquelle (z.B. aus einer Datenbank). Zur Verwendung des Tags muss auch dieses zunächst dem APF-Parser bekannt gegeben werden.

3. DOM-Struktur

Wie im Kapitel Kapitel 2 angesprochen erzeugt der Page-Controller aus der Tag-Struktur in den Template-Dateien einen DOM-Baum. Dieser ähnelt dem eines Browsers, der aus den HTML-Tags ebenfalls eine Speicherstruktur erzeugt.

Der Unterschied zur DOM-Struktur eines Browsers besteht darin, dass die einzelnen Knoten jeweils eigene Funktionalität zur Transformation bzw. Darstellung beinhalten, die der Page-Controller während der Transformationsphase nutzt. Details zum Timing-Modell des Page-Controller finden Sie hier.

Jeder Tag - bzw. ab einem definierten Zeitpunkt seine Instanz - durchläuft einen definierten Zyklus. Zunächst wird die Struktur des Tags analysiert, dann seine Substanz (Attribute und Inhalt). Anschließend wird gemäß der Tag-Definition eine Instanz des Tags erstellt und mit dem vom XMLParser analysierten Inhalt initialisiert.

Der Page-Controller verarbeitet Tags nur dann, wenn sie im aktuellen Knoten bekannt sind. Aus diesem Grund müssen eigene Tags zunächst bekannt gegeben werden. Dies kann mit Hilfe des <core:addtaglib />-Tag oder einem
PHP-Code
$this->tagLibs[] = new TagLib(...);
in einem eigenen Tag erreicht werden. Die Nutzung des <core:addtaglib />-Tags ist immer dann notwendig, wenn Sie innerhalb eines Templates einen eigenen Tag bekannt geben wollen, das PHP-Code-Beispiel nutzen Sie, wenn Sie innerhalb eines eigenen Tags dem APF-Parser eine weitere Hierarchie-Stufe bekannt geben wollen.

Zur Bekanntgabe eines Tags werden Präfix und Name sowie Namespace und Klassen-Name der Tag-Implementierung beschrieben. Dabei kann eine Implementierung über unterschiedliche Präfix- und Namens-Angaben mehrfach in unterschiedlichen Hierarchien oder Projekten genutzt werden. Die Klasse TagLib besitzt folgende Signatur (gekürzt):

PHP-Code
final class TagLib { private $namespace; private $class; private $prefix; private $name; public function __construct($namespace, $class, $prefix, $name) { } }

Details zu unterschiedlichen Ebenen von Tags können Sie im Kapitel Tag-Hierarchie nachlesen.

4. Klassenstruktur eines Tags

Die Struktur eines Tags beschreibt die Klasse Document. Diese ist die Mutter aller Tag-Implementierungen des APF und definiert eine Reihe von Methoden, die innerhalb des Timing-Modells des Page-Controllers zu unterschiedlichen Zwecken aufgerufen werden.

Die Signatur de Klasse Document ist wie folgt (gekürzt):

PHP-Code
class Document extends APFObject { protected $objectId = null; protected $parentObject = null; protected $children = array(); protected $content; protected $tagLibs = array(); public function __construct() { } public function onParseTime() { } public function onAfterAppend() { } public function transform() { } }

Den aufgeführten Elementen kommt dabei folgende Bedeutung zu:

  • Im Feld objectId wird die eindeutige interne Objekt-Id gespeichert, die jeder Tag vom APF-Parser bei Erzeugen erhält. Diese wird u.a. bei der Vergabe von Benchmark-Tags genutzt um jedes DOM-Element eindeutig zu referenzieren.
  • parentObject referenziert auf das Vater-Objekt innerhalb des DOM-Baumes. Diese Referenz kann zur Traversierung des DOM-Baumes genutzt werden. Der Root-Knoten hat keinen Vater.
  • In der Liste children finden sich alle Kind-Knoten des aktuellen DOM-Elements. Diese Liste kann zur Traversierung des DOM-Baumes genutzt werden. Hat ein DOM-Knoten keine Kinder, ist die Liste leer.
  • Die Variable content beinhaltet den textuellen Inhalt eines Tags und die Platzhalter, die der Page-Controller bei der Erzeugung des DOM-Baumes hinterlässt um den exakten Zusammenbau des HTML-Quelltextes sicher zu stellen.
  • In der Variable attributes - aus der Klasse APFObject vererbt - findet sich eine assoziative Liste der Tag-Attribute wieder. Besitzt das Tag keine Attribute, ist die Liste leer.
  • Die tagLibs-Liste beinhaltet die für den aktuellen Knoten bekannte Tags. Wie im vorangegangenen Kapitel angesprochen, verarbeitet der APF-Parser nur bekannte Tags innerhalb einer Hierarchie-Stufe bzw. innerhalb eines DOM-Elements. Welche Tags bekannt oder eben nicht bekannt sind, bestimmt demnach der Inhalt dieser Liste.
    Die tagLibs-Liste kann in eigenen Tag-Implementierungen dazu genutzt werden, neue Tags bekannt zu geben oder bereits bekannte Tags zu entfernen. Da die Manipulation der Liste in die eine oder die andere Richtung direkte Auswirkung auf die Verarbeitung von Tags hat, behalten Sie bitte die Bedeutung stets im Hinterkopf!
  • Der Konstruktor einer Tag-Klasse wird bei der Erzeugung der Tag-Instanz aufgerufen. Der Parser übergibt jedoch keine Argumente. Die Methode kann dazu genutzt werden, weitere Tags für die vorliegende Hierarchie bekannt zu geben oder Standard-Werte zu belegen.
    Zum Zeitpunkt der Ausführung des Konstruktors sind weder die Attribute des Tags noch der Inhalt verfügbar. Ebenso sind der Kontext und die Sprache des Tags noch nicht initialisiert.
  • Nachdem der Tag geparst wurde wird die Methode onParseTime() aufgerufen. Zu diesem Zeitpunkt sind die Attribute des Tags und der Inhalt in der Instanz verfügbar. Auch Kontext und Sprache des Tags sind initialisiert. Diese Methode kann dazu genutzt werden, den Tag mit Hilfe der verfügbaren Informationen weiter zu initialsieren oder den Tag-Inhalt weitere zu analysieren (z.B. enthaltene Kind-Tags parsen).
  • Ist der Knoten in den DOM-Baum eingehangen - sprich sind Vater und Kinder bekannt - wird die Methode onAfterAppend() aufgerufen. Innerhalb dieser Methode können Sie nun Logik platzieren, die auf Vater und Kinder Auswirkung hat.
  • Wird der aktuelle DOM-Knoten transformiert, ruft der Page-Controller die Methode transform() Ihres Tags auf. Platzieren Sie hier bitte diejenige Logik, die zur Erzeugung von HTML-Quelltext benötigt wird. Tags, die innerhalb des DOM-Baumes lediglich zur Konfiguration oder Initialisierung dienen generieren oft keine Ausgabe (z.B. <core:addtaglib />). Wie sich Ihr Tag letztlich verhält bleibt Ihrer Implementierung überlassen.
    Sofern die Methode transform() in eigenen TagLibs überschrieben wird, muss sich der Entwickler selbst um die Transformation der Kind-Knoten kümmern. Dies kann im einfachsten Fall durch
    PHP-Code
    foreach($this->children as $objectId => $DUMMY){ $this->content = str_replace( '<'.$objectId.' />', $this->children[$objectId]->transform(), $this->content ); }
    erreicht werden, wenn in den Methoden onParseTime() oder onAfterAppend() die Parser-Funktion extractTagLibTags() aufgerufen wurde. Weitere Hinweise können dem Foren-Beitrag transform() von eigenem taglib nicht ausgeführt entnommen werden.
Im Release 1.17 wurden alte Namenskonventionen von Methoden entfernt. Sofern Sie das APF in der Version 1.16 oder kleiner verwenden lautet der Name der Parser-Funktion __extractTagLibTags() statt extractTagLibTags().
Im Release 1.17 wurden alte Namenskonventionen von Klassen-Variablen entfernt. Dies betraf insbesondere die Signature der Klassen APFObject und Document. Die folende Code-Box zeigt die Signatur der Klasse Document vor Version 1.17:
PHP-Code
class Document extends APFObject { protected $__ObjectID = null; protected $__ParentObject = null; protected $__Children = array(); protected $__Content; protected $__TagLibs = array(); public function __construct() { } public function onParseTime() { } public function onAfterAppend() { } public function transform() { } }

Seit Release 1.16 können für die Transformation von Kind-Knoten folgende Methoden genutzt werden:

  • transformChildren() Transformiert die Kind-Knoten im internen Inhalts-Puffer ($this->content).
  • transformChildrenAndPreserveContent() Transformiert die Kind-Knoten und gibt das Ergebnis zurück. Der interne Inhalts-Puffer ($this->content) wird dabei für eine erneute Transformation beibehalten.
  • transformChildrenAsEmpty() Entfernt die internen Positionsmarker des APF-Parsers (siehe Hinweis in Kapitel 5.2.4. Erzeugen der Ausgabe) im internen Inhalts-Puffer ($this->content).
  • transformChildrenAsEmptyAndPreserveContent() Entfernt die internen Positionsmarker des APF-Parsers (siehe Hinweis in Kapitel 5.2.4. Erzeugen der Ausgabe) und gibt das Ergebnis zurück. Der interne Inhalts-Puffer ($this->content) wird dabei für eine erneute Transformation beibehalten.

5. Implementierung

5.1. Einfacher Tag

Dieses Kapitel beschäftigt sich mit einfachen Tags. "Einfach" meint in diesem Zusammenhang einen Tag, der eine bestimmte Aufgabe erledigt, jedoch keine weitere Hierarchie erzeugt - sprich keine Kind-Tags besitzt.

Als Beispiel soll die Ausgabe der aktuellen Uhrzeit aus Kapitel 2 dienen. Hierzu muss der Tag lediglich zur Transformations-Zeit die Uhrzeit im gewünschten Format zurückgeben. Abhängigkeiten zu anderen Tags im Baum und die Notwendigkeit der Initialisierung bestehen nicht. Der Quellcode des Tags gestaltet sich damit wie folgt (Namespace: examples::tags::pres):

PHP-Code
class HourDisplayTag extends Document { public function transform() { return date($this->getAttribute('format')); } }

Innerhalb eines Templates lässt er sich wie anschließend gezeigt verwenden:

APF-Template
<core:addtaglib namespace="examples::tags::pres" class="HourDisplayTag" prefix="current" name="date" /> <current:date format="H:i:s" />
Der Konstruktor der Klasse Document gibt mögliche Kind-Tags bekannt, die in der aktuellen Implementierung nicht benötigt werden. Insofern könnte als mögliche Speicherverbrauchsoptimierung ebenso der Konstruktor überschrieben werden damit sichergestellt ist, dass der Tag keine weiteren Kinder verarbeiten kann.

5.2. Komplexer Tag

Als "komplexer" Tag versteht das APF einen Tag mit weiteren - in der Hierarchie nicht eingeschränkten - Kind-Knoten.

Als Beispiel soll die Ausgabe einer Bildergalerie aus Kapitel 2 dienen. Dieser Tag definiert zunächst statischen Inhalt, der zur Ausgabe einer Überschrift und eines Einleitungstextes genutzt wird. Die Generierung der Ausgabe selbst wird vom <img:gallery />-Tag mit Hilfe eines Content-Providers, der durch den <gallery:datasource />-Tag definiert wird.

Bitte beachten Sie, dass die Struktur des Beispiels willkürlich gewählt und nicht für Ihre Implementierungen bindend ist. Es wäre sowohl denk- als auch realisierbar den Content-Provider über Tag-Attribute zu definieren und die Formatierung der Galerie über weitere Elemente (siehe z.B. Iterator) zu realisieren.
5.2.1. Basis

Die Basis-Struktur des Tags gestaltet sich wie folgt:

PHP-Code
import('examples::tags::pres', 'GalleryDataSourceTag'); class GalleryTag extends Document { public function __construct() { $this->tagLibs[] = new TagLib( 'examples::tags::pres', 'GalleryDataSourceTag', 'gallery', 'datasource' ); } ... }

Im Konstruktor des Tags wird dem Parser der <gallery:datasource />-Tag bekannt gegeben. Damit der Parser die Klasse GalleryDataSourceTag findet, wird sie per import() eingebunden.

5.2.2. Konfiguration des Content-Provider

Widmen wir uns nun zunächst der Konfiguration des Content-Providers. Hierzu spendieren wir dem <img:gallery />-Tag ein privates Feld und einen Setter, damit der <gallery:datasource />-Tag die Instanz übergeben kann:

PHP-Code
class GalleryTag extends Document { private $contentProvider; ... public function setContentProvider(ImageGalleryContentProvider $provider) { $this->contentProvider = $provider; } ... }

Der Provider selbst definiert sich durch folgendes Interface:

PHP-Code
interface ImageGalleryContentProvider { /** * @return GalleryImage[] */ public function getImages(); } class GalleryImage { private $title; private $description; private $imageUrl; public function __construct($title, $description, $imageUrl) { $this->title = $title; $this->description = $description; $this->imageUrl = $imageUrl; } public function getDescription() { return $this->description; } public function getImageUrl() { return $this->imageUrl; } public function getTitle() { return $this->title; } }

Gemäß unseres Beispieles ist der <gallery:datasource />-Tag dafür zuständig den gewünschten Provider zu erzeugen und dem GalleryTag mitzugeben. Dies lässt sich innerhalb der onAfterAppend()-Methode erledigen, da zu diesem Zeitpunkt der Vater-Knoten bekannt ist:

PHP-Code
class GalleryDataSourceTag extends Document { public function __construct() { } public function onAfterAppend() { $provider = &$this->getServiceObject( $this->getAttribute('namespace'), $this->getAttribute('class') ); /* @var $parent GalleryTag */ $parent = &$this->getParentObject(); $parent->setContentProvider($provider); } public function transform() { return ''; } }

Da der GalleryDataSourceTag keine weiteren Kinder erwartet, wurde der Konstruktor von Document leer überschrieben. In der Methode onAfterAppend() erzeugt das Tag den gewünschten Provider mit Hilfe des ServiceManager um der Instanz Kontext und Sprache des aktuellen Elements mitzugeben. Anschließend wird dem Vater-Element des DOM-Baumes - hier eine Instanz der Klasse GalleryTag wie mit dem Type-Hint angegeben wird - der Provider injiziert.

Die leer überschriebene transform()-Methode sorgt dafür, dass keine weiteren Kind-Elemente transformiert oder eine Ausgabe erzeugt wird. Dies ist im aktuellen Beispiel nicht notwendig.

5.2.3. Parsen des GalleryDataSourceTag

Damit der GalleryDataSourceTag seine Arbeit verrichten kann muss der GalleryTag dafür sorgen, dass er auch vom Page-Controller erfasst und ausgeführt wird. Dazu bringt der Page-Controller bereits eine Parser-Methode mit, die in jeder von Document erbenden Klasse ausgeführt werden kann - extractTagLibTags().

Zu welchem Zeitpunkt diese aufgerufen werden soll entscheiden zunächst Sie selbst. Beachten Sie dabei jedoch immer das Ablaufdiagramm des Page-Controller um den für den Anwendungsfall richtigen Zeitpunkt zu wählen. In unserem Fall ist lediglich wichtig, dass der Provider vor der Erzeugung der Inhalte ausgeführt wird. Damit sind theoretisch drei Möglichkeiten denkbar:

  • In einer eigenen onParseTime()-Methode.
  • In einer eigenen onAfterAppend()-Methode.
  • Zu Beginn der transform()-Methode.

Üblicherweise schickt es sich für die Analyse von weiteren Kind-Strukturen die onParseTime()-Methode zu nutzen, da Sie damit sicherstellen, dass die weiteren Elemente im Baum ebenfalls die Möglichkeit haben, Kind-Knoten zeitlich korrekt zu erzeugen.

Um den GalleryDataSourceTag zu erzeugen und auszuführen, reicht folgender Quellcode:

PHP-Code
class GalleryTag extends Document { ... public function onParseTime() { $this->extractTagLibTags(); } ... }

Alles Weitere erledigt der APF-Parser für Sie gemäß den bekannten Tags.

5.2.4. Erzeugen der Ausgabe

Die Ausgabe der Bildergalerie erfolgt in der Methode transform(), die vom Page-Controller bei der Transformation des Baumes aufgerufen wird. Diese muss nun dafür sorgen, dass die Bilder, die vom Provider geliefert werden entsprechend ausgegeben werden.

Unter der Annahme, dass alle Bilder als ungeordnete Liste mit entsprechendem Platz für Beschreibungen ausgegeben werden, gestaltet sich die Methode transform() wie folgt:

PHP-Code
class GalleryTag extends Document { ... public function transform() { $images = $this->contentProvider->getImages(); $buffer = '<ul class="gallery-images">'; foreach ($images as $image) { $buffer .= '<li>' . '<img src="' . $image->getImageUrl() . '" alt="' . $image->getTitle() . '" />' . '<p>' . $image->getDescription() . '</p>' . '</li>'; } $buffer .= '</ul>'; $this->setContent($this->getContent() . $buffer); } }

Mit der aktuellen Implementierung werden alle Bilder als Listen-Elemente nach dem im <img:gallery />-Tag vorhandenen statischen Inhalt angezeigt.

Soll in einem Tag kein statischer Inhalt zugelassen werden, so kann dieser in der transform()-Methode durch setContent() durch einen dynamisch erzeugten überschrieben werden.
Damit bei der Transformation die Inhalte eines Tags an der korrekten Stelle ausgegeben werden, erzeugt die Methode extractTagLibTags() Platzhalter der Form
APF-Template
<{OBJECT_ID} />
Dabei entspricht {OBJECT_ID} dem Wert der Klassen-Variable $this->objectId und dem Array-Offset, in dem die Kind-Tags gespeichert werden ($this->children). Dieser Wert kann dann bei der Implementierung von eigenen transform()-Methoden für die Ersetzung des Inhalts genutzt werden.

Im Fall des <img:gallery />-Tags sollen die Kind-Tags keine Ausgabe erzeugen, was prinzipiell mit der GalleryDataSourceTag::transform()-Methode sichergestellt. Da der APF-Parser jedoch zur Positionierung der Ausgabe Platzhalter erzeugt, müssen diese in unserem Beispiel dennoch ersetzt werden. Dies kann durch folgende Erweiterung passieren:

PHP-Code
class GalleryTag extends Document { ... public function transform() { $images = $this->contentProvider->getImages(); $buffer = '<ul class="gallery-images">'; foreach ($images as $image) { $buffer .= '<li>' . '<img src="' . $image->getImageUrl() . '" alt="' . $image->getTitle() . '" />' . '<p>' . $image->getDescription() . '</p>' . '</li>'; } $buffer .= '</ul>'; $this->setContent($this->getContent() . $buffer); foreach ($this->children as $objectId => $DUMMY) { $this->content = str_replace( '<' . $objectId . ' />', $this->children[$objectId]->transform(), $this->content ); } } }

5.3. Document-API

Zur Implementierung von Tag-Logik stehen zahlreiche Methoden zur Verfügung. Diese umfassen das Parsen von Tag-Strukturen, das Traversieren des Baumes und das Manipulieren oder Auslesen von Tag-Informationen. Diese sind:

  • extractTagLibTags(): Analysiert die bekannten Kind-Tags und erstellen den DOM-Baum daraus.
  • getParentObject(): Liefert die Instanz des Vater-Tags im DOM-Baum. Sofern kein Vater vorhanden ist, liefert die Methode null.
  • setParentObject(): Injiziert eine Instanz als Vater-Tags des entsprechenden DOM-Knotens.
  • getChildren(): Liefert die Liste aller Kinder des Tags. Sofern keine Kinder vorhanden sind, wird eine leere Liste zurückgegeben.
  • getContent(): Liefert den Inhalt des befragten DOM-Elements.
  • setContent(): Beschreibt den Inhalt des entsprechenden DOM-Elements.
  • getChildNode(): Liefert einen Kind-Knoten, der einem definierten Selektor genügt.
  • getChildNodes(): Liefert mehrere Kind-Knoten, der einem definierten Selektor genügen.
  • getAttribute(): Liefert den Wert eines Attributes.
  • setAttribute(): Definiert den Wert eines Attributes.
  • getAttributes(): Liefert die Liste der aktuellen Attribute.
  • getAttributesAsString(): Generiert eine XML-Repräsentation der übergebenen Attribute an Hand einer optionalen Whitelist.

Weitere Methoden der Klasse Document oder der Basis-Klasse ihres aktuellen Tags finden Sie in der API-Dokumentation.

6. Tag-Hierarchie

Der Tag-Parser des APF besitzt aus Performance- und Konsistenz-Gründen einigen Restriktionen gegenüber einem "echten" XML-Parser. Diese sind:

  • Tag-Attribute können nur durch Leerzeichen getrennt werden. Tab-Zeichen nicht nicht möglich.
  • Der Inhalt von Tag-Attribute wird nur in doppelten Anführungszeichen erkannt.
  • Tag-Hierarchien werden nur dann richtig aufgelöst, wenn ein Tag - bestehend aus Präfix und Namen - nur in einer Ebene bekannt sind. Dies erfordert die Registrierung einer Tag-Implementierung in unterschiedlichen Ebenen mit einer anderen Präfix-Name-Kombination.
    Möchten Sie einen Tag auf "höchster" Ebene in einer Template-Datei und gleichzitig in einer tiefer gelegenen Struktur verwenden, ordnet der Parser die Instanzen falsch zu. Beispiel:
    APF-Template
    <core:addtaglib namespace="..." class="FooBarTag" prefix="foo" name="tag" /> <foo:bar id="..."/> <html:template name="..."> <template:addtaglib namespace="..." class="FooBarTag" prefix="foo" name="tag" /> <foo:bar id="..."/> </html:template>
    In diesem Fall ordnet der Parser die Instanz des Tags <foo:bar /> der obersten Ebene zu. Im <html:template />-Tag ist kein <foo:bar /> vorhanden, sondern lediglich ein Platzhalter und es kommt bei der Transformation zu einem Fehler bzw. einem nicht nachvollziehbaren Verhalten.
  • Die Definition eines Tags wirden nur dann erkannt, wenn Tag-Deklaration und Attribute durch ein Leerzeichen getrennt sind. Dies gilt insbesondere für mehrzeilige Tag-Definitionen von verschachtelten Tags. Folgendes Beispiel wird vom APF-Parser nicht erkannt:
    APF-Template
    <form:recaptcha name="my-captcha"> <recaptcha:getstring ... /> </form:recaptcha>
    Um die auftretende ParserException zu beheben, kann entweder hinter <form:recaptcha ein Leerzeichen eingefügt oder das Attribut name auf der selben Zeile wie <form:recaptcha aufgeführt werden.
    Dieser Fehler tritt vermehrt in IDEs auf, die beim Speichern von HTML-Dateien unnötige Leerzeilen am Ende einer Zeile automatisch entfernen.
  • Als verarbeitbare Tags werden nur APF-Tags erkannt. Einfache HTML-Tags können nicht erfasst werden und dies erfordert u.U. die Implementierung von Wrapper-Tags für die Abstraktion.

6.1. Einfache Hierarchien

Einfache Tag-Hierarchien sind solche, bei denen Tags in unterschiedlichen Ebenen nur einmal vorkommen. Die Einschränkung des Tag-Parsers wirkt sich dabei vor allem auf den aktuell verarbeiteten Knoten - in der Regel das aktuelle Template - aus.

Als Beispiel soll uns folgendes Template dienen, in dem ein weiteres eingebunden wird:

APF-Template
<html:placeholder name="..." /> <core:importdesign namespace="" template="template2" />

Das per template2 eingebundene Template - dieses stellt die nächste Hierarchie-Stufe im DOM-Baum dar - hat folgenden Inhalt:

APF-Template
<html:placeholder name="" /> <html:template name="..."> ... </html:template>

Da die beiden <html:placeholder />-Tags in unterschiedlichen Template-Dateien definiert sind, werden sie den jeweiligen Hierarchien korrekt zugeordnet.

Bitte beachten Sie, dass APF-Tags nur dann verarbeitet werden können, wenn sie dem APF-Parser bekannt sind. Benutzen Sie eigene Tags, geben Sie diese bitte mit Hilfe der <*:addtaglib />-Tags bekannt.
Ein per <*:addtaglib />-Tag bekannt gegebener Tag gilt nur innerhalb des aktuellen DOM-Knotens! Beabsichtigen Sie einen eigenen Tag sowohl in der ersten Hierarchie - quasi "direkt" in der Template-Datei - als auch einer tiefer gelegenen zu verwenden, so muss der Tag in beiden bekannt gegeben werden. Beachten Sie hierzu bitte die Hinweise im nächsten Kapitel.

6.2. Komplexe Hierarchien

Als Beispiel für komplexe Hierarchien, soll ein Tag dienen, der auf Basis eines Schlüssels Übersetzungen für die aktuell gewählte Sprache ausgibt. Er soll auf Ebene einer Template-Datei, innerhalb eines <html:template />-Tags und in einem eigenen Tag eingesetzt werden.

Als Basis für die weitere Diskussion soll folgende Template-Datei dienen:

APF-Template
<core:addtaglib namespace="..." class="GetTextTag" prefix="html" name="text" /> <html:text key="..." /> <html:template name=""> <core:addtaglib namespace="..." class="GetTextTag" prefix="template" name="text" /> <template:text key="..." /> </html:template> <user:control-panel> <control-panel:text key="..." /> <control-panel:navi /> </user:control-panel>

Der GetTextTag wird innerhalb der ersten Hierarchie und im <html:template />-Tag durch einen <*:addtaglib />-Tag bekannt gegeben, im <user:control-panel />-Tag ist er bereits durch die Implementierung bekannt.

Durch die in Kapitel 6 beschriebene Einschränkungen des APF-Parsers ist es notwendig für die Verwendung des <*:text />-Tags innerhalb von <html:template /> und <user:control-panel /> ein eigenes Präfix zu wählen. Sind Präfix und Name des <*:text />-Tags in allen Hierarchien identisch wird der Parser die Instanzen derjenigen Hierarchie zuweisen in der er zuerst bekannt gegeben wurde - in diesem Fall der ersten.
Aus den oben genannten Gründen und um die Zugehörigkeit eines Tags zu einer Hierarchie auszudrücken empfiehlt es sich, Präfix und Namen eines Tags gemäß der Hierarchie zu vergeben. Im obigen Beispiel wurden die Kind-Tags des <user:control-panel /> mit dem Präfix control-panel ausgestattet. Für tiefer strukturierte Hierarchien kann folgendes Beispiel als Vorlage dienen:
APF-Template
<shop:basket> <basket:title /> <basket:products> <products:listing /> <products:sum /> </basket:products> </shop:basket>
Weitere Hinweise finden Sie im Forum unter Schachtelung TagLibs.

6.3. Verschachtelte Strukturen

Ab dem Release 1.17 ist der APF-Parser in der Lage verschachtelte Strukturen von Tags mit identischem Prefix und Namen innerhalb eines Dokuments bzw. einer Datei zu verarbeiten (siehe Kapitel 6). Die übrigen Einschränkung des APF-Parsers gegenüber einem vollwertigen XML-Parser bleiben jedoch erhalten.

Die folgende Code-Box stellt ein Beispiel einer hierarchischen Struktur da, die ab dem APF 1.17 korrekt verarbeitet wird:

APF-Template
<foo:bar> ... <foo:bar> ... <foo:bar> ... </foo:bar> ... </foo:bar> ... <foo:bar /> ... <foo:bar> ... </foo:bar> ... </foo:bar> <foo:bar> ... </foo:bar>

Der beschriebene Quellcode zeigt eine asymmetrische Struktur von unterschiedlichen <foo:bar />-Tags, die unterschiedlich tief verschachtelt sind und selbst- und explizit schließende Tags beinhaltet. Der Parser verarbeitet nun symmentrische (gleiche Anzahl von öffnenden wie schließenden Tags) und asymmetrische (ungleiche Anzahl von öffnenden und schließenden Tags) Strukturen und ordnet die jeweiligen Inhalte den definierten Ebenen korrekt zu.

Als Anwendungsbeispiel soll folgende verschachtelte HTML-Liste dienen, die sich am <ul />-Tag orientiert:

APF-Template
<html:list> <list:item>Kapitel 1</list:item> <list:item> Kapitel 2 <html:list> <list:item>Kapitel 2.1</list:item> <list:item>Kapitel 2.2</list:item> </html:list> </list:item> <list:item>Kapitel 3</list:item> </html:list>

Bei der Implementierung von Tags für verschachtelte Strukturen kann das Verhalten je nach Platzierung bzw. Hierarchie-Stufe im Baum variieren. Dies ist beim gewählten Beispiel nicht der Fall, da sich das zu generierende HTML an der Tag-Struktur orientiert.

Der APF-Parser analysiert die in einem Dokument enthaltenen Tags an Hand der registrierten Tags, die im $this->tagLibs-Array vorgehalten werden. Dabei geht er chronologisch vor und analysiert die definierten Tags in der Reihenfolge, in der sie registriert wurden.

Diese Tatsache ist insbesondere für sich gegenseitig überlagernde Strukturen wichtig, da bei falsch gewählter Reihenfolge der bekannten Tags die korrekte Zuordnung der Instanzen zu den gewünschten Ebenen nicht korrekt ausgeführt wird. Das oben aufgeführte Beispiel basiert auf der folgenen, vereinfachten Struktur:

APF-Template
<ul> <li> <ul> <li></li> </ul> </li> </ul>

Beim Parsen der Tags muss nun darauf geachtet werden, dass zuerst alle <ul/>-Tags und innerhalb dieser zunächst die <li/>-Tags verarbeitet werden. Anschließend kann innerhalb der <li/>-Tags nach weiteren Unterstrukturen gesucht werden.

Der APF-Parser arbeitet nach dem Prinzip, dass die innenliegenden Strukturen zunächst nicht weiter analysiert werden, sondern direkt an die Kind-Instanz weiter gereicht werden. So sind Sie in der Lage bei der Implementierung eines Tags selbst in der Kind-Instanz über die Verarbeitung zu entscheiden.

Diese beiden Hinweise können direkt als Handlungsanweisung für die Implementierung genutzt werden. Die Tag-Repräsentation einer Liste hat damit folgende Gestalt:

PHP-Code
class UnorderedListTag extends Document { public function __construct() { $this->tagLibs = array( new TagLib('sandbox::pres::taglib', 'ListElementTag', 'list', 'item') ); } public function onParseTime() { $this->extractTagLibTags(); } public function transform() { return '<ul>' . parent::transform() . '</ul>'; } }

Der UnorderedListTag registriert Listen-Einträge als mögliche Kinder und erzeugt die HTML-Ausgabe durch Umschließen des Inhalts mit einem <ul/>-Tag. Weitere <html:list/>-Tags werden nicht beachtet, da diese erst innerhalb von <list:item/>-Tags verarbeitet werden sollen.

Bei Nutzung von verschachtelten Strukturen müssen Sie selbst für die Verarbeitung des Inhalts eines Tags sorgen. Dazu ist im Allgemeinen die Registrierung des äußeren Tags als innere Struktur notwendig - der Tag muss sich also zusätzlich selbst kennen! Ist dies nicht der Fall, wird der Aufbau der rekursiven Struktur unterbrochen.

Die Implementierung eines Listen-Elements kann wie folgt realisiert werden:

PHP-Code
class ListElementTag extends Document { public function __construct() { $this->tagLibs = array( new TagLib('sandbox::pres::taglib', 'UnorderedListTag', 'html', 'list') ); } public function onParseTime() { $this->extractTagLibTags(); } public function transform() { return '<li>' . parent::transform() . '</li>'; } }

Der Konstruktor der ListElementTag-Klasse registriert für die Analyse des Inhalts zunächst den <html:list/>-Tag um sicherzustellen, dass weiterführende Unterstrukturen richtig aufgebaut werden. Die Listen-Tag-Implementierung kennt also die Listen-Element-Implementierung und umgekehrt. So ist bei verschachtelten Strukturen ein rekursiver Aufbau von beliebig tiefen Strukturen möglich.

7. Anwendungsbeispiele

In diesem Kapitel finden Sie Anwendungsbeispiele für Tags, die Sie in Ihrer täglichen Arbeit mit dem APF als Vorlage für eigene Implementierungen bzw. als Coding-Guideline nutzen können. Als Daumenregel gilt:

Ein Tag wird erst notwendig, wenn die Anforderung mit Hilfe eines Templates - inkl. aller Standard TagLibs - und einem (Document-)Controller nicht mehr zu realisieren ist (z.B. Manipulationen des DOM-Baums). Weiterhin empfielt es sich, Funktionalität in Tags abzufassen, wenn immer wieder verwendete Elemente in unterschiedlichen Templates genutzt werden soll ohne den Code zu duplizieren oder unnötige Vererbungshierarchien zu erzeugen.

7.1. Einfacher Tag mit Attributen

Als Beispiel für einen einfachen Tag soll uns der <html:text />-Tag aus Kapitel 6.2 dienen. Dieser gibt einen sprachabhängigen Text aus, der an Hand eines Attributs definiert wird. Beispiel:

APF-Template
<html:text key="log-in.mousover.text" />

Die Implementierung des Tags beinhaltet die Verarbeitung des Attributs und die Ausgabe des Textes zur Transformationszeit. Dies kann mit folgender Implementierung bewerkstelligt werden:

PHP-Code
class TranslationTextTag extends Document { public function transform() { return gettext($this->getAttribute('key')); } }

Sollen die Attribute eines Tags bei der Generierung der Ausgabe einbezogen werden, kann die Methode getAttributesAsString() genutzt werden. Als Beispiel soll ein Tag genutzt werden, der die Ausgabe eines Bildes aus einer Medien-Datenbank an Hand einer externen ID erzeugt:

APF-Template
<html:img key="IMG-12345" width="100" height="120" alt="Diese Bild zeigt ein rotes Auto" />

Die zugehörige Implementierung kann wie folgt aussehen:

PHP-Code
class MAMImageTag extends Document { public function transform() { $width = $this->getAttribute('width', '50'); $height = $this->getAttribute('height', '50'); $key = $this->getAttribute('key'); $whiteList = array( 'alt', 'height', 'width', 'id', 'style', 'title' ); return '<img src="' . $this->getImageUrl($key, $width, $height) . '" ' . $this->getAttributesAsString($this->getAttributes(), $whiteList) . ' />'; } private function getImageUrl($key, $width, $height) { ... } }

In der obigen Code-Box wird die Methode getAttribute() dazu genutzt um die Werte der angegebenen Attribute auszulesen. Da einige der Attribute für die Ausgabe wiederverwendet werden sollen, wird der Methode getAttributesAsString() eine Liste an erlaubten Attributen - in diesem Fall kompatibel zur XHTML- bzw. HTML5-Spezifikation - mitgegeben. Die Ausgabe des Tags ist damit ein Image-Tag, der mit Hilfe der (HTML-)Attribute id, style und class formatiert werden kann.

7.2. Einfacher Tag mit Inhalt

Neben den Attributen eines Tags kann dieser auch einfachen und komplexen Inhalt definieren. Als Implementierungsbeispiel soll in diesem Kapitel folgender Tag genutzt werden:

APF-Template
<html:entityencode>nobody@example.com</html:entityencode>

Ausgabe des Tags soll eine in HTML-Entitäten codierte E-Mail-Adresse sein, um sie vor Bots oder Spidern zu schützen. Hierzu muss der Inhalt des Tags ausgelesen und im Rahmen der Transformation des Tags ausgegeben werden. Dies führt zu folgender Implementierung:

PHP-Code
import('tools::string', 'StringAssistant'); class EntityEncodeTag extends Document { public function transform() { return StringAssistant::encodeCharactersToHTML($this->getContent()); } }

Mit der Methode getContent() kann auf den Inhalt des Tags zugegriffen werden. Dieser wird mit Hilfe des StringAssistant in HTML-Entitäten umgewandelt.

7.3. Zugriff auf den umliegenden DOM-Baum

Wie im Kapitel 1 angesprochen erzeugt der Page-Controller aus den genutzten Templates und den darin enthaltenen Tags einen DOM-Baum. Das Verhalten ist - bis auf die Art der Erzeugung - mit dem eines Browsers vergleichbar, der ebenfalls aus dem angelieferten HTML einen DOM-Baum erzeugt und diesen grafisch aufbereitet.

Da Tags und (Document-)Controller Teil des Baumes sind haben Sie dort die Möglichkeit auf alle umliegenden Knoten zuzugreifen. Hierzu gibt es im Wesentlichen zwei Methoden:

  • getParentObject()
  • getChildren()

Innerhalb eines Tags können Sie mit diesen Methoden direkt auf die umliegenden Elemente zugreifen, innerhalb eines (Document-)Controller beziehen Sie mit getDocument() eine Referenz auf den aktuellen DOM-Knoten und können ab diesem mit den oben genannten Methoden navigieren.

Als Beispiel für den Zugriff auf soll folgendes Template dienen:

APF-Template
<html:template name="test1"> ... </html:template> <html:template name="test2"> <template:addtaglib namespace="..." class="TemplateNameDisplayTag" prefix="template" name="display-name" /> ... </html:template>

Der TemplateNameDisplayTag hat dabei die Aufgabe, die Namen aller Templates auszugeben, die im Baum auf gleicher Ebene hängen wie sein Vater. Hierzu kann folgende Implementierung genutzt werden:

PHP-Code
class TemplateNameDisplayTag extends Document { public function transform() { $template = &$this->getParentObject(); $grandFather = &$template->getParentObject(); $nodes = $grandFather->getChildren(); $buffer = '<ul>'; foreach ($nodes as $objectId => $DUMMY) { $buffer .= '<li>' . $nodes[$objectId]->getAttribute('name') . '</li>'; } return $buffer . '</ul>'; } }

Ein weiterführendes Beispiel finden Sie im Forum unter Placeholder über Taglib füllen.

7.4. Komplexer Tag mit Attributen und Inhalt

Die vorangegangenen Kapitel haben sich mit dem Zugriff auf Attribute, der Ausgabe von Attributen und dem Zugriff und der Verarbeitung von Tag-Inhalten beschäftigt. In diesem Abschnitt beschäftigen wir uns mit komplexeren Tags, die weitere Strukturen definieren und Inhalte sowie Attribute in die Abbildung ihrer Logik einbeziehen.

Als Beispiel soll uns ein Tag dienen, der Navigationsknoten vom Typ NavigationNode ausgibt:

APF-Template
<core:addtaglib namespace="extensions::navigation::pres::tags" class="NavigationNodeTag" prefix="navi" name="template" /> <navi:template id="main-navi"><!-- NavigationNodeTag --> <navi:item status="active"><!-- NavigationItemTag --> <li class="active"> <item:content/><!-- ItemTemplateContentTag --> </li> </navi:item> <navi:item status="inactive"> <li> <item:content/> </li> </navi:item> <ul id="main-navigation"> <navi:content/><!-- NavigationContentTag --> </ul> </navi:template>

Der Tag wird in einem (Document-)Controller gefüllt und stellt die Liste der Kinder des übergebenen Navigationsknotens gemäß den definierten Formatierungen dar. Dabei beschreiben die <navi:item />-Tags die aktiven und inaktiven Zustände der Navigationsknotens und mit Hilfe von <navi:content/> lässt sich die Ausgabe in ein HTML-Gerüst packen. Der Tag <item:content/> platziert die Ausgabe eines konkreten Knotens und kann ebenfalls mit eigenem HTML versehen werden. Zur Erläuterung der Tag-Hierarchie sind die Namen der korrespondierenden Tags als HTML-Kommentar angefügt.

Zur Befüllung des <navi:template />-Tags in einem Template lässt sich folgender Controller nutzen:

PHP-Code
import('extensions::navigation::biz', 'SimpleNavigationNode'); class NavigationTagExampleController extends BaseDocumentController { public function transformContent() { $root = new SimpleNavigationNode(null, null, null); $levelOne = new SimpleNavigationNode('Level 1', '#'); $root->setChildren(array( clone $levelOne->setInactive(), clone $levelOne->setActive(), clone $levelOne->setInactive() )); $navi = $this->getDocument()->getChildNode('id', 'main-navi', 'NavigationNodeTag'); $navi->setNode($root); } }

Die folgende Code-Box zeigt nun die Implementierung der Tags:

PHP-Code
interface NavigationNode { public function getLabel(); public function getUrl(); public function getParent(); public function getChildren(); } class SimpleNavigationNode implements NavigationNode { private $label; private $url; private $isActive = false; private $parent; private $children = array(); public function __construct($label, $url) { $this->label = $label; $this->url = $url; } public function getLabel() { return $this->label; } public function getUrl() { return $this->url; } public function isActive() { return $this->isActive; } public function setActive() { $this->isActive = true; return $this; } public function setInactive() { $this->isActive = false; return $this; } public function getParent() { return $this->parent; } public function getChildren() { return $this->children; } public function setParent(NavigationNode $node) { $this->parent = $node; } public function setChildren(array $nodes) { $this->children = $nodes; } } class NavigationNodeTag extends Document { private $node; public function __construct() { $this->tagLibs = array( new TagLib('extensions::navigation::pres::tags', 'NavigationItemTag', 'navi', 'item'), new TagLib('extensions::navigation::pres::tags', 'NavigationContentTag', 'navi', 'content') ); } public function setNode(NavigationNode $node) { $this->node = $node; } public function onParseTime() { $this->extractTagLibTags(); } public function transform() { $buffer = ''; $navigationNodes = $this->node->getChildren(); if (count($navigationNodes) > 0) { foreach ($navigationNodes as $node) { $buffer .= $this ->getTemplate($node->isActive() ? 'active' : 'inactive') ->getOutput($node); } } $content = $this->getContent(); $children = &$this->getChildren(); foreach ($children as $objectId => $DUMMY) { if ($children[$objectId] instanceof NavigationContentTag) { // fill the navi:content place holder if we get him $content = str_replace('<' . $objectId . ' />', $buffer, $content); } else { // replace parser marker to avoid direct tag output $content = str_replace('<' . $objectId . ' />', '', $content); } } return $content; } private function getTemplate($status) { return $this->getChildNode('status', $status, 'NavigationItemTag'); } } class NavigationItemTag extends Document { public function __construct() { $this->tagLibs = array( new TagLib('extensions::navigation::pres::tags', 'ItemTemplateContentTag', 'item', 'content') ); } public function onParseTime() { $this->extractTagLibTags(); } public function getOutput(NavigationNode $node) { $content = $this->getContent(); $children = &$this->getChildren(); foreach ($children as $objectId => $DUMMY) { if ($children[$objectId] instanceof ItemTemplateContentTag) { // fill the item:content place holder if we get him $content = str_replace('<' . $objectId . ' />', $children[$objectId]->setNode($node)->transform(), $content); } else { // replace parser marker to avoid direct tag output $content = str_replace('<' . $objectId . ' />', '', $content); } } return $content; } public function transform() { return ''; } } class ItemTemplateContentTag extends Document { private $node; public function setNode(NavigationNode $node) { $this->node = $node; return $this; } public function transform() { if ($this->node === null) { return ''; } return '<a href="' . $this->node->getUrl() . '">' . $this->node->getLabel() . '</a>'; } } class NavigationContentTag extends Document { }
Das hier beschriebene Beispiel dient dazu, die Möglichkeiten des APF aufzuzeigen und ist nicht als Referenz-Implementierung für die Ausgabe von Navigationen gedacht.

Sofern Sie weitere Informationen benötigen, können Sie folgende Threads aus dem Forum nutzen:

Im Wiki finden sich folgende Seiten zu den APF-Tags:

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.