Eigene Tags erstellen
Diese Seite bietet eine umfassende Anleitung zum Erstellen eigener Tags in Latte. Wir gehen alles durch, von einfachen Tags bis zu komplexeren Szenarien mit verschachteltem Inhalt und besonderen Anforderungen ans Parsen, und bauen dabei auf Ihrem Verständnis davon auf, wie Latte Templates kompiliert.
Eigene Tags bieten die weitreichendste Kontrolle über die Syntax des Templates und die Rendering-Logik, sind aber auch der komplexeste Erweiterungspunkt. Bevor Sie sich für einen eigenen Tag entscheiden, prüfen Sie immer, ob es eine einfachere Lösung gibt oder ob ein passender Tag bereits im Standardsatz enthalten ist. Verwenden Sie eigene Tags nur dann, wenn die einfacheren Alternativen für Ihren Zweck nicht ausreichen.
Den Kompilierungsprozess verstehen
Um wirksam eigene Tags erstellen zu können, ist es hilfreich zu erklären, wie Latte Templates verarbeitet. Das Verständnis dieses Prozesses macht klar, warum Tags so aufgebaut sind, wie sie es sind, und wie sie sich ins Gesamtbild einfügen.
Die Kompilierung eines Templates in Latte umfasst, vereinfacht gesagt, diese wesentlichen Schritte:
- Lexing: Der Lexer liest den Quellcode des Templates (die
.latte-Datei) und zerlegt ihn in eine Folge kleiner, klar abgegrenzter Teile, die Tokens genannt werden (z. B.{,foreach,$variable,}, HTML-Text usw.). - Parsen: Der Parser nimmt diesen Strom von Tokens und baut daraus eine sinnvolle Baumstruktur, die Logik und Inhalt des Templates abbildet. Dieser Baum heißt abstrakter Syntaxbaum (AST).
- Compiler-Pässe: Bevor der PHP-Code erzeugt wird, führt Latte Compiler-Pässe aus. Das sind Funktionen, die den gesamten AST durchlaufen und ihn verändern oder Informationen sammeln können. Dieser Schritt ist entscheidend für Funktionen wie Sicherheit (Sandbox) oder Optimierungen.
- Code-Erzeugung: Zum Schluss durchläuft der Compiler den (möglicherweise veränderten) AST und erzeugt den entsprechenden Code der PHP-Klasse. Dieser PHP-Code ist es, der beim Ausführen das Template tatsächlich rendert.
- Caching: Der erzeugte PHP-Code wird auf der Festplatte gecacht, wodurch weitere Renderings sehr schnell sind, weil die Schritte 1 bis 4 entfallen.
Tatsächlich ist die Kompilierung etwas komplizierter. Latte hat zwei Lexer und Parser: einen für das HTML-Template und einen für den PHP-ähnlichen Code innerhalb der Tags. Außerdem läuft das Parsen nicht nach der Tokenisierung, sondern Lexer und Parser laufen parallel in zwei “Threads” und stimmen sich ab. Glauben Sie mir, David Grudl – das zu programmieren fühlte sich an wie Raketenwissenschaft :-)
Der gesamte Prozess, vom Laden des Template-Inhalts über das Parsen bis zum Erzeugen der Ergebnisdatei, lässt sich mit diesem Code nachvollziehen, mit dem Sie experimentieren und die Zwischenergebnisse ausgeben können:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Die Anatomie eines Tags
Das Erstellen eines vollständig funktionsfähigen eigenen Tags in Latte umfasst mehrere zusammenhängende Teile. Bevor wir uns in die Implementierung stürzen, sehen wir uns die grundlegenden Konzepte und Begriffe an – in Analogie zu HTML und zum Document Object Model (DOM).
Tags vs. Knoten (Analogie zu HTML)
In HTML schreiben wir Tags wie <p> oder <div>...</div>. Diese Tags sind
Syntax im Quellcode. Wenn ein Browser dieses HTML parst, erzeugt er eine Darstellung im Speicher, die Document Object Model
(DOM) heißt. Im DOM werden die HTML-Tags durch Knoten repräsentiert (genauer gesagt durch Element-Knoten
in der Terminologie des JavaScript-DOM). Mit diesen Knoten arbeiten wir programmatisch (zum Beispiel gibt
document.getElementById(...) in JavaScript einen Element-Knoten zurück). Der Tag ist nur die textuelle Darstellung
in der Quelldatei, der Knoten ist die Objektdarstellung im logischen Baum.
Latte funktioniert ähnlich:
- In einer
.latte-Template-Datei schreiben Sie Latte-Tags wie{foreach ...}und{/foreach}. Das ist die Syntax, mit der Sie als Autor des Templates arbeiten. - Wenn Latte das Template parst, baut es einen abstrakten Syntaxbaum (AST) auf. Dieser Baum besteht aus Knoten. Jeder Latte-Tag, jedes HTML-Element, jedes Stück Text und jeder Ausdruck im Template wird zu einem oder mehreren Knoten in diesem Baum.
- Die Basisklasse für alle Knoten im AST ist
Latte\Compiler\Node. Genauso wie das DOM verschiedene Knotentypen hat (Element, Text, Kommentar), hat auch der AST von Latte verschiedene Knotentypen. Sie begegnenLatte\Compiler\Nodes\TextNodefür statischen Text,Latte\Compiler\Nodes\Html\ElementNodefür HTML-Elemente,Latte\Compiler\Nodes\Php\ExpressionNodefür Ausdrücke innerhalb von Tags und – für eigene Tags entscheidend – Knoten, die vonLatte\Compiler\Nodes\StatementNodeerben.
Warum StatementNode?
HTML-Elemente (Html\ElementNode) repräsentieren vor allem Struktur und Inhalt. PHP-Ausdrücke
(Php\ExpressionNode) repräsentieren Werte oder Berechnungen. Aber was ist mit Latte-Tags wie {if},
{foreach} oder unserem eigenen {datetime}? Diese Tags führen Aktionen aus, steuern den
Programmablauf oder erzeugen anhand einer Logik eine Ausgabe. Sie sind die funktionalen Einheiten, die Latte zu einer mächtigen
Templating-Engine machen und nicht bloß zu einer Auszeichnungssprache.
In der Programmierung nennt man solche aktionsausführenden Einheiten oft “Statements”. Deshalb erben die Knoten, die diese
funktionalen Latte-Tags repräsentieren, üblicherweise von Latte\Compiler\Nodes\StatementNode. Das unterscheidet sie
von rein strukturellen Knoten (wie HTML-Elementen) oder von Knoten, die Werte darstellen (wie Ausdrücke).
Die wichtigsten Komponenten
Sehen wir uns die Hauptkomponenten an, die zum Erstellen eines eigenen Tags nötig sind:
Tag-Parsing-Funktion
- Dieses PHP-Callable parst die Syntax des Latte-Tags (
{...}) im Quelltext des Templates. - Über ein Objekt Latte\Compiler\Tag erhält sie
Informationen über den Tag (etwa seinen Namen, seine Position und ob es sich um ein n:Attribut handelt) sowie als zweites
Argument den Haupt-Parser Latte\Compiler\TemplateParser. Ihre vollständige
Signatur lautet
callable(Tag, TemplateParser): (Node|\Generator|void). - Ihr wichtigstes Werkzeug zum Parsen der Argumente und Ausdrücke innerhalb der Tag-Begrenzer ist das Objekt Latte\Compiler\TagParser, zugänglich über
$tag->parser(das ist ein anderer Parser als der, der das gesamte Template parst). - Bei Paar-Tags verwendet sie
yield, um Latte zu signalisieren, dass es den inneren Inhalt zwischen dem öffnenden und dem schließenden Tag parsen soll. - Das eigentliche Ziel der Parsing-Funktion ist es, eine Instanz der Node-Klasse zu erzeugen und zurückzugeben, die dann in den AST eingefügt wird.
- Es ist üblich (aber nicht vorgeschrieben), die Parsing-Funktion als statische Methode (oft
creategenannt) direkt in der zugehörigen Node-Klasse zu implementieren. Das hält die Parsing-Logik und die Repräsentation des Knotens sauber beieinander, erlaubt bei Bedarf den Zugriff auf private und geschützte Klassenelemente und verbessert die Übersichtlichkeit.
Node-Klasse
- Repräsentiert die logische Funktion Ihres Tags innerhalb des abstrakten Syntaxbaums (AST).
- Hält die geparsten Informationen (etwa Argumente oder Inhalt) in öffentlichen Properties. Diese Properties enthalten oft
weitere
Node-Instanzen (z. B.ExpressionNodefür geparste Argumente,AreaNodefür geparsten Inhalt). - Die Methode
print(PrintContext $context): stringerzeugt den PHP-Code (ein Statement oder eine Folge von Statements), der beim Rendern des Templates die Aktion des Tags ausführt. - Die Methode
getIterator(): \Generatormacht die Kindknoten (Argumente, Inhalt) für das Durchlaufen durch Compiler-Pässe zugänglich. Sie muss Referenzen (&) liefern, damit die Pässe die Unterknoten verändern oder ersetzen können. - Nachdem das gesamte Template in einen AST geparst wurde, führt Latte eine Reihe von Compiler-Pässen aus. Diese Pässe durchlaufen den gesamten AST über
die Methode
getIterator(), die jeder Knoten bereitstellt. Sie können Knoten untersuchen, Informationen sammeln und den Baum sogar verändern (etwa indem sie die öffentlichen Properties von Knoten ändern oder Knoten vollständig ersetzen). Dieser Entwurf, der ein vollständigesgetIterator()verlangt, ist entscheidend. Er erlaubt mächtige Funktionen wie die Sandbox, das Verhalten jedes Teils des Templates – einschließlich Ihrer eigenen Tags – zu analysieren und gegebenenfalls zu ändern, was Sicherheit und Konsistenz gewährleistet.
Registrierung über eine Extension
- Sie müssen Latte von Ihrem neuen Tag erzählen und davon, welche Parsing-Funktion dafür zu verwenden ist. Das geschieht in einer Latte-Extension.
- In Ihrer Extension-Klasse implementieren Sie die Methode
getTags(): array. Diese Methode gibt ein assoziatives Array zurück, dessen Schlüssel die Namen der Tags sind (z. B.'mytag','n:myattribute') und dessen Werte die PHP-Callables der jeweiligen Parsing-Funktionen sind (z. B.MyNamespace\DatetimeNode::create(...)).
Zusammengefasst: Die Tag-Parsing-Funktion verwandelt den Quelltext Ihres Tags im Template in einen
AST-Knoten. Die Node-Klasse weiß dann, wie sie sich selbst in ausführbaren PHP-Code für das
kompilierte Template verwandelt, und stellt ihre Unterknoten über getIterator() den Compiler-Pässen zur
Verfügung. Die Registrierung über eine Extension verbindet den Namen des Tags mit der Parsing-Funktion und macht ihn
Latte bekannt.
Sehen wir uns nun Schritt für Schritt an, wie sich diese Komponenten implementieren lassen.
Einen einfachen Tag erstellen
Stürzen wir uns auf Ihren ersten eigenen Latte-Tag. Wir beginnen mit einem sehr einfachen Beispiel: einem Tag namens
{datetime}, der das aktuelle Datum und die aktuelle Uhrzeit ausgibt. Anfangs nimmt dieser Tag keine Argumente
entgegen, aber wir erweitern ihn später im Abschnitt Parsen der Tag-Argumente. Er
hat auch keinen inneren Inhalt.
Dieses Beispiel führt Sie durch die wesentlichen Schritte: das Definieren der Node-Klasse, das Implementieren ihrer Methoden
print() und getIterator(), das Erstellen der Parsing-Funktion und schließlich das Registrieren
des Tags.
Ziel: {datetime} so implementieren, dass es mit der PHP-Funktion date() das aktuelle Datum und
die aktuelle Uhrzeit ausgibt.
Erstellen der Node-Klasse
Zuerst brauchen wir eine Klasse, die unseren Tag im abstrakten Syntaxbaum (AST) repräsentiert. Wie oben besprochen, erben wir
von Latte\Compiler\Nodes\StatementNode.
Legen Sie eine Datei an (z. B. DatetimeNode.php) und definieren Sie die Klasse:
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Tag-Parsing-Funktion, aufgerufen, wenn {datetime} gefunden wird.
*/
public static function create(Tag $tag): self
{
// Unser Tag gibt Inhalt aus, deshalb behalten wir die umgebende Einrückung bei
$tag->outputMode = $tag::OutputKeepIndentation;
// Unser einfacher Tag nimmt derzeit keine Argumente entgegen, wir müssen also nichts parsen
$node = $tag->node = new self;
return $node;
}
/**
* Erzeugt den PHP-Code, der beim Rendern des Templates ausgeführt wird.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Stellt den Zugriff auf die Kindknoten für die Compiler-Pässe von Latte bereit.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Wenn Latte im Template auf {datetime} stößt, ruft es die Tag-Parsing-Funktion create() auf. Ihre
Aufgabe ist es, eine Instanz von DatetimeNode zurückzugeben. Außerdem setzen wir $tag->outputMode
auf OutputKeepIndentation; weil ein Tag im Standardmodus OutputNone läuft (erklärt in Ausgabemodi von Tags), könnte ein Tag, der vor dem ersten Text des Templates steht, seine
Ausgabe sonst in der erzeugten Methode prepare() statt in main() erzeugen. Das Setzen dieses Modus
stellt sicher, dass die Ausgabe dort landet, wo der Tag steht.
Die Methode print() erzeugt den PHP-Code, der beim Rendern des Templates ausgeführt wird. Wir rufen die Methode
$context->format() auf, die den resultierenden String mit PHP-Code für das kompilierte Template zusammensetzt.
Das erste Argument, 'echo date('Y-m-d H:i:s') %line;', ist die Maske, in die die folgenden Parameter eingesetzt
werden. Der Platzhalter %line sagt der Methode format(), sie solle das nächste Argument nehmen, also
$this->position, und einen Kommentar wie /* pos 15:1 */ einfügen, der den erzeugten PHP-Code mit der
ursprünglichen Zeile des Templates verknüpft, was für das Debugging entscheidend ist.
Die Property $this->position wird von der Basisklasse Node geerbt und automatisch vom Parser von
Latte gesetzt. Sie enthält ein Objekt Latte\Compiler\Range (eine Unterklasse von
Position, erweitert um eine length in Bytes), das angibt, wo sich der Tag in der
.latte-Quelldatei befindet. Bei Paar-Tags reicht der Bereich vom öffnenden bis zum schließenden Tag, und Nachfahren
von StatementNode bieten zusätzlich $this->tagRanges, das den Range jedes beteiligten
Tags auflistet (öffnender Tag, Zwischen-Tags wie {else}/{case} und schließender Tag).
Die Methode getIterator() ist für die Compiler-Pässe unerlässlich. Sie muss alle Kindknoten liefern, aber unser
einfacher DatetimeNode hat derzeit weder Argumente noch Inhalt, also auch keine Kindknoten. Die Methode muss dennoch
existieren und ein Generator sein, das Schlüsselwort yield muss also irgendwie im Methodenrumpf vorkommen.
Registrierung über eine Extension
Zum Schluss teilen Sie Latte den neuen Tag mit. Erstellen Sie eine Extension-Klasse (z. B.
MyLatteExtension.php) und registrieren Sie den Tag in ihrer Methode getTags().
<?php
namespace App\Templating;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Gibt die Liste der Tags zurück, die diese Extension bereitstellt.
* @return array<string, callable> Map: 'tag-name' => Parsing-Funktion
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Hier später weitere Tags registrieren
];
}
}
Registrieren Sie diese Extension anschließend bei der Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
Erstellen Sie ein Template:
<p>Seite erzeugt am: {datetime}</p>
Erwartete Ausgabe: <p>Seite erzeugt am: 2023-10-27 11:00:00</p>
Zusammenfassung dieser Phase
Wir haben erfolgreich einen einfachen eigenen Tag {datetime} erstellt. Wir haben seine Repräsentation im AST
definiert (DatetimeNode), sein Parsen umgesetzt (create()), festgelegt, wie er PHP-Code erzeugen soll
(print()), dafür gesorgt, dass seine Kinder durchlaufen werden können (getIterator()), und ihn bei
Latte registriert.
Im nächsten Abschnitt erweitern wir diesen Tag so, dass er Argumente entgegennimmt, und zeigen dabei, wie man Ausdrücke parst und Kindknoten verwaltet.
Parsen der Tag-Argumente
Unser einfacher Tag {datetime} funktioniert, ist aber nicht besonders flexibel. Erweitern wir ihn so, dass er ein
optionales Argument entgegennimmt: einen Format-String für die Funktion date(). Die gewünschte Syntax lautet
{datetime $format}.
Ziel: {datetime} so ändern, dass es einen optionalen PHP-Ausdruck als Argument entgegennimmt, der als
Format-String für date() verwendet wird.
Vorstellung des TagParser
Bevor wir den Code ändern, ist es wichtig, das Werkzeug zu verstehen, das wir verwenden werden: Latte\Compiler\TagParser. Wenn der Haupt-Parser von
Latte (TemplateParser) auf einen Latte-Tag wie {datetime ...} oder auf ein n:Attribut stößt, delegiert
er das Parsen des Inhalts innerhalb des Tags (des Teils zwischen { und } bzw. des Werts des
Attributs) an einen spezialisierten TagParser.
Dieser TagParser arbeitet ausschließlich mit den Argumenten des Tags. Seine Aufgabe ist es, die Tokens
dieser Argumente zu konsumieren. Entscheidend ist: Er muss den gesamten übergebenen Inhalt parsen. Wenn Ihre
Parsing-Funktion endet, der TagParser aber das Ende der Argumente noch nicht erreicht hat (prüfbar über
$tag->parser->isEnd()), wirft Latte eine Exception, denn das bedeutet, dass unerwartete Tokens im Tag übrig
geblieben sind. Umgekehrt sollten Sie, wenn ein Tag Argumente verlangt, am Anfang Ihrer Parsing-Funktion
$tag->expectArguments() aufrufen. Diese Methode prüft, ob Argumente vorhanden sind, und wirft eine hilfreiche
Exception, wenn der Tag ohne Argumente verwendet wurde.
Der TagParser bietet nützliche Methoden zum Parsen verschiedener Arten von Argumenten:
parseExpression(): ExpressionNode: Parst einen PHP-ähnlichen Ausdruck (Variablen, Literale, Operatoren, Funktions- und Methodenaufrufe usw.). Er beherrscht den Syntaxzucker von Latte, etwa dass einfache alphanumerische Strings wie Strings in Anführungszeichen behandelt werden (z. B. wirdfoogeparst, als stünde dort'foo').parseUnquotedStringOrExpression(): ExpressionNode: Parst entweder einen üblichen Ausdruck oder einen String ohne Anführungszeichen. Das sind Zeichenfolgen, die Latte ohne Anführungszeichen zulässt und die oft für Dinge wie Dateipfade verwendet werden (z. B.{include ../file.latte}). Parst er einen String ohne Anführungszeichen, gibt er einenStringNodezurück.parseArguments(): ArrayNode: Parst durch Kommas getrennte Argumente, gegebenenfalls mit Schlüsseln, etwa10, name: 'John', true.parseModifier(): ModifierNode: Parst Filter wie|upper|truncate:10.parseType(): ?SuperiorTypeNode: Parst PHP-Typdeklarationen wieint,?string,array|Foo.
Für komplexeres oder tiefer liegendes Parsen können Sie über $tag->parser->stream direkt mit dem Token-Stream arbeiten. Dieses Objekt bietet
Methoden, um einzelne Tokens zu untersuchen und zu konsumieren:
$tag->parser->stream->is(...): bool: Prüft, ob das aktuelle Token einem der angegebenen Typen (z. B.Token::Php_Variable) oder einem der angegebenen literalen Werte (z. B.'as') entspricht, ohne es zu konsumieren. Nützlich zum Vorausschauen.$tag->parser->stream->consume(...): Token: Konsumiert das aktuelle Token und rückt die Position im Stream vor. Werden erwartete Token-Typen oder -Werte als Argumente angegeben und das aktuelle Token passt nicht dazu, wirft die Methode eineCompileException. Verwenden Sie sie, wenn Sie ein bestimmtes Token erwarten.$tag->parser->stream->tryConsume(...): ?Token: Versucht das aktuelle Token nur dann zu konsumieren, wenn es einem der angegebenen Typen oder Werte entspricht. Passt es, konsumiert sie das Token und gibt es zurück. Passt es nicht, lässt sie die Position im Stream unverändert und gibtnullzurück. Verwenden Sie sie für optionale Tokens oder wenn Sie zwischen verschiedenen Syntaxvarianten wählen.
Aktualisierung der Parsing-Funktion create()
Mit diesem Wissen ändern wir nun die Methode create() in DatetimeNode so, dass sie das optionale
Format-Argument mit $tag->parser parst.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
// Neue öffentliche Property für den geparsten Knoten des Format-Ausdrucks
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Prüfen, ob überhaupt Tokens vorhanden sind
if (!$tag->parser->isEnd()) {
// Das Argument mit dem TagParser als PHP-ähnlichen Ausdruck parsen.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... die Methoden print() und getIterator() aktualisieren wir als Nächstes ...
}
Wir haben die öffentliche Property $format ergänzt. In create() prüfen wir nun mit
$tag->parser->isEnd(), ob Argumente vorhanden sind. Wenn ja, konsumiert
$tag->parser->parseExpression() die Tokens des Ausdrucks. Weil der TagParser alle seine
Eingabe-Tokens konsumieren muss, meldet Latte automatisch einen Fehler, wenn der Benutzer hinter dem Format-Ausdruck etwas
Unerwartetes schreibt (z. B. {datetime 'Y-m-d', unexpected}).
Aktualisierung der Methode print()
Ändern wir nun die Methode print() so, dass sie den geparsten Format-Ausdruck aus $this->format
verwendet. Wurde kein Format angegeben ($this->format ist null), sollten wir einen
Standard-Format-String verwenden, zum Beispiel 'Y-m-d H:i:s'.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node gibt die PHP-Code-Darstellung von $formatNode aus.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
In der Variablen $formatNode speichern wir den AST-Knoten, der den Format-String für die PHP-Funktion
date() repräsentiert. Wir verwenden hier den Null-Koaleszenz-Operator (??). Hat der Benutzer im
Template ein Argument angegeben (z. B. {datetime 'd.m.Y'}), enthält die Property $this->format den
entsprechenden Knoten (in diesem Fall einen StringNode mit dem Wert 'd.m.Y'), und dieser Knoten wird
verwendet. Hat der Benutzer kein Argument angegeben (also nur {datetime} geschrieben), ist die Property
$this->format gleich null, und wir erzeugen stattdessen einen neuen StringNode mit dem
Standardformat 'Y-m-d H:i:s'. So enthält $formatNode immer einen gültigen AST-Knoten für das
Format.
In der Maske 'echo date(%node) %line;' wird der neue Platzhalter %node verwendet. Er sagt der Methode
format(), sie solle das erste folgende Argument nehmen (also unseren $formatNode), dessen Methode
print() aufrufen (die seine PHP-Code-Darstellung zurückgibt) und das Ergebnis an der Stelle des Platzhalters
einfügen.
getIterator() für Unterknoten implementieren
Unser DatetimeNode hat jetzt einen Kindknoten: den Ausdruck $format. Wir müssen diesen
Kindknoten für die Compiler-Pässe zugänglich machen, indem wir ihn in der Methode getIterator() liefern. Denken
Sie daran, eine Referenz (&) zu liefern, damit die Pässe den Knoten gegebenenfalls ersetzen können.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Warum ist das entscheidend? Stellen Sie sich einen Sandbox-Pass vor, der prüfen muss, ob das Argument $format
einen verbotenen Funktionsaufruf enthält (z. B. {datetime dangerousFunction()}). Liefert getIterator()
das $this->format nicht, würde der Sandbox-Pass den Aufruf von dangerousFunction() im Argument
unseres Tags nie sehen, und es entstünde eine potenzielle Sicherheitslücke. Indem wir ihn liefern, erlauben wir der Sandbox (und
anderen Pässen), den Knoten des Ausdrucks $format zu untersuchen und gegebenenfalls zu verändern.
Verwendung des erweiterten Tags
Der Tag verarbeitet nun ein optionales Argument korrekt:
Standardformat: {datetime}
Eigenes Format: {datetime 'd.m.Y'}
Verwendung einer Variablen: {datetime $userDateFormatPreference}
{* Das würde nach dem Parsen von 'd.m.Y' einen Fehler verursachen, weil ", foo" unerwartet ist *}
{* {datetime 'd.m.Y', foo} *}
Als Nächstes sehen wir uns an, wie man Paar-Tags erstellt, die den Inhalt zwischen sich verarbeiten.
Umgang mit Paar-Tags
Bisher ist unser Tag {datetime} (begrifflich) selbstschließend. Er hat keinen Inhalt zwischen einem
öffnenden und einem schließenden Tag. Viele nützliche Tags arbeiten jedoch mit einem Block von Template-Inhalt. Sie heißen
Paar-Tags. Beispiele sind {if}...{/if}, {block}...{/block} oder der eigene Tag, den wir jetzt
bauen: {debug}...{/debug}.
Mit diesem Tag können wir Debugging-Informationen in unsere Templates aufnehmen, die nur während der Entwicklung sichtbar sein sollen.
Ziel: Einen Paar-Tag {debug} erstellen, dessen Inhalt nur dann gerendert wird, wenn ein bestimmtes Flag
für den “Entwicklungsmodus” aktiv ist.
Vorstellung der Provider
Manchmal brauchen Ihre Tags Zugriff auf Daten oder Dienste, die nicht direkt als Template-Parameter übergeben werden – etwa um festzustellen, ob die Anwendung im Entwicklungsmodus läuft, um auf ein Benutzerobjekt zuzugreifen oder um Konfigurationswerte zu holen. Latte stellt dafür einen Mechanismus namens Provider bereit.
Provider werden in Ihrer Extension über die
Methode getProviders() registriert. Diese Methode gibt ein assoziatives Array zurück, dessen Schlüssel die Namen
sind, unter denen die Provider im Laufzeitcode des Templates zugänglich sind, und dessen Werte die eigentlichen Daten oder
Objekte sind.
Im PHP-Code, den die Methode print() Ihres Tags erzeugt, greifen Sie dann über die spezielle Objekt-Property
$this->global auf diese Provider zu. Weil diese Property von allen Extensions gemeinsam genutzt wird, ist es eine
gute Praxis, die Namen Ihrer Provider mit einem Präfix zu versehen, um mögliche Namenskollisionen mit den Kern-Providern
von Latte oder mit Providern anderer Extensions zu vermeiden. Üblich ist ein kurzes, eindeutiges Präfix, das sich am Namen Ihres
Herstellers oder Ihrer Extension orientiert. Für unser Beispiel verwenden wir das Präfix app, und das Flag für den
Entwicklungsmodus ist als $this->global->appDevMode verfügbar.
Das Schlüsselwort yield zum Parsen von Inhalt
Wie sagen wir dem Parser von Latte, dass er den Inhalt zwischen {debug} und {/debug}
verarbeiten soll? Hier kommt das Schlüsselwort yield ins Spiel.
Wird yield in der Funktion create() verwendet, wird sie zu einem PHP-Generator. Ihre Ausführung pausiert, und die
Kontrolle geht an den Haupt-Parser TemplateParser zurück. Der TemplateParser parst den Inhalt des
Templates dann weiter, bis er auf den zugehörigen schließenden Tag stößt (in unserem Fall {/debug}).
Sobald der schließende Tag gefunden ist, setzt der TemplateParser die Ausführung unserer Funktion
create() direkt hinter der yield-Anweisung fort. Der von yield zurückgegebene Wert
ist ein Array mit zwei Elementen:
- einem
AreaNode, der den geparsten Inhalt zwischen dem öffnenden und dem schließenden Tag repräsentiert, - dem
Tag-Objekt, das den schließenden Tag repräsentiert (z. B.{/debug}).
Erstellen wir die Klasse DebugNode und ihre Methode create mit yield.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Öffentliche Property für den geparsten inneren Inhalt
public AreaNode $content;
/**
* Parsing-Funktion für den Paar-Tag {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // beachten Sie den Rückgabetyp
{
$node = $tag->node = new self;
// Parsen pausieren, inneren Inhalt und schließenden Tag erhalten, sobald {/debug} gefunden wird
[$node->content, $endTag] = yield;
return $node;
}
// ... print() und getIterator() implementieren wir als Nächstes ...
}
Hinweis: $endTag ist null, wenn der Tag als n:Attribut verwendet wird, also
<div n:debug>...</div>.
Ein Paar-Tag lässt sich auch mit einem Schrägstrich schließen, etwa {debug/} (oder
<div n:debug/>). Er hat dann keinen inneren Inhalt: Der Generator erhält
[$emptyFragmentNode, $startTag], wobei das zweite Element der öffnende Tag selbst ist und nicht
null.
print() für bedingtes Rendern implementieren
Die Methode print() muss nun PHP-Code erzeugen, der zur Laufzeit den Provider appDevMode prüft und
den Code für den inneren Inhalt nur dann ausführt, wenn das Flag wahr ist.
public function print(PrintContext $context): string
{
// Erzeugt ein PHP-'if'-Statement, das den Provider zur Laufzeit prüft
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Im Entwicklungsmodus den inneren Inhalt ausgeben
%node
}
XX,
$this->position, // Für den %line-Kommentar
$this->content, // Der Knoten mit dem AST des inneren Inhalts
);
}
Das ist unkompliziert. Mit PrintContext::format() erzeugen wir ein übliches PHP-if-Statement.
Innerhalb des if setzen wir den Platzhalter %node für $this->content. Latte ruft
rekursiv $this->content->print($context) auf, um den PHP-Code für den inneren Teil des Tags zu erzeugen –
aber nur, wenn $this->global->appDevMode zur Laufzeit als wahr ausgewertet wird.
getIterator() für den Inhalt implementieren
Genau wie beim Argumentknoten im vorigen Beispiel hat unser DebugNode nun einen Kindknoten: den
AreaNode $content. Wir müssen ihn durchlaufbar machen, indem wir ihn in getIterator() liefern:
public function &getIterator(): \Generator
{
// Die Referenz auf den Inhaltsknoten liefern
yield $this->content;
}
Das erlaubt es den Compiler-Pässen, in den Inhalt unseres Tags {debug} hinabzusteigen, was auch dann wichtig ist,
wenn der Inhalt nur bedingt gerendert wird. Die Sandbox muss den Inhalt zum Beispiel unabhängig davon analysieren, ob
appDevMode wahr oder falsch ist.
Registrierung und Verwendung
Registrieren Sie den Tag und den Provider in Ihrer Extension:
class MyLatteExtension extends Extension
{
// Angenommen, $isDevelopmentMode wird irgendwo ermittelt (z. B. aus der Konfiguration)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Den neuen Tag registrieren
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Den Provider registrieren
];
}
}
// Beim Registrieren der Extension:
$isDev = true; // Bestimmen Sie das anhand der Umgebung Ihrer Anwendung
$latte->addExtension(new MyLatteExtension($isDev));
Und die Verwendung im Template:
<p>Gewöhnlicher Inhalt, immer sichtbar.</p>
{debug}
<div class="debug-panel">
ID des aktuellen Benutzers: {$user->id}
Zeit des Requests: {=time()}
</div>
{/debug}
<p>Weiterer gewöhnlicher Inhalt.</p>
Integration von n:Attributen
Latte bietet für viele Paar-Tags eine bequeme Kurzschreibweise: n:Attribute. Wenn Sie einen Paar-Tag wie {tag}...{/tag}
haben und seine Wirkung direkt auf ein einzelnes HTML-Element anwenden möchten, können Sie ihn oft kürzer als Attribut
n:tag an diesem Element schreiben.
Für die meisten Paar-Tags, die Sie definieren (wie unseren {debug}), aktiviert Latte die entsprechende
n:-Attribut-Variante automatisch. Bei der Registrierung müssen Sie nichts weiter tun:
{* Übliche Verwendung als Paar-Tag *}
{debug}<div>Debug-Informationen</div>{/debug}
{* Gleichwertige Verwendung als n:Attribut *}
<div n:debug>Debug-Informationen</div>
Beide rendern das <div> nur dann, wenn $this->global->appDevMode wahr ist. Auch die
Präfixe inner- und tag- funktionieren wie erwartet.
Manchmal muss sich die Logik Ihres Tags leicht anders verhalten, je nachdem, ob er als üblicher Paar-Tag oder als n:Attribut
verwendet wird oder ob ein Präfix wie n:inner-tag bzw. n:tag-tag im Spiel ist. Das Objekt
Latte\Compiler\Tag, das Ihrer Parsing-Funktion create() übergeben wird, liefert diese Information:
$tag->isNAttribute(): bool: Gibttruezurück, wenn der Tag als n:Attribut geparst wird$tag->prefix: ?string: Gibt das mit dem n:Attribut verwendete Präfix zurück, dasnullsein kann (kein n:Attribut),Tag::PrefixNone,Tag::PrefixInneroderTag::PrefixTag
Nachdem wir nun einfache Tags, das Parsen von Argumenten, Paar-Tags, Provider und n:Attribute verstanden haben, nehmen wir uns
ein komplexeres Szenario vor: Tags, die in anderen Tags verschachtelt sind, ausgehend von unserem Tag {debug}.
Zwischen-Tags
Manche Paar-Tags erlauben oder verlangen sogar, dass vor dem abschließenden schließenden Tag weitere Tags in ihnen
erscheinen. Sie heißen Zwischen-Tags. Klassische Beispiele sind {if}...{elseif}...{else}...{/if} oder
{switch}...{case}...{default}...{/switch}.
Erweitern wir unseren Tag {debug} um eine optionale {else}-Klausel, die gerendert wird, wenn die
Anwendung nicht im Entwicklungsmodus läuft.
Ziel: {debug} so ändern, dass er einen optionalen Zwischen-Tag {else} unterstützt. Die
endgültige Syntax soll {debug} ... {else} ... {/debug} lauten.
Zwischen-Tags mit yield parsen
Wir wissen bereits, dass yield die Parsing-Funktion create() pausiert und den geparsten Inhalt
zusammen mit dem schließenden Tag zurückgibt. yield bietet jedoch mehr Kontrolle: Sie können ihm ein Array mit
Namen von Zwischen-Tags übergeben. Wenn der Parser auf einen dieser angegebenen Tags auf derselben
Verschachtelungsebene stößt (also als direkte Kinder des übergeordneten Tags, nicht innerhalb anderer Blöcke oder Tags
darin), hört er ebenfalls auf, den Inhalt zu parsen.
Endet das Parsen wegen eines Zwischen-Tags, hört es mit dem Inhalt auf, setzt den Generator create() fort und
übergibt den teilweise geparsten Inhalt sowie den Zwischen-Tag selbst (statt des abschließenden schließenden Tags).
Unsere Funktion create() kann diesen Zwischen-Tag dann behandeln (etwa seine Argumente parsen, falls er welche hat)
und erneut yield aufrufen, um den nächsten Teil des Inhalts zu parsen, bis der abschließende
schließende Tag oder ein weiterer erwarteter Zwischen-Tag gefunden wird.
Ändern wir DebugNode::create() so, dass es {else} erwartet:
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Inhalt für den Teil {debug}
public AreaNode $thenContent;
// Optionaler Inhalt für den Teil {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield und entweder {/debug} oder {else} erwarten
[$node->thenContent, $nextTag] = yield ['else'];
// Prüfen, ob der Tag, bei dem wir angehalten haben, {else} war
if ($nextTag?->name === 'else') {
// Erneut yield, um den Inhalt zwischen {else} und {/debug} zu parsen
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() und getIterator() aktualisieren wir als Nächstes ...
}
yield ['else'] sagt Latte nun, dass es das Parsen nicht nur bei {/debug}, sondern auch bei
{else} anhalten soll. Trifft es auf {else}, enthält $nextTag das Tag-Objekt
für {else}. Wir rufen dann erneut yield ohne Argumente auf, erwarten also nur noch den abschließenden
Tag {/debug}, und speichern das Ergebnis in $node->elseContent. Wurde {else} nicht
gefunden, wäre $nextTag der Tag für {/debug} (oder null, wenn er als
n:Attribut verwendet wird), und $node->elseContent bliebe null.
print() mit {else} implementieren
Die Methode print() muss die neue Struktur abbilden. Sie soll anhand des Providers appDevMode ein
PHP-if/else-Statement erzeugen.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Code für den 'then'-Zweig (Inhalt von {debug})
} else {
%node // Code für den 'else'-Zweig (Inhalt von {else})
}
XX,
$this->position, // Zeilennummer für die 'if'-Bedingung
$this->thenContent, // Erster %node-Platzhalter
$this->elseContent ?? new NopNode, // Zweiter %node-Platzhalter
);
}
Das ist eine übliche PHP-if/else-Struktur. Wir verwenden %node zweimal; format() setzt
die übergebenen Knoten der Reihe nach ein. Mit ?? new NopNode vermeiden wir Fehler, falls
$this->elseContent gleich null ist – der NopNode gibt schlicht nichts aus.
getIterator() für beide Inhalte implementieren
Wir haben nun potenziell zwei Kindknoten mit Inhalt ($thenContent und $elseContent). Wir müssen
beide liefern, sofern sie existieren:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Verwendung des erweiterten Tags
Der Tag lässt sich nun mit einer optionalen {else}-Klausel verwenden:
{debug}
<p>Debug-Informationen werden angezeigt, weil devMode EIN ist.</p>
{else}
<p>Debug-Informationen sind verborgen, weil devMode AUS ist.</p>
{/debug}
Umgang mit Zustand und Verschachtelung
Unsere bisherigen Beispiele ({datetime}, {debug}) waren in ihren Methoden print()
weitgehend zustandslos. Sie haben entweder direkt Inhalt ausgegeben oder anhand eines globalen Providers eine einfache Bedingung
geprüft. Viele Tags müssen jedoch beim Rendern eine Form von Zustand verwalten oder Ausdrücke auswerten, die der
Benutzer angibt und die aus Gründen der Leistung oder der Korrektheit nur einmal ausgeführt werden dürfen. Außerdem müssen
wir bedenken, was passiert, wenn unsere eigenen Tags verschachtelt werden.
Veranschaulichen wir diese Konzepte, indem wir einen Tag {repeat $count}...{/repeat} erstellen. Dieser Tag
wiederholt seinen inneren Inhalt $count-mal.
Ziel: {repeat $count} implementieren, der seinen Inhalt eine bestimmte Anzahl von Malen wiederholt.
Warum temporäre und eindeutige Variablen nötig sind
Stellen Sie sich vor, der Benutzer schreibt:
{repeat rand(1, 5)} Inhalt {/repeat}
Wenn wir in unserer Methode print() naiv eine PHP-for-Schleife wie diese erzeugen würden:
// Vereinfachter, FALSCHER erzeugter Code
for ($i = 0; $i < rand(1, 5); $i++) {
// Inhalt ausgeben
}
wäre das falsch! Der Ausdruck rand(1, 5) würde bei jedem Schleifendurchlauf erneut ausgewertet, was zu
einer unvorhersehbaren Anzahl von Wiederholungen führt. Wir müssen den Ausdruck $count einmal vor dem Start
der Schleife auswerten und sein Ergebnis speichern.
Wir erzeugen also PHP-Code, der zuerst den Zähl-Ausdruck auswertet und ihn in einer temporären Laufzeitvariablen
speichert. Um Kollisionen mit Variablen zu vermeiden, die der Benutzer des Templates definiert, und mit internen Variablen
von Latte (wie $ʟ_...), verwenden wir für unsere temporären Variablen die Konvention mit dem Präfix
$__ (doppelter Unterstrich).
Der erzeugte Code sähe dann so aus:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// Inhalt ausgeben
}
Betrachten wir nun die Verschachtelung:
{repeat $countA} {* Äußere Schleife *}
{repeat $countB} {* Innere Schleife *}
...
{/repeat}
{/repeat}
Wenn der äußere und der innere Tag {repeat} Code mit denselben Namen für die temporären Variablen
erzeugen würden (etwa $__count und $__i), würde die innere Schleife die Variablen der äußeren
überschreiben und die Logik zerstören.
Wir müssen sicherstellen, dass die für jede Instanz des Tags {repeat} erzeugten temporären Variablen
eindeutig sind. Das erreichen wir mit PrintContext::generateId(). Diese Methode gibt während der Kompilierung
eine eindeutige ganze Zahl zurück. Diese ID können wir an die Namen unserer temporären Variablen anhängen.
Statt $__count erzeugen wir also einen Namen mit eindeutigem numerischem Suffix, etwa $__count_0, und
ebenso für den Schleifenzähler, etwa $__i_0. Die konkreten Zahlen stammen aus einem kompilierungsweiten Zähler,
den alle Knoten gemeinsam nutzen. Sie sind daher nur garantiert eindeutig, bilden aber keine Folge pro Tag.
RepeatNode implementieren
Erstellen wir die Node-Klasse.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class RepeatNode extends StatementNode
{
public ExpressionNode $count;
public AreaNode $content;
/**
* Parsing-Funktion für {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // stellt sicher, dass $count angegeben ist
$node = $tag->node = new self;
// Den Zähl-Ausdruck parsen
$node->count = $tag->parser->parseExpression();
// Den inneren Inhalt holen
[$node->content] = yield;
return $node;
}
/**
* Erzeugt die PHP-'for'-Schleife mit eindeutigen Variablennamen.
*/
public function print(PrintContext $context): string
{
// Eindeutige Variablennamen erzeugen
$id = $context->generateId();
$countVar = '$__count_' . $id; // eindeutiger Name, z. B. $__count_0
$iteratorVar = '$__i_' . $id; // eindeutiger Name, z. B. $__i_0
return $context->format(
<<<'XX'
// Den Zähl-Ausdruck *einmal* auswerten und speichern
%raw = (int) (%node);
// Mit dem gespeicherten Zähler und der eindeutigen Iteratorvariablen schleifen
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Den inneren Inhalt rendern
}
XX,
$countVar, // %0 - Variable zum Speichern des Zählers
$this->count, // %1 - Der Knoten mit dem Ausdruck für den Zähler
$iteratorVar, // %2 - Name der Iteratorvariablen der Schleife
$this->position, // %3 - Kommentar mit der Zeilennummer der Schleife selbst
$this->content // %4 - Der Knoten mit dem inneren Inhalt
);
}
/**
* Liefert die Kindknoten (den Zähl-Ausdruck und den Inhalt).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
Die Methode create() parst den erforderlichen Ausdruck $count mit parseExpression().
Zuerst wird $tag->expectArguments() aufgerufen. Das stellt sicher, dass der Benutzer hinter {repeat}
irgendetwas angegeben hat. Zwar würde auch $tag->parser->parseExpression() fehlschlagen, wenn nichts
angegeben wäre, aber die Fehlermeldung würde von unerwarteter Syntax handeln. expectArguments() liefert eine viel
klarere Meldung, die ausdrücklich sagt, dass dem Tag {repeat} die Argumente fehlen.
Die Methode print() erzeugt den PHP-Code, der zur Laufzeit für die Wiederholungslogik zuständig ist. Zunächst
erzeugt sie eindeutige Namen für die temporären PHP-Variablen, die sie braucht.
Die Methode $context->format() wird mit dem neuen Platzhalter %raw aufgerufen, der den rohen
String einfügt, der als entsprechendes Argument übergeben wurde. Hier fügt er den eindeutigen Variablennamen aus
$countVar ein (z. B. $__count_1). Und was ist mit %0.raw und %2.raw? Das zeigt
positionsbezogene Platzhalter. Statt einfach %raw, das sich das nächste verfügbare Roh-Argument
nimmt, greift %2.raw ausdrücklich auf das Argument mit dem Index 2 zu (also auf $iteratorVar) und
fügt dessen rohen String-Wert ein. So können wir den String $iteratorVar wiederverwenden, ohne ihn der
Argumentliste von format() mehrfach zu übergeben.
Dieser sorgfältig konstruierte Aufruf von format() erzeugt eine effiziente und sichere PHP-Schleife, die den
Zähl-Ausdruck korrekt behandelt und Kollisionen von Variablennamen auch dann vermeidet, wenn {repeat}-Tags
verschachtelt werden.
Registrierung und Verwendung
Registrieren Sie den Tag in Ihrer Extension:
use App\Templating\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Den Tag repeat registrieren
];
}
}
Verwenden Sie ihn im Template, auch verschachtelt:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Innere Schleife</td>
{/repeat}
</tr>
{/repeat}
Dieses Beispiel zeigt, wie sich Zustand (Schleifenzähler) und mögliche Probleme durch Verschachtelung mit temporären
Variablen behandeln lassen, die das Präfix $__ tragen und durch IDs aus PrintContext::generateId()
eindeutig gemacht werden.
Reine n:Attribute
Viele n:Attribute wie n:if oder n:foreach dienen als bequeme Kurzschreibweise für ihre
Gegenstücke als Paar-Tag ({if}...{/if}, {foreach}...{/foreach}). Latte erlaubt es aber auch, Tags zu
definieren, die nur in der Form eines n:Attributs existieren. Sie werden oft dazu verwendet, die Attribute oder das
Verhalten des HTML-Elements zu verändern, an dem sie stehen.
Standardbeispiele, die in Latte eingebaut sind, sind n:class, das beim dynamischen Aufbau des Attributs
class hilft, und n:attr, das mehrere beliebige
Attribute setzen kann.
Erstellen wir unser eigenes reines n:Attribut n:confirm, das vor einer Aktion (etwa dem Folgen eines Links oder
dem Absenden eines Formulars) einen JavaScript-Bestätigungsdialog anzeigt.
Ziel: n:confirm="'Sind Sie sicher?'" implementieren, das einen onclick-Handler ergänzt, der
die Standardaktion verhindert, wenn der Benutzer den Bestätigungsdialog abbricht.
ConfirmNode implementieren
Wir brauchen eine Node-Klasse und eine Parsing-Funktion.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
class ConfirmNode extends StatementNode
{
public ExpressionNode $message;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->message = $tag->parser->parseExpression();
return $node;
}
/**
* Erzeugt den Code des Attributs 'onclick' mit korrektem Escaping.
*/
public function print(PrintContext $context): string
{
// Er sorgt für das korrekte Escaping sowohl im JavaScript- als auch im HTML-Attribut-Kontext.
return $context->format(
<<<'XX'
echo ' onclick="', LR\HtmlHelpers::escapeAttr('return confirm(' . LR\Helpers::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
Die Methode print() erzeugt den PHP-Code, der beim Rendern des Templates letztlich das HTML-Attribut
onclick="..." ausgibt. Der Umgang mit verschachtelten Kontexten (JavaScript innerhalb eines HTML-Attributs) erfordert
sorgfältiges Escaping. Der Helfer LR\Helpers::escapeJs(%node) wird zur Laufzeit aufgerufen und escapt die Nachricht
korrekt für die Verwendung innerhalb von JavaScript (die Ausgabe wäre etwa "Sicher?"). Anschließend escapt der
Helfer LR\HtmlHelpers::escapeAttr(...) die Zeichen, die innerhalb von HTML-Attributen besondere Bedeutung haben,
sodass daraus return confirm("Sicher?") wird. Dieses zweistufige Escaping zur Laufzeit stellt
sicher, dass die Nachricht für JavaScript sicher ist und der entstehende JavaScript-Code sicher im HTML-Attribut
onclick eingebettet werden kann.
Registrierung und Verwendung
Registrieren Sie das n:Attribut in Ihrer Extension. Denken Sie an das Präfix n: im Schlüssel:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // n:confirm registrieren
];
}
}
Nun können Sie n:confirm an Links, Buttons oder Formularelementen verwenden:
<a href="delete.php?id=123" n:confirm='"Möchten Sie den Eintrag {$id} wirklich löschen?"'>Löschen</a>
Erzeugtes HTML:
<a href="delete.php?id=123" onclick="return confirm("Möchten Sie den Eintrag 123 wirklich löschen?")">Löschen</a>
Wenn der Benutzer auf den Link klickt, führt der Browser den onclick-Code aus, zeigt den Bestätigungsdialog an
und geht nur dann zu delete.php, wenn der Benutzer auf “OK” klickt.
Dieses Beispiel zeigt, wie sich ein reines n:Attribut erstellen lässt, das das Verhalten oder die Attribute seines
HTML-Elements verändert, indem es in seiner Methode print() passenden PHP-Code erzeugt. Denken Sie an das doppelte
Escaping, das oft nötig ist: einmal für den Zielkontext (hier JavaScript) und noch einmal für den Kontext des
HTML-Attributs.
Zwei weitere Mitglieder des Objekts Tag sind beim Schreiben reiner n:Attribute nützlich:
$tag->htmlElement gibt Ihnen Zugriff auf das umgebende HTML-Element (einen ElementNode), sodass Sie
es untersuchen oder anpassen können, und $tag->replaceNAttribute($node) erlaubt es Ihnen, das Attribut durch
einen selbst gebauten Knoten zu ersetzen. Tatsächlich ersetzt der Knoten, den create() eines reinen n:Attributs
zurückgibt, das Attribut an seinem Element automatisch.
Fortgeschrittene Themen
Die vorigen Abschnitte decken die grundlegenden Konzepte ab. Hier folgen noch einige fortgeschrittene Themen, denen Sie beim Erstellen eigener Latte-Tags begegnen können.
Ausgabemodi von Tags
Das Objekt Tag, das Ihrer Funktion create() übergeben wird, hat die Property
outputMode. Diese Property beeinflusst, wie Latte mit dem umgebenden Leerraum und der Einrückung umgeht, besonders
wenn der Tag allein auf einer Zeile steht. Sie können diese Property in Ihrer Funktion create() ändern.
Tag::OutputNone(der Standard für jeden Tag und das, was Kontrollstrukturen wie{if}oder{foreach}beibehalten): Der Leerraum um den Tag wird genauso behandelt wie beiOutputRemoveIndentation– die führende Einrückung und ein einzelner nachfolgender Zeilenumbruch werden entfernt. Der eigentliche Unterschied ist intern: Dieser Modus hält den Template-Parser im “Kopf”-Modus des Templates. Er eignet sich für Deklarations- und Einrichtungs-Tags wie{var}oder{default}, die keine direkte Ausgabe erzeugen.Tag::OutputRemoveIndentation(ausdrücklich gesetzt von den Block-Tags{block},{embed},{include}und{sandbox}): Entfernt die führende Einrückung vor dem Tag und einen einzelnen nachfolgenden Zeilenumbruch. Das hält den erzeugten PHP-Code sauberer und vermeidet zusätzliche Leerzeilen in der HTML-Ausgabe, die durch den Tag selbst entstünden.Tag::OutputKeepIndentation(ausdrücklich gesetzt von Ausgabe-Tags wie{=...}): Latte versucht, die Einrückung vor dem Tag zu erhalten; Zeilenumbrüche nach dem Tag bleiben in der Regel erhalten. Das eignet sich für Tags, die Inhalt inline ausgeben – siehe das Beispiel{datetime}weiter oben, das genau aus diesem Grund diesen Modus setzt.
Wählen Sie den Modus, der zum Zweck Ihres Tags am besten passt. Weil der Standard OutputNone ist, brauchen Tags
für Ablaufsteuerung und Deklarationen keine Änderung; setzen Sie OutputKeepIndentation für Tags, die auf einer
eigenen Zeile Inhalt ausgeben.
Zugriff auf übergeordnete und nächstgelegene Tags
Manchmal muss sich das Verhalten eines Tags nach dem Kontext richten, in dem er verwendet wird, genauer gesagt danach,
innerhalb welcher übergeordneten Tags er steht. Genau dafür bietet das Objekt Tag, das Ihrer Funktion
create() übergeben wird, die Methode closestTag(array $classes, ?callable $condition = null): ?Tag.
Diese Methode sucht aufwärts durch die Hierarchie der aktuell geöffneten Latte-Tags (die Kette $tag->parent;
umgebende HTML-Elemente gehören nicht dazu) und gibt das Tag-Objekt des nächstgelegenen Vorfahren zurück, der
bestimmte Kriterien erfüllt. Wird kein passender Vorfahr gefunden, gibt sie null zurück.
Das Array $classes legt fest, nach welcher Art von Vorfahren-Tags Sie suchen. Geprüft wird, ob die Klasse des zum
Vorfahren-Tag gehörenden Knotens ($ancestorTag->node) genau einer der aufgeführten Klassen entspricht;
Unterklassen zählen nicht.
function create(Tag $tag)
{
// Den nächstgelegenen Vorfahren-Tag suchen, dessen Knoten eine Instanz von ForeachNode ist
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Wir können auf die Instanz von ForeachNode selbst zugreifen:
$foreachNode = $foreachTag->node;
}
}
Beachten Sie $foreachTag->node: Das funktioniert nur deshalb, weil es bei der Entwicklung von Latte-Tags
Konvention ist, den erzeugten Knoten in der Methode create() sofort $tag->node zuzuweisen, so wie wir
es immer getan haben.
Manchmal reicht es nicht, nur den Typ des Knotens abzugleichen. Vielleicht müssen Sie eine bestimmte Property des möglichen
Vorfahren-Tags oder seines Knotens prüfen. Das optionale zweite Argument von closestTag() ist ein Callable, das das
mögliche Vorfahren-Tag-Objekt erhält und zurückgeben soll, ob es passt.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Bedingung: Der Block muss dynamisch sein
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
Mit closestTag() können Sie Tags erstellen, die ihren Kontext kennen und die korrekte Verwendung innerhalb Ihrer
Template-Struktur erzwingen, was zu robusteren und verständlicheren Templates führt.
Platzhalter von PrintContext::format()
Wir haben PrintContext::format() häufig verwendet, um in den Methoden print() unserer Knoten
PHP-Code zu erzeugen. Die Methode nimmt einen Masken-String und die folgenden Argumente entgegen, die die Platzhalter in der Maske
ersetzen. Hier eine Übersicht der verfügbaren Platzhalter:
%node: Das Argument muss eineNode-Instanz sein. Es ruft die Methodeprint()des Knotens auf und fügt den resultierenden String mit PHP-Code ein.%dump: Das Argument ist ein beliebiger PHP-Wert. Er wird in gültigen PHP-Code exportiert. Geeignet für Skalare, Arrays und null.$context->format('echo %dump;', 'Hello')→echo 'Hello';$context->format('$arr = %dump;', [1, 2])→$arr = [1, 2];
%raw: Fügt das Argument direkt in den erzeugten PHP-Code ein, ohne Escaping oder sonstige Änderung. Mit Vorsicht verwenden, vor allem zum Einfügen bereits erzeugter PHP-Code-Schnipsel oder von Variablennamen.$context->format('%raw = 1;', '$variableName')→$variableName = 1;
%args: Das Argument muss einExpression\ArrayNodesein. Es gibt die Elemente des Arrays als Argumente eines Funktions- oder Methodenaufrufs aus (durch Kommas getrennt, benannte Argumente werden berücksichtigt).$argsNode = new ArrayNode([...]);$context->format('myFunc(%args);', $argsNode)→myFunc(1, name: 'Joe');
%line: Das Argument muss einPosition- (oderRange-) Objekt sein (üblicherweise$this->position). Es fügt einen PHP-Kommentar/* pos X:Y */ein, der Zeile und Spalte in der Quelle angibt.$context->format('echo "Hi" %line;', $this->position)→echo "Hi" /* pos 42:1 */;
%escape(...): Erzeugt PHP-Code, der den inneren Ausdruck zur Laufzeit nach den aktuellen Regeln des kontextsensitiven Escapings escapt.$context->format('echo %escape(%node);', $variableNode)
%modify(...): Das Argument muss einModifierNodesein. Es erzeugt PHP-Code, der die imModifierNodeangegebenen Filter auf den inneren Inhalt anwendet, einschließlich des kontextsensitiven Escapings, sofern es nicht mit|noescapeabgeschaltet wurde.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...): Ähnlich wie%modify, aber für das Verändern von Blöcken erfassten Inhalts gedacht (häufig HTML).
Sie können Argumente ausdrücklich über ihren nullbasierten Index ansprechen: %0.node, %1.dump,
%2.raw usw. So lässt sich ein Argument in der Maske mehrfach verwenden, ohne es format() mehrfach zu
übergeben. Siehe das Beispiel des Tags {repeat}, in dem %0.raw und %2.raw verwendet
wurden.
Beispiel für komplexes Parsen von Argumenten
parseExpression(), parseArguments() und ähnliche Methoden decken viele Fälle ab, manchmal brauchen
Sie aber eine feinere Parsing-Logik mit dem tiefer liegenden TokenStream, der über
$tag->parser->stream zugänglich ist.
Ziel: Einen Tag {embedYoutube $videoID, width: 640, height: 480} erstellen. Wir wollen eine erforderliche
Video-ID (String oder Variable) parsen, gefolgt von optionalen Schlüssel-Wert-Paaren für die Maße.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Tag;
use Latte\Compiler\Token;
class YoutubeNode extends StatementNode
{
public ExpressionNode $videoId;
public ?ExpressionNode $width = null;
public ?ExpressionNode $height = null;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
// Die erforderliche Video-ID parsen
$node->videoId = $tag->parser->parseExpression();
// Optionale Schlüssel-Wert-Paare parsen
$stream = $tag->parser->stream; // Den Token-Stream holen
while ($stream->tryConsume(',')) { // Verlangt Trennung durch Komma
// Bezeichner 'width' oder 'height' erwarten
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Doppelpunkt als Trennzeichen erwarten
$value = $tag->parser->parseExpression(); // Den Ausdruck des Werts parsen
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Unbekanntes Argument '$key'. Erwartet wurde 'width' oder 'height'.", $keyToken->position);
}
}
return $node;
}
// ... print() und getIterator() ...
}
Dieser Grad an Kontrolle erlaubt es Ihnen, durch die direkte Arbeit mit dem Token-Stream sehr spezifische und komplexe Syntaxen für Ihre eigenen Tags zu definieren.
AuxiliaryNode verwenden
Latte stellt für besondere Situationen bei der Code-Erzeugung oder innerhalb von Compiler-Pässen allgemeine “Hilfsknoten”
bereit. Das sind AuxiliaryNode und Php\Expression\AuxiliaryNode.
Stellen Sie sich AuxiliaryNode als flexiblen Container-Knoten vor, der seine Kernaufgaben – die Code-Erzeugung
und das Bereitstellen der Kindknoten – an die Argumente delegiert, die seinem Konstruktor übergeben werden:
- Delegation von
print(): Das erste Konstruktorargument ist eine PHP-Closure. Wenn Latte die Methodeprint()einesAuxiliaryNodeaufruft, führt es diese Closure aus. Die Closure erhält denPrintContextsowie alle Knoten, die im zweiten Konstruktorargument übergeben wurden, sodass Sie die Erzeugung des PHP-Codes völlig frei festlegen können. - Delegation von
getIterator(): Das zweite Konstruktorargument ist ein Array vonNode-Objekten. Wenn Latte die Kinder einesAuxiliaryNodedurchlaufen muss (etwa während der Compiler-Pässe), liefert seine MethodegetIterator()einfach die Knoten aus diesem Array.
Beispiel:
$node = new AuxiliaryNode(
// 1. Diese Closure wird zum Rumpf von print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Diese Knoten liefert getIterator() und übergibt sie an die obige Closure
[$argumentNode1, $argumentNode2]
);
Latte bietet zwei verschiedene Typen, je nachdem, wo Sie den erzeugten Code einfügen müssen:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: Verwenden Sie ihn, wenn Sie ein Stück PHP-Code erzeugen müssen, das einen Ausdruck darstelltLatte\Compiler\Nodes\AuxiliaryNode: Verwenden Sie ihn für allgemeinere Zwecke, wenn Sie einen Block PHP-Code einfügen müssen, der ein oder mehrere Statements darstellt
Der wichtige Grund, in Ihrer Methode print() oder in einem Compiler-Pass AuxiliaryNode statt
üblicher Knoten (wie StaticMethodCallNode) zu verwenden, ist die Steuerung der Sichtbarkeit für nachfolgende
Compiler-Pässe, insbesondere für sicherheitsrelevante wie die Sandbox.
Stellen Sie sich folgendes Szenario vor: Ihr Compiler-Pass muss einen vom Benutzer angegebenen Ausdruck
($userExpr) in einen Aufruf einer bestimmten, vertrauenswürdigen Hilfsfunktion
myInternalSanitize($userExpr) einpacken. Wenn Sie einen üblichen Knoten
new FunctionCallNode('myInternalSanitize', [$userExpr]) erzeugen, ist er für den AST-Traverser vollständig
sichtbar. Läuft später ein Sandbox-Pass und steht myInternalSanitize nicht auf dessen Allowlist, könnte die
Sandbox diesen Aufruf blockieren oder verändern und damit die interne Logik Ihres Tags zerstören, obwohl Sie als
Autor des Tags wissen, dass genau dieser Aufruf sicher und notwendig ist. Sie können den Aufruf daher direkt in der Closure des
AuxiliaryNode erzeugen.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... innerhalb von print() oder einem Compiler-Pass ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Direkte Erzeugung von PHP-Code
$userExpr,
),
// WICHTIG: Den ursprünglichen Knoten des Benutzerausdrucks hier trotzdem übergeben!
[$userExpr],
);
In diesem Fall sieht der Sandbox-Pass den AuxiliaryNode, analysiert aber den von seiner Closure erzeugten
PHP-Code nicht. Er kann den Aufruf von myInternalSanitize, der innerhalb der Closure erzeugt wird, nicht
direkt blockieren.
Der erzeugte PHP-Code selbst bleibt zwar vor den Pässen verborgen, die Eingaben dieses Codes (Knoten, die Benutzerdaten
oder Ausdrücke darstellen) müssen jedoch weiterhin durchlaufbar sein. Deshalb ist das zweite Argument des Konstruktors
von AuxiliaryNode so wichtig. Sie müssen ein Array mit allen ursprünglichen Knoten übergeben, die Ihre
Closure verwendet (wie $userExpr im obigen Beispiel). Die Methode getIterator() von
AuxiliaryNode liefert diese Knoten, sodass Compiler-Pässe wie die Sandbox sie auf mögliche Probleme
untersuchen können.
Best Practices
- Klarer Zweck: Sorgen Sie dafür, dass Ihr Tag einen klaren und notwendigen Zweck hat. Erstellen Sie keine Tags für Aufgaben, die sich leicht mit Filtern oder Funktionen lösen lassen.
getIterator()korrekt implementieren: Implementieren SiegetIterator()immer und liefern Sie Referenzen (&) auf alle Kindknoten (Argumente, Inhalt), die aus dem Template geparst wurden. Das ist für Compiler-Pässe, für die Sicherheit (Sandbox) und für mögliche künftige Optimierungen unerlässlich.- Öffentliche Properties für Knoten: Machen Sie die Properties, die Kindknoten halten, öffentlich, damit Compiler-Pässe sie bei Bedarf verändern können.
PrintContext::format()verwenden: Nutzen Sie die Methodeformat()zum Erzeugen von PHP-Code. Sie kümmert sich um Anführungszeichen, setzt die Platzhalter korrekt ein und ergänzt automatisch Kommentare mit Zeilennummern.- Temporäre Variablen (
$__): Wenn Sie Laufzeit-PHP-Code erzeugen, der temporäre Variablen braucht (etwa für Zwischenergebnisse oder Schleifenzähler), verwenden Sie die Konvention mit dem Präfix$__, um Kollisionen mit Benutzervariablen und mit den internen$ʟ_-Variablen von Latte zu vermeiden. - Verschachtelung und eindeutige IDs: Wenn Ihr Tag verschachtelt werden kann oder zur Laufzeit einen instanzbezogenen
Zustand braucht, verwenden Sie in Ihrer Methode
print()$context->generateId(), um eindeutige Suffixe für Ihre temporären$__-Variablen zu erzeugen. - Provider für externe Daten: Verwenden Sie Provider (registriert über
Extension::getProviders()), um auf Laufzeitdaten oder Dienste zuzugreifen ($this->global->…), statt Werte fest zu verdrahten oder sich auf globalen Zustand zu verlassen. Verwenden Sie für die Namen der Provider Hersteller-Präfixe. - n:Attribute bedenken: Wenn Ihr Paar-Tag logisch auf einem einzelnen HTML-Element arbeitet, bietet Latte
höchstwahrscheinlich automatische Unterstützung als
n:Attribut. Denken Sie zum Komfort der Benutzer daran. Wenn Sie einen Tag erstellen, der Attribute verändert, überlegen Sie, ob ein reinesn:Attributdie passendste Form ist. - Testen: Schreiben Sie Tests für Ihre Tags, die sowohl das Parsen verschiedener Syntax-Eingaben als auch die Korrektheit der Ausgabe des erzeugten PHP-Codes abdecken.
Wenn Sie diese Richtlinien befolgen, können Sie mächtige, robuste und wartbare eigene Tags erstellen, die sich nahtlos in die Templating-Engine Latte einfügen.
Das Studium der Node-Klassen, die Teil von Latte sind, ist der beste Weg, um alle Feinheiten des Parsing-Prozesses kennenzulernen.