Compiler-Pässe erstellen
Compiler-Pässe bieten einen mächtigen Mechanismus, um Latte-Templates zu analysieren und zu verändern, nachdem sie in einen abstrakten Syntaxbaum (AST) geparst wurden und bevor der endgültige PHP-Code erzeugt wird. Das ermöglicht fortgeschrittene Manipulation von Templates, Optimierungen, Sicherheitsprüfungen (wie die Sandbox) und das Sammeln von Erkenntnissen über Templates. Diese Anleitung führt Sie durch das Erstellen eigener Compiler-Pässe.
Was ist ein Compiler-Pass?
Um die Rolle der Compiler-Pässe zu verstehen, sehen Sie sich den Kompilierungsvorgang von Latte an. Wie Sie sehen, arbeiten Compiler-Pässe an einer entscheidenden Stelle und erlauben einen tiefen Eingriff zwischen dem ersten Parsen und der endgültigen Ausgabe des Codes.
Im Kern ist ein Compiler-Pass schlicht ein PHP-Callable (etwa eine Funktion, eine statische Methode oder eine Instanzmethode),
das ein Argument entgegennimmt: den Wurzelknoten des AST des Templates, immer eine Instanz von
Latte\Compiler\Nodes\TemplateNode.
Das Hauptziel eines Compiler-Passes ist üblicherweise eines oder beide der folgenden:
- Analyse: den AST durchlaufen und Informationen über das Template sammeln (z. B. alle definierten Blöcke finden, die Verwendung bestimmter Tags prüfen, sicherstellen, dass bestimmte Sicherheitsbedingungen erfüllt sind).
- Veränderung: die Struktur des AST oder die Eigenschaften von Knoten ändern (z. B. automatisch HTML-Attribute ergänzen, bestimmte Tag-Kombinationen optimieren, veraltete Tags durch neue ersetzen, Sandbox-Regeln umsetzen).
Registrierung
Compiler-Pässe werden über die Methode getPasses() einer Extension registriert. Diese Methode gibt ein assoziatives
Array zurück, in dem die Schlüssel eindeutige Namen der Pässe sind (intern und für die Reihenfolge verwendet) und die Werte
die PHP-Callables mit der Logik des Passes.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... weitere Pässe ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// Implementierung...
}
}
Die von den Kern-Extensions von Latte und Ihren eigenen Extensions registrierten Pässe laufen der Reihe nach. Die Reihenfolge
kann wichtig sein, vor allem wenn ein Pass auf den Ergebnissen oder Änderungen eines anderen aufbaut. Latte bietet bei Bedarf
einen Hilfsmechanismus, um diese Reihenfolge zu steuern; Einzelheiten finden Sie in der Dokumentation zu Extension::getPasses().
Beispiel eines AST
Um eine bessere Vorstellung vom AST zu bekommen, fügen wir ein Beispiel an. Das ist das Quell-Template:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Und das ist seine Darstellung in Form des AST:
Latte\Compiler\Nodes\TemplateNode(
Latte\Compiler\Nodes\FragmentNode(
- Latte\Essential\Nodes\ForeachNode(
expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode(
object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category')
name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems')
)
value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
content: Latte\Compiler\Nodes\FragmentNode(
- Latte\Compiler\Nodes\TextNode(' ')
- Latte\Compiler\Nodes\Html\ElementNode('li')(
content: Latte\Compiler\Nodes\PrintNode(
expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode(
object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
name: Latte\Compiler\Nodes\Php\IdentifierNode('name')
)
modifier: Latte\Compiler\Nodes\Php\ModifierNode(
filters:
- Latte\Compiler\Nodes\Php\FilterNode('upper')
)
)
)
)
else: Latte\Compiler\Nodes\FragmentNode(
- Latte\Compiler\Nodes\TextNode('no items found')
)
)
)
)
Den AST mit NodeTraverser durchlaufen
Rekursive Funktionen zum Durchlaufen der komplexen AST-Struktur von Hand zu schreiben ist mühsam und fehleranfällig. Latte bietet dafür ein eigenes Werkzeug: Latte\Compiler\NodeTraverser. Diese Klasse setzt das Entwurfsmuster Visitor um und macht das Durchlaufen des AST systematisch und beherrschbar.
Die grundlegende Verwendung besteht darin, eine Instanz von NodeTraverser zu erstellen und ihre Methode
traverse() aufzurufen, der man den Wurzelknoten des AST und ein oder zwei “Visitor”-Callables übergibt:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// 'enter'-Visitor: wird beim Betreten eines Knotens aufgerufen (vor seinen Kindern)
enter: function (Node $node) {
echo "Entering node of type: " . $node::class . "\n";
// Hier können Sie den Knoten untersuchen
if ($node instanceof Nodes\TextNode) {
// echo "Found text: " . $node->content . "\n";
}
},
// 'leave'-Visitor: wird beim Verlassen eines Knotens aufgerufen (nach seinen Kindern)
leave: function (Node $node) {
echo "Leaving node of type: " . $node::class . "\n";
// Hier können Sie handeln, nachdem die Kinder verarbeitet wurden
},
);
Sie können je nach Bedarf nur den enter-Visitor, nur den leave-Visitor oder beide angeben.
enter(Node $node): Diese Funktion wird für jeden Knoten ausgeführt, bevor der Traverser eines
seiner Kinder besucht. Sie ist nützlich für:
- das Sammeln von Informationen beim Abstieg durch den Baum,
- Entscheidungen vor dem Verarbeiten der Kinder (etwa die Entscheidung, sie zu überspringen, siehe Den Durchlauf optimieren),
- gegebenenfalls das Verändern des Knotens, bevor die Kinder besucht werden (seltener).
leave(Node $node): Diese Funktion wird für jeden Knoten ausgeführt, nachdem alle seine Kinder (und
deren gesamte Teilbäume) vollständig besucht wurden (betreten und verlassen). Sie ist der häufigste Ort für:
- das Ersetzen eines Knotens, nachdem seine Kinder verarbeitet wurden,
- das Entfernen von Knoten aus dem AST,
- das Zusammenfassen der aus dem gesamten Teilbaum gesammelten Informationen.
Sowohl der enter- als auch der leave-Visitor können optional einen Wert zurückgeben, der den
Verlauf des Durchlaufs beeinflusst. Die Rückgabe von null (oder von nichts) setzt den Durchlauf normal fort, die
Rückgabe einer Node-Instanz ersetzt den aktuellen Knoten, und die Rückgabe besonderer Konstanten wie
NodeTraverser::RemoveNode oder NodeTraverser::StopTraversal verändert den Ablauf, wie die folgenden
Abschnitte erklären.
Wie der Durchlauf funktioniert
Der NodeTraverser verwendet intern die Methode getIterator(), die jede Node-Klasse
implementieren muss (wie unter Eigene Tags erstellen
besprochen). Er iteriert über die von getIterator() gelieferten Kinder, ruft auf ihnen rekursiv
traverse() auf und stellt sicher, dass die Visitor enter und leave für jeden über
Iteratoren erreichbaren Knoten des Baums in der richtigen Tiefensuche-Reihenfolge aufgerufen werden. Das unterstreicht erneut,
warum ein korrekt implementiertes getIterator() in den Knoten Ihrer eigenen Tags für das Funktionieren der
Compiler-Pässe absolut unerlässlich ist.
Schreiben wir einen einfachen Pass, der zählt, wie oft der Tag {do} (dargestellt durch
Latte\Essential\Nodes\DoNode) im Template verwendet wird.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\DoNode;
function countDoTags(TemplateNode $templateNode): void
{
$count = 0;
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use (&$count): void {
if ($node instanceof DoNode) {
$count++;
}
},
// der 'leave'-Visitor wird für diese Aufgabe nicht gebraucht
);
echo "Found {do} tag $count times.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
In diesem Beispiel brauchten wir nur den enter-Visitor, um den Typ jedes angetroffenen Knotens zu prüfen.
Als Nächstes sehen wir uns an, wie sich diese Visitor nutzen lassen, um den AST tatsächlich zu verändern.
Den AST verändern
Einer der Hauptzwecke von Compiler-Pässen ist das Verändern des abstrakten Syntaxbaums. Das ermöglicht mächtige
Transformationen, Optimierungen oder das Durchsetzen von Regeln direkt an der Struktur des Templates, bevor der PHP-Code erzeugt
wird. Der NodeTraverser bietet dafür innerhalb der Visitor enter und leave
mehrere Wege.
Wichtiger Hinweis: Das Verändern des AST erfordert Sorgfalt. Falsche Änderungen – etwa das Entfernen wesentlicher Knoten oder das Ersetzen eines Knotens durch einen unpassenden Typ – können beim Erzeugen des Codes zu Fehlern führen oder unerwartetes Verhalten zur Laufzeit erzeugen. Testen Sie Ihre verändernden Pässe stets gründlich.
Eigenschaften von Knoten ändern
Der einfachste Weg, den Baum zu verändern, ist das direkte Ändern der öffentlichen Properties der beim Durchlauf angetroffenen Knoten. Alle Knoten speichern ihre geparsten Argumente, Inhalte oder Attribute in öffentlichen Properties.
Beispiel: Erstellen wir einen Pass, der alle statischen Textknoten findet (TextNode, die reines HTML oder
Text außerhalb von Latte-Tags darstellen) und ihren Inhalt direkt im AST in Großbuchstaben umwandelt.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\TextNode;
function uppercaseStaticText(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Wir können 'enter' verwenden, da TextNode keine Kinder hat, die zuerst zu verarbeiten wären
enter: function (Node $node) {
// Ist dieser Knoten ein statischer Textblock?
if ($node instanceof TextNode) {
// Ja! Direkt seine öffentliche Property 'content' verändern.
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// Es muss nichts zurückgegeben werden, die Änderung geschieht an Ort und Stelle.
},
);
}
In diesem Beispiel prüft der enter-Visitor, ob der aktuelle $node ein TextNode ist.
Wenn ja, aktualisieren wir direkt seine öffentliche Property $content mit mb_strtoupper(). Das ändert
den im AST gespeicherten statischen Textinhalt unmittelbar, bevor der PHP-Code erzeugt wird. Weil wir das Objekt direkt
verändern, müssen wir aus dem Visitor nichts zurückgeben.
Wirkung: Enthielt das Template <p>Hello</p>{= $var }<span>World</span>, stellt der AST
nach diesem Pass etwa Folgendes dar: <p>HELLO</p>{= $var }<span>WORLD</span>. Auf den Inhalt
von $var wirkt sich das NICHT aus.
Knoten ersetzen
Eine mächtigere Technik der Veränderung ist, einen Knoten vollständig durch einen anderen zu ersetzen. Das geschieht durch
Rückgabe der neuen Node-Instanz aus dem enter- oder leave-Visitor. Der
NodeTraverser ersetzt den ursprünglichen Knoten dann in der Struktur des Elternknotens durch den
zurückgegebenen.
Beispiel: Erstellen wir einen Pass, der alle Verwendungen der Konstante PHP_VERSION findet (dargestellt
durch ConstantFetchNode) und sie direkt durch ein String-Literal (StringNode) mit der
tatsächlichen, während der Kompilierung ermittelten PHP-Version ersetzt. Das ist eine Form der Optimierung zur
Kompilierzeit.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
function inlinePhpVersion(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// 'leave' wird für Ersetzungen oft verwendet, damit die Kinder (sofern vorhanden)
// zuerst verarbeitet werden, obwohl 'enter' hier ebenfalls funktionieren würde.
leave: function (Node $node) {
// Ist dieser Knoten ein Zugriff auf eine Konstante mit dem Namen 'PHP_VERSION'?
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// Einen neuen StringNode mit der aktuellen PHP-Version erzeugen
$newNode = new StringNode(PHP_VERSION);
// Optional, aber gute Praxis: Positionsangabe kopieren
$newNode->position = $node->position;
// Den neuen StringNode zurückgeben. Der Traverser ersetzt
// den ursprünglichen ConstantFetchNode durch diesen $newNode.
return $newNode;
}
// Geben wir keinen Node zurück, bleibt der ursprüngliche $node erhalten.
},
);
}
Hier findet der leave-Visitor den konkreten ConstantFetchNode für PHP_VERSION. Er
erzeugt dann einen völlig neuen StringNode mit dem Wert der Konstante PHP_VERSION zur
Kompilierzeit. Durch die Rückgabe dieses $newNode weist er den Traverser an, den ursprünglichen
ConstantFetchNode im AST zu ersetzen.
Wirkung: Enthielt das Template {= PHP_VERSION } und läuft die Kompilierung auf PHP 8.2.1, stellt der AST nach
diesem Pass faktisch {= '8.2.1' } dar.
enter oder leave für das Ersetzen wählen:
- Verwenden Sie
leave, wenn das Erzeugen des neuen Knotens von den Ergebnissen der Verarbeitung der Kinder des alten Knotens abhängt oder wenn Sie schlicht sicherstellen wollen, dass die Kinder vor dem Ersetzen besucht werden (übliche Praxis). - Verwenden Sie
enter, wenn Sie einen Knoten ersetzen wollen, bevor seine Kinder überhaupt besucht werden.
Knoten entfernen
Sie können einen Knoten vollständig aus dem AST entfernen, indem Sie aus einem Visitor die besondere Konstante
NodeTraverser::RemoveNode zurückgeben.
Beispiel: Entfernen wir alle HTML-Kommentare (<!-- ... -->) aus der Ausgabe. Latte-Kommentare
{* ... *} lassen sich auf diese Weise nicht ansprechen, weil der Parser ihren Inhalt verwirft und sie durch einen
leeren NopNode statt durch einen eigenen Kommentarknoten ersetzt; HTML-Kommentare bleiben jedoch als Knoten
Html\CommentNode erhalten, sodass wir sie hier entfernen können.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Html\CommentNode;
function removeHtmlComments(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// 'enter' genügt hier, da wir zum Entfernen eines Kommentars keine Informationen über Kinder brauchen
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// Dem Traverser signalisieren, diesen Knoten aus dem AST zu entfernen
return NodeTraverser::RemoveNode;
}
},
);
}
Achtung: Verwenden Sie RemoveNode mit Bedacht. Das Entfernen eines Knotens, der wesentlichen Inhalt
enthält oder die Struktur beeinflusst (etwa das Entfernen des Inhaltsknotens einer Schleife), kann zu kaputten Templates oder
ungültigem erzeugtem Code führen. Am sichersten ist es bei Knoten, die wirklich optional oder in sich abgeschlossen sind (wie
Kommentare oder Debug-Tags), oder bei leeren Strukturknoten (ein leerer FragmentNode lässt sich in manchen
Zusammenhängen von einem Aufräum-Pass gefahrlos entfernen).
Diese drei Methoden – Properties verändern, Knoten ersetzen und Knoten entfernen – bilden das grundlegende Werkzeug zur Manipulation des AST in Ihren Compiler-Pässen.
Den Durchlauf optimieren
Der AST eines Templates kann recht groß werden und womöglich Tausende Knoten enthalten. Jeden einzelnen Knoten zu durchlaufen
kann unnötig sein und die Leistung der Kompilierung beeinträchtigen, wenn sich Ihr Pass nur für bestimmte Teile des Baums
interessiert. Der NodeTraverser bietet Wege, den Durchlauf zu optimieren:
Kinder überspringen
Wenn Sie wissen, dass ab einem bestimmten Knotentyp keiner seiner Nachfahren die gesuchten Knoten enthalten kann, können Sie
dem Traverser sagen, dass er das Besuchen seiner Kinder überspringen soll. Das geschieht durch Rückgabe der Konstante
NodeTraverser::DontTraverseChildren aus dem enter-Visitor. Sie schneiden damit ganze Zweige aus
dem Pfad des Durchlaufs heraus und sparen womöglich erheblich Zeit, besonders bei Templates mit komplexen PHP-Ausdrücken
in Tags.
Den Durchlauf anhalten
Wenn Ihr Pass nur das erste Vorkommen von etwas finden muss (einen bestimmten Knotentyp, eine erfüllte Bedingung),
können Sie den gesamten Durchlauf anhalten, sobald Sie es gefunden haben. Das erreichen Sie durch Rückgabe der Konstante
NodeTraverser::StopTraversal aus dem enter- oder dem leave-Visitor. Die Methode
traverse() besucht dann keine weiteren Knoten mehr. Das ist ausgesprochen wirksam, wenn Sie in einem womöglich sehr
großen Baum nur den ersten Treffer brauchen.
Nützliche Klasse NodeHelpers
Der NodeTraverser bietet feingranulare Kontrolle, Latte stellt darüber hinaus aber auch die praktische
Hilfsklasse Latte\Compiler\NodeHelpers bereit,
die den NodeTraverser für mehrere gängige Such- und Analyseaufgaben kapselt und dabei oft weniger Boilerplate-Code
erfordert.
find (Node $startNode, callable $filter): array
Diese statische Methode findet alle Knoten im Teilbaum ab $startNode (einschließlich), die den Callback
$filter erfüllen. Sie gibt ein Array der passenden Knoten zurück.
Beispiel: Alle Variablenknoten (VariableNode) im gesamten Template finden.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
use Latte\Compiler\Nodes\TemplateNode;
function findAllVariables(TemplateNode $templateNode): array
{
return NodeHelpers::find(
$templateNode,
fn($node) => $node instanceof VariableNode,
);
}
findFirst (Node $startNode, callable $filter): ?Node
Ähnlich wie find, hält den Durchlauf aber sofort an, nachdem der erste Knoten gefunden wurde, der den
Callback $filter erfüllt. Sie gibt das gefundene Objekt Node zurück oder null, wenn kein
passender Knoten gefunden wurde. Das ist im Grunde eine bequeme Kapselung von NodeTraverser::StopTraversal.
Beispiel: Den Knoten {parameters} finden.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;
function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
return NodeHelpers::findFirst(
$templateNode->head, // aus Effizienzgründen nur im head-Abschnitt suchen
fn($node) => $node instanceof ParametersNode,
);
}
clone (Latte\Compiler\Node $node): Node
Diese statische Methode erstellt eine tiefe Kopie eines Knotens und seines gesamten Teilbaums. Sie ist nützlich, wenn Sie einen Zweig des AST duplizieren müssen, zum Beispiel um eine veränderte Kopie eines Knotens einzufügen und das Original unangetastet zu lassen.
use Latte\Compiler\NodeHelpers;
$copy = NodeHelpers::clone($node);
toValue (ExpressionNode $node, bool $constants = false): mixed
Diese statische Methode versucht, einen ExpressionNode zur Kompilierzeit auszuwerten und den entsprechenden
PHP-Wert zurückzugeben. Zuverlässig funktioniert sie nur bei einfachen Literalknoten (StringNode,
IntegerNode, FloatNode, BooleanNode, NullNode) und bei
ArrayNode-Instanzen, die nur solche auswertbaren Elemente enthalten.
Ist $constants auf true gesetzt, versucht sie außerdem, ConstantFetchNode und
ClassConstantFetchNode aufzulösen, indem sie defined() prüft und constant()
verwendet.
Enthält der Knoten Variablen, Funktionsaufrufe oder andere dynamische Elemente, lässt er sich zur Kompilierzeit nicht
auswerten und die Methode wirft eine InvalidArgumentException.
Anwendungsfall: Den statischen Wert eines Tag-Arguments während der Kompilierung ermitteln, um Entscheidungen zur Kompilierzeit zu treffen.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\ExpressionNode;
function getStaticStringArgument(ExpressionNode $argumentNode): ?string
{
try {
$value = NodeHelpers::toValue($argumentNode);
return is_string($value) ? $value : null;
} catch (\InvalidArgumentException $e) {
// Das Argument war kein statischer String-Literal
return null;
}
}
toText (?Node $node): ?string
Diese statische Methode ist nützlich, um aus einfachen Knoten den reinen Textinhalt zu gewinnen. Sie arbeitet vor allem mit:
TextNode: gibt dessen$contentzurück.FragmentNode: verkettet das Ergebnis vontoText()für alle seine Kinder. Lässt sich ein Kind nicht in Text umwandeln (enthält es z. B. einenPrintNode), gibt sienullzurück.NopNode: gibt einen leeren String zurück.- andere Knotentypen: geben
nullzurück.
Anwendungsfall: Den statischen Textinhalt des Werts eines HTML-Attributs oder eines einfachen HTML-Elements zur Analyse in einem Compiler-Pass gewinnen.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// $attr->value ist typischerweise ein AreaNode (etwa FragmentNode oder TextNode)
return NodeHelpers::toText($attr->value);
}
// Beispiel für die Verwendung in einem Pass:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = $node->getAttribute('name');
// if ($nameAttrValue === 'description') { ... }
// }
NodeHelpers kann Ihre Compiler-Pässe vereinfachen, indem es fertige Lösungen für gängige Aufgaben beim
Durchlaufen und Analysieren des AST bietet.
Praktische Beispiele
Wenden wir die Konzepte des Durchlaufens und Veränderns des AST auf einige praktische Probleme an. Diese Beispiele zeigen gängige Muster, die in Compiler-Pässen verwendet werden.
loading="lazy" bei <img> automatisch
ergänzen
Moderne Browser unterstützen natives Lazy Loading von Bildern über das Attribut loading="lazy". Erstellen wir
einen Pass, der dieses Attribut automatisch bei allen Tags <img> ergänzt, die noch kein Attribut
loading haben.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Html;
function addLazyLoading(Nodes\TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Wir können 'enter' verwenden, da wir den Knoten direkt verändern
// und für diese Entscheidung nicht auf die Kinder angewiesen sind.
enter: function (Node $node) {
// Ist es ein HTML-Element namens 'img'?
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// Prüfen, ob das Attribut 'loading' bereits existiert (ohne Beachtung der Groß-/Kleinschreibung)
foreach ($node->attributes->children as $attrNode) {
if ($attrNode instanceof Html\AttributeNode
&& $attrNode->name instanceof Nodes\TextNode // statischer Attributname
&& strtolower($attrNode->name->content) === 'loading'
) {
return; // existiert bereits, nichts tun
}
}
// Ein Leerzeichen voranstellen, wenn die Attribute nicht leer sind
if ($node->attributes->children) {
$node->attributes->children[] = new Nodes\TextNode(' ');
}
// Den neuen Attributknoten erzeugen: loading="lazy"
$node->attributes->children[] = new Html\AttributeNode(
name: new Nodes\TextNode('loading'),
value: new Nodes\TextNode('lazy'),
quote: '"',
);
// Änderung geschieht an Ort und Stelle, keine Rückgabe nötig.
}
},
);
}
Erklärung:
- Der
enter-Visitor sucht nach KnotenHtml\ElementNodemit dem Namenimg. - Er durchläuft die vorhandenen Attribute (
$node->attributes->children), um zu prüfen, ob bereits ein Attributloadingvorhanden ist. - Findet er keines, erzeugt er einen neuen
Html\AttributeNodefürloading="lazy"und fügt ihn hinzu (bei Bedarf mit vorangestelltem Leerzeichen).
Funktionsaufrufe prüfen
Compiler-Pässe sind die Grundlage der Sandbox von Latte. Die echte Sandbox ist ausgefeilt, wir können aber das Grundprinzip der Prüfung auf verbotene Funktionsaufrufe zeigen.
Ziel: Die Verwendung der potenziell gefährlichen Funktion shell_exec in Template-Ausdrücken
verhindern.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Php;
use Latte\SecurityViolationException;
function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void
{
$forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // einfache Liste
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use ($forbiddenFunctions) {
// Ist es ein Knoten für einen direkten Funktionsaufruf?
if ($node instanceof Php\Expression\FunctionCallNode
&& $node->name instanceof Php\NameNode
&& isset($forbiddenFunctions[strtolower((string) $node->name)])
) {
throw new SecurityViolationException(
"Function {$node->name}() is not allowed.",
$node->position,
);
}
},
);
}
Erklärung:
- Wir definieren eine Liste verbotener Funktionsnamen.
- Der
enter-Visitor prüft aufFunctionCallNode. - Ist der Name der Funktion (
$node->name) ein statischerNameNode, prüfen wir seine kleingeschriebene String-Darstellung gegen unsere Verbotsliste. - Wird eine verbotene Funktion gefunden, werfen wir eine
Latte\SecurityViolationException, die klar auf einen Verstoß gegen eine Sicherheitsregel hinweist und die Kompilierung anhält.
Diese Beispiele zeigen, wie sich Compiler-Pässe mithilfe des NodeTraverser für Analysen, automatische
Änderungen und das Durchsetzen von Sicherheitsbedingungen einsetzen lassen, indem sie direkt mit der AST-Struktur des Templates
arbeiten.
Bewährte Praktiken
Behalten Sie beim Schreiben von Compiler-Pässen diese Leitlinien im Blick, um robuste, wartbare und effiziente Erweiterungen zu schaffen:
- Die Reihenfolge zählt: Achten Sie darauf, in welcher Reihenfolge die Pässe laufen. Baut Ihr Pass auf der AST-Struktur
auf, die ein anderer Pass erzeugt hat (etwa ein Kern-Pass von Latte oder ein anderer eigener Pass), oder könnten andere Pässe
von Ihren Änderungen abhängen, nutzen Sie den von
Extension::getPasses()gebotenen Mechanismus, um Abhängigkeiten festzulegen (before/after). Einzelheiten finden Sie in der Dokumentation zuExtension::getPasses(). - Eine Verantwortung: Streben Sie Pässe an, die eine einzige, klar umrissene Aufgabe erfüllen. Erwägen Sie bei komplexen Transformationen, die Logik auf mehrere Pässe aufzuteilen – vielleicht einen für die Analyse und einen weiteren für die Änderung anhand der Analyseergebnisse. Das verbessert Klarheit und Testbarkeit.
- Leistung: Denken Sie daran, dass Compiler-Pässe die Kompilierzeit des Templates verlängern (was üblicherweise nur
einmal geschieht, bis sich das Template ändert). Vermeiden Sie in Ihren Pässen nach Möglichkeit rechenintensive Operationen.
Nutzen Sie Optimierungen des Durchlaufs wie
NodeTraverser::DontTraverseChildrenundNodeTraverser::StopTraversal, wann immer Sie wissen, dass Sie bestimmte Teile des AST nicht besuchen müssen. NodeHelpersverwenden: Prüfen Sie bei gängigen Aufgaben wie dem Finden bestimmter Knoten oder dem statischen Auswerten einfacher Ausdrücke, obLatte\Compiler\NodeHelperseine passende Methode bietet, bevor Sie eigene Logik mit demNodeTraverserschreiben. Das spart Zeit und reduziert Boilerplate.- Fehlerbehandlung: Erkennt Ihr Pass einen Fehler oder einen ungültigen Zustand im AST des Templates, werfen Sie eine
Latte\CompileException(oder bei Sicherheitsproblemen eineLatte\SecurityViolationException) mit einer klaren Meldung und dem passenden ObjektPosition(üblicherweise$node->position). Das gibt dem Entwickler des Templates hilfreiche Rückmeldung. - Idempotenz (wenn möglich): Idealerweise sollte das mehrfache Ausführen Ihres Passes auf demselben AST dasselbe Ergebnis liefern wie das einmalige. Das ist nicht immer machbar, vereinfacht aber das Debuggen und das Nachdenken über das Zusammenspiel der Pässe. Sorgen Sie zum Beispiel dafür, dass Ihr verändernder Pass prüft, ob die Änderung bereits angewendet wurde, bevor er sie erneut anwendet.
Wenn Sie sich an diese Praktiken halten, können Sie Compiler-Pässe wirkungsvoll nutzen, um die Fähigkeiten von Latte auf mächtige und verlässliche Weise zu erweitern, und tragen so zu sichererer, stärker optimierter oder funktionsreicherer Verarbeitung von Templates bei.