Ich weiß, es gibt bereits viele Templater, aber die meisten scheitern entweder an einfachsten Aufgaben, oder sind zu überdimensioniert oder anderweitig zu Speziell für bestimmte Anforderungen. Daher mein Einwurf für die Diskussion um einen Templater: das Projekt 1kLOC Templater ist ein Versuch, einen flexiblen, performanten, aber gleichzeitig kleinen Templater zu schreiben.
Das Projekt ist dabei mehr oder weniger ein Test gewesen, ob es möglich ist, entwickelte sich aber recht schnell zu einer Art kleinen Wettbewerb, wie klein man solch einen Templater bekommt. Aber vielleicht fange ich am besten einmal bei der Vorgeschichte an, da noch ein weiteres Template-System ja an sich nichts Besonderes ist.
Andere Template-Systeme
Meine Arbeit mit verschiedenen Template-Systemen fing etwa 2002 mit dem im phpBB enthaltenen Templater an. Damals bestand mein Kontakt mit diesen Systemen im Wesentlichen aus dem Anpassen der Templates, was gerade beim phpBB eine recht einfache und übersichtliche Aufgabe ist, in der sich auch ein Einsteiger recht einfach zurechtfindet. Da ich irgendwann vom reinen phpBB auf Grund der Vielzahl an einzubauenden Mods irgendwann zum phpBB Plus bzw. CBack Orion gewechselt bin, wechselte sich auch das Templatesystem mit dem ich Hauptkontakt hatte zum Xtreme Styles Mod, der im CBack-Forum eingebaut war. Dieser bot gegenüber dem Standard-Templater des phpBB eine Reihe von Vorteile, wie z.B. das nutzen von PHP-Code direkt im Template oder ein verbessertes Schleifenhandling, was jedoch mit einfachsten Template-Fehlern aus dem Tritt zu bringen war. So genügte es oftmals bereits, in einem Schleifenkopf auf den falschen Kontext zuzugreifen (also foo.bar statt nur bar, wenn foo bereits eine Schleife war), um Syntax-Fehler im Template-Cache zu provozieren.
Später kam ich auch kurz mit Smarty in Berührung, hielt es aber auf Grund seiner Größe für keine sinnvolle Alternative zu den im phpBB verwendeten Templatern – Smarty ist einfach extrem aufgebläht, auch wenn hier das erste Mal richtige If-Anweisungen verfügbar waren, die im phpBB und Xtreme Styles Mod immer umständlich über Switches\Schleifen realisiert werden mussten. Dies war zwar ein gewaltiger Fortschritt rechtfertigt aber bei weitem nicht, einen Templater mit über 3 MB Source zu haben: der Xtreme Styles Mod ist etwa 70 kB groß und beherrscht Styling, d.h. verschiedene Template-Pfade pro Template.
Später stieß ich auch auf Dwoo, was mehr oder ein Rewrite von Smarty ist. Also mit all seinen Nachteilen. So erlauben sowohl Dwoo wie auch Smarty das direkte Ausgeben der Request-Parameter in einem Template – eine Funktion, die aus Sicherheitsaspekten ein Alptraum ist, da sie gerade unerfahrenen Nutzern die Möglichkeit bietet, sich XSS-Lücken einzuhandeln, ohne es zu merken. Wieder ein Punkt, was im Xtreme Styles Mod intelligenter gelöst war: Hier musste man jede zu nutzende Variable explizit ins Template importieren, was zwar in Schreibarbeit ausarten konnte, aber einem sofort bewusst gemacht hat, dass man sich um die auszugebenden Daten kümmern muss. Dies hat zwar auch das unbedachte Erzeugen von XSS-Lücken ausgeschlossen, hat jedoch zumindest das Bewusstsein für die auszugebenden Daten geschärft.
Zwischenzeitlich bin ich bei ViewVC (Python) auch mit deren Template-System in Berührung gekommen, was im Grunde zwar an Smarty angelehnt ist, jedoch eine noch kryptischere Syntax aufweist, als Smarty ohnehin schon.
Wenn man diverse PHP-Anwendungen warten und ggf. sogar umprogrammieren muss, kommt man nicht zuletzt auch einmal an der – für mich bisher schlimmsten – Form von Templating vorbei, die gegenüber dem Umstand, weshalb man eigentlich Templating nutzt nahezu nichts ändert: PHP Templates. Eine Mischung aus Wir wöllten gern und könnens aber nicht. Diese Abart von Templates kommt bevorzugt in CMS (Code Mangling Systems) zum Einsatz – also gerade da, wo das System eh schon komplex genug ist, als dass man erwarten könnte, wenigstens das Bearbeiten des Seitenstils stark zu vereinfachen.
Anforderungen an meinen Templater
Nun möchte ich in absehbarer Zeit ein Projekt anfertigen, bei dem ich auf recht alter Hardware arbeiten muss, womit bei meiner Wahl Schwergewichte wie Smarty und Dwoo von vornherin ausfallen. Abarten wie PHP-Templates natürlich auch, da Templates zum Steigern der Übersichtlichkeit gedacht sind. Und obwohl ich die beiden Template-Systeme für’s phpBB eigentlich recht gut mag, fallen diese auf Grund ihrer starken Integration ins phpBB für meine Zwecke auch flach, obwohl ich auf diesen aufbauend bereits ein anderes Projekt gebaut habe, was bisher mehr als zufriedenstellend läuft.
Da somit jegliche Templatesysteme mehr oder weniger ausgeschlossen sind, stellte sich die Frage, ob es eine eigenentwicklung werden sollte: Nach mehr oder weniger kurzem Überlegen stand die Antwort „Ja“ fest, da basierend auf den Erfahrungen mit GeSHi das Schreiben eines vernünftigen und vor allem schnellen Parsers kein Problem darstellen sollte.
Und so entstanden auch die Rahmenbedingungen und Anforderungenfür den Templater: Möglichst klein – 1kLOC klang hier als eine durchaus machbare Grenze, die dennoch eine Herausforderung darstellt -, eine dem phpBB-Parser ähnliche Syntax – auch wenn hier {$…} für Variablen ein wenig abweicht -, und möglichst einfach erweiterbar.
Ferner sollte der Templater Styling untersützen, verschiedene Ablageorte für Templates ermöglichen, Cachen können und KEIN eval beinhalten. Auch strikt untersagt war natürlich der Evil-Modifier, zu dem ich ja bereits an anderer Stelle etwas geschrieben hatte.
Eine weitere Grundlage für das Design war die Nutzung von PHP5 und seiner Möglichkeiten. Dies hieß nicht nur, dass Klassen zum Einsatz kommen sollten, sondern mit den Klassen auch Code-Reduktion und Redundanz-Vermeidung zu betreiben war – mehr oder weniger durch die begrenzte Anzahl verfürbarer Code-Zeilen ;-).
Funktionsweise des Systems
Was nun gegenüber den vorab erwähnten Templatern neu ist, ist die Möglichkeit von Subtemplating, d.h. Rendern von Templates, die aus anderen Templates zusammengesetzt werden. der phpBB-Templater löste diese Aufgabe mit verschiedenen Template-Handles, die man dann zwischen den Templates kopiert hat. Dies war nicht nur aufwändig zu programmieren, sondern war auch sehr unflexibel, da es für häufig vorkommende Boxen, in denen weitere Elemente zu platzieren sind immer eigenen Code zum Rendern dieser Elemente benötigte. Hier sollte mein Templater von vornherein eine effizientere Lösung anbieten, die auch ohne Code auf der Seite der Anwendung diese doch recht häufig anzutreffende Form der Style-Nutzung ermöglicht: Halt Subtemplating durch Anweisungen im Template.
Dieses Feature wird dabei, ähnlich wie andere Erweiterungsfunktionen durch eine Markierung im Template aktiviert:
<!--#TPL name="sub.tpl"#-->
<!--#TPLParam name="param1"#-->Hallo Welt!<!--#/TPLParam#-->
<!--#/TPL#-->
Hierbei gibt der TPL-Tag an, dass ein weiteres Template einzubinden ist. Um dieses nun an den aufrufenden Kontext anzupassen, können eine Reihe von TPLParams (Template Parameter) übergeben werden. Diese werden in diesem Subtemplate als ganz normale Variablen eingeblendet, so dass es für ein Template keinen Unterschied macht, wo es in einer Seite aufgerufen wird. Bei diesem Einblenden überschreiben die TPLParams aus dem Elternkontext geerbte Variablen, um auch das Verschachteln von Templates zu ermöglichen.
In diesem Zusammenhang ist dabei auch das Variablenhandling des 1kLOC-Templaters interessant. Während andere Systeme im Wesentlichen Strings verarbeiten bietet der 1kLOC-Templater explizit die Möglichkeit auch ganze Objekte in den Variablen-Baum einzupflegen und damit die Daten virtuell in den Template-Kontext einzubinden. Auf diese Art wird es möglich, dass man ein User-Objekt, dessen Daten eh bereits vorhanden sind, einfach über eine kurze Schnittstelle an das System anbinden muss und somit der Templater die Daten live aus dem Objekt liest, statt dass diese explizit kopiert werden müssen – das Escaping der Daten muss dabei bei der Anbindung der Schnittstelle gemacht werden, was jedoch kein großes Problem darstellt.
Als Parser kommt wie bereits erwähnt das Wissen aus der Programmierung am GeSHi zum Einsatz. Um das Template möglichst schnell verarbeiten zu können wird hierbei ähnlich wie dies auch im GeSHi v1.0.X realisiert ist, nach dem frühsten möglichen Kontextwechsel gesucht, Text bis dorthin als Text-Knoten interpretiert und der gefundene Subkontext weiterverarbeitet. Dieses Parsing passiert hierbei unter Nutzung von Callback-Funktionen, was die einfache Weiterverwendung im Template Compiler ermöglicht, der die linear vom Parser erhaltenen Tokens in einen PArsing Tree konveriteren kann und anschließend aus diesem Baum den Cache-Source erstellen kann. Erstmals kam diese Art des Parsings im GeSHI jedoch nicht bei 1.0.X zum Einsatz, sondern wurde mehr oder weniger aus GeSHi 1.1.X gebackported, wobei dort noch eine Reihe anderer Faktoren eine wesentliche Rolle spielen und somit das Parsing ein wenig aufwändiger als beim nahezu ausgereizten GeSHi 1.0.X ist.
Die Struktur vom 1kLOC Templater ist hierbei relativ einfach, aber dennoch sehr flexibel. Zentrale Aufrufstelle ist hierbei die TTemplater-Klasse. Diese stellt keine zwar keine Wesentliche Logik bereit, vereinfacht aber das Erzeugen von TEmplate-Objekten, ohne dass hierfür jedes Mal alle Referenzen für die anderen verwendeten Klassen übergeben werden müssen. Mit der über LoadTemplate erreichbaren Klasse TTemplate erhält man nun die Repräsentation aller zu einem Tempalte gehörigen Datenstände, d.h. dem Styler, dem Cache, dem Compiler sowie einem Variablen-Kontext, der die im Template verwendbaren Variablen beinhaltet.
Der Styler wiederum kümmert sich um das Lesen von Rohdaten für ein Template. Diesem wird vom Rest des Tempalte-Systems nur der Name des Tempaltes übergeben. Weitere Angaben wie Verzeichnisse, Datenbank-Verbindungen und dergleichen sind von der Implementation dieses Stylers abhängig, was es ermöglicht, sowohl Templates aus dem Dateisystem des Servers, aber auch wie z.B. bei phpKit der Fall aus der Datenbank zu lesen. Um ein Mapping dieser Templaterohdaten auf den Cache zu ermöglichen, muss jeder Styler für ein Template eine eindeutige ID liefern können. Diese wird im weiteren Verlauf der Verarbeitung als GUID für dieses Tempalte genutzt und dient je nach Cache später auch zum Ablegen und wiederfinden der compilierten Daten.
Der nächste Schritt beim Rendern eines Templates, nach dem dieses vom Styler gelesen wurde ist das Compilieren. Hierzu wird die Compiler-Klasse aufgerufen, der ein Parser vorgeschalten wird. Der Parser wird hierbei wenn nötig intern erzeugt, kann aber auch extern ausgetauscht werden. Dies ermöglicht die Wiederverwendung des Compilers, auch wenn sich das Layout bzw. die Art der Template-Speicherung geändert hat. Einzig die Art, wie der Parser die Tokens liefert muss mit dem Format und der Schnittstelle des Compilers kompatibel sein, um einen Austausch zu ermöglichen.
Nach dem der Compiler aus dem Template-Source eine Cache-fähige Version erzeugt hat, was sowohl PHP-Source (default) aber auch vollkommen andere Informationen wie Bytecode sein könnten, wird der Cache damit beauftragt, diese Informationen zwischenzuspeichern, um die recht aufwendige Arbeit des Compilierens wann immer möglich einzusparen.
Der Compiler ist beim 1kLOC-Templater neben der Umsetzung in Code auch mit dessen Ausführung beschäftigt, da er als einziger die genaue Art und Weise zu dessen Ausführung kennt. Somit ist im Endeffekt nicht mal festgelegt, wie ein compiliertes Template aussehen muss, was insbesondere dann Vorteile bietet, wenn durch zusätzliche Extensions effizientere Ausgabemöglichkeiten entstehen die Ausführung wesentlich beschleunigt werden kann, ohne den Templater an sich über den Haufen werfen zu müssen.
Nutzung des 1kLOC Templaters
Wer bereits einmal Erfahrungen mit den Templatern für phpBB gesammelt hat, dürfte mit der Nutzung meines Templaters nahezu keine Probleme haben, da die Nutzung sehr intuitiv gestaltet ist. Meine Demo-Seite besteht mit den beiden genutzten Templates aus lediglich folgenden Codezeilen:
<?php
include dirname(__FILE__).'/TTemplater.php';
$tpl = new TTemplater();
$main = $tpl->LoadTemplate('main.tpl');
$main->assignVar('foo', 42);
$main->assignBlock('bar', array('a'=>1, 'b'=>2, 'c'=>3));
$main->assignBlock('bar', array('a'=>4, 'b'=>5, 'c'=>6));
$main->assignBlock('bar', array('a'=>7, 'b'=>8, 'c'=>9));
$main->render();
?>
Die Templates sind dabei auch recht einfach aufgebaut. Da man ein HTML-Grundgerüst häufig benötigt, ist dieses als Subtemplate realisiert:
<html>
<head>
<title>{$title}</title>
</head>
<body bgcolor="#ffeecc">
<h1>It Works!</h1>
{$content}
</body>
</html>
Die eigentliche Seite ist in main.tpl realisiert und fordert darin an, in dieses Grundgerüst eingesetzt zu werden:
<!--#TPL name="structure.tpl"#-->
<!--#TPLParam name="title"#-->Hallo Welt!<!--#/TPLParam#-->
<!--#TPLParam name="content"#-->
The answer is {$foo}.<br />
<!--#If isset="foo" #-->
<b>Told you so!</b><br />
<!--#/If#-->
<br />
<!--#For name="bar" #-->
A:{$bar:a}, B:{$bar:b}, C:{$bar:c}<br />
<!--#/For#-->
<!--#/TPLParam#-->
<!--#/TPL#-->
Hierbei ist es möglich, nicht nur Skalare an assignVar zu übergeben, sondern sogar Objekte, sofern diese die __toString-Methode von PHP implementieren, um eine String-Repräsentation von sich selbst zu erzeugen. Dies ist für die TTemplate-Klasse durch einen internen Aufruf der render()-Methode und der Rückgabe der erzeugten Ausgabe realisiert. Diese Funktion kann aber durch zusätzliche Implementierung des ITPLContext-Interfaces noch erweitert werden, da dann das Template-System auch Anfragen an Unterkontexte erlaubt, d.h. das Objekt wie im Template-Kontext gespeicherte Daten behandelt und Aufrufe entsprechend weiterleitet. Somit lässt sich über einen minimalen Wrapper jedes Objekt direkt im Template-System nutzen ohne explizit jede einzelne Eigenschaft kopieren zu müssen.
Die Datenhaltung erfolgt aus Sicht des Template-Systems immer über Arrays, denen ein Name zugeordnet ist. Hierbei ist jeder Teilname durch einen Doppelpunkt getrennt (XML-Syntax). Die Auswertung der Namen erfolgt dabei von links nach rechts, wobei als Rückgabe eines Namens immer dessen aktueller Index zurückgeliefert wird. Das mehrfache setzen eines Wertes ergänzt weitere Werte zu diesem Namen. Diese Eigenschaft kann insbesondere für Schleifen genutzt werden, da sich somit recht einfach umfangreiche Datenstrukturen erzeugen lassen.
Möchte man nun neben den im Grundsystem enthaltenen Tags für If…Then…ElseIf…Else, For (ForElse kommt ggf. demnächst noch) sowie Subtemplating mehr haben, so kann man dies ganz einfach nachrüsten, indem man von der TTemplateCompiler_Tag-Klasse, bzw. für Blöcke von TTemplateCompiler_Block ableitet und den Tagnamen an diesen Grundnamen anhängt und die statische getCode-Methode implementiert. Die Code-Erzeugung läuft dabei über eine Callback-Funktion, der der zu erzeugende Code übergeben wird. Im erzeugten Code gibt es nun 5 Variablen von Bedeutung:
- $this = Der Compiler
- $tpl = Das Tempalte-Objekt, das gerendert wird
- $context = Der aktuelle Variablen-Kontext
- $subcontext = Temporäre Variable zum Erzeugen von Sukontexten
- $out = Callback für Datenausgabe
Diese Vorgehensweise ist jedoch von Compiler zu Compiler abhängig, wird in dieser Form jedoch vom Standardcompiler umgesetzt. Dies ist insofern zu beachten, da im Template-System sogar der Compiler ausgetauscht werden kann, wenn er einem nicht gefällt, oder es andere Gründe gibt, von der Standardimplementierung abzuweichen, um z.B. andere Tagformate zu unterstützen oder Vorteile von Beschleuniger-Extensions besser ausnutzen zu können.
Jetzt will ich das Teil endlich haben …
Kein Problem: Der Templater steht unter GPLv3, kann aber unter Nennung guter Gründe und Leistung wirkungsvoller Überzeugungsarbeit in Einzelfällen auch unter anderen Lizenzen bereitgestellt werden. 😉 Obwohl nicht zwingend notwendig, wäre zudem eine Rückmeldung über Neuerungen, Verbesserungen an mich wünschenswert, um Verbesserungen bereits Upstream einpflegen zu können. Den Download gibt’s >>> hier <<<. Die Standard-Konfiguration geht von zwei Unterverzeichnissen cache/ (Writeable) und tpl (Read-Only) aus, die bereits existieren müssen. Ansonsten einfach einbinden und freuen!
Für Rückfragen steh ich gerne bereit.
P.S.: Ja, ich weiß, dass da noch 150 Zeilen Platz sind …
Jep anerkannt. ^^
Kommentar by neo — 10.09.2009 @ 22:16:42