Kompilierungsdurchläufe
Kompilierungsdurchläufe bieten einen leistungsstarken Mechanismus zur Analyse und Modifikation von Latte-Templates nach deren Parsen in einen abstrakten Syntaxbaum (AST) und vor der Generierung des finalen PHP-Codes. Dies ermöglicht fortgeschrittene Template-Manipulation, Optimierungen, Sicherheitsprüfungen (wie die Sandbox) und das Sammeln von Informationen über Templates. Diese Anleitung führt Sie durch die Erstellung Ihrer eigenen Kompilierungsdurchläufe.
Was ist ein Kompilierungsdurchlauf?
Um die Rolle der Kompilierungsdurchläufe zu verstehen, sehen Sie sich den Latte-Kompilierungsprozess an. Wie Sie sehen können, operieren Kompilierungsdurchläufe in einer entscheidenden Phase und ermöglichen einen tiefen Eingriff zwischen dem anfänglichen Parsen und der finalen Code-Ausgabe.
Im Kern ist ein Kompilierungsdurchlauf einfach ein PHP Callable (wie eine Funktion, statische Methode oder Instanzmethode), das
ein Argument akzeptiert: den Wurzelknoten des AST des Templates, der immer eine Instanz von
Latte\Compiler\Nodes\TemplateNode
ist.
Das primäre Ziel eines Kompilierungsdurchlaufs ist normalerweise eines oder beide der folgenden:
- Analyse: Den AST durchlaufen und Informationen über das Template sammeln (z. B. alle definierten Blöcke finden, die Verwendung spezifischer Tags prüfen, sicherstellen, dass bestimmte Sicherheitsbeschränkungen erfüllt sind).
- Modifikation: Die Struktur des AST oder die Attribute der Knoten ändern (z. B. automatisch HTML-Attribute hinzufügen, bestimmte Tag-Kombinationen optimieren, veraltete Tags durch neue ersetzen, Sandbox-Regeln implementieren).
Registrierung
Kompilierungsdurchläufe werden mit der Methode getPasses()
der
Erweiterung registriert. Diese Methode gibt ein assoziatives Array zurück, bei dem die Schlüssel eindeutige Namen der
Durchläufe sind (intern und zur Sortierung verwendet) und die Werte PHP Callables sind, die die Logik des Durchlaufs
implementieren.
Die von den Basis-Latte-Erweiterungen und Ihren eigenen Erweiterungen registrierten Durchläufe laufen sequentiell ab. Die
Reihenfolge kann wichtig sein, insbesondere wenn ein Durchlauf von den Ergebnissen oder Modifikationen eines anderen abhängt.
Latte bietet einen Hilfsmechanismus zur Steuerung dieser Reihenfolge, falls erforderlich; siehe Dokumentation zu Extension::getPasses()
für Details.
AST-Beispiel
Um eine bessere Vorstellung vom AST zu bekommen, fügen wir ein Beispiel hinzu. Dies ist das Quell-Template:
Und dies ist seine Repräsentation in Form eines 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\Essential\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') ) ) ) )
Durchlaufen des AST mit NodeTraverser
Das manuelle Schreiben rekursiver Funktionen zum Durchlaufen der komplexen AST-Struktur ist mühsam und fehleranfällig. Latte bietet ein spezielles Werkzeug für diesen Zweck: Latte\Compiler\NodeTraverser. Diese Klasse implementiert das Visitor Design Pattern, dank dem das Durchlaufen des AST systematisch und einfach zu handhaben ist.
Die grundlegende Verwendung umfasst die Erstellung einer Instanz von NodeTraverser
und den Aufruf ihrer Methode
traverse()
, wobei der Wurzelknoten des AST und ein oder zwei „Visitor“-Callables übergeben werden:
Sie können nur den enter
-Visitor, nur den leave
-Visitor oder beide bereitstellen, je nach Ihren
Bedürfnissen.
enter(Node $node)
: Diese Funktion wird für jeden Knoten ausgeführt, bevor der Traverser eines der
Kinder dieses Knotens besucht. Sie ist nützlich für:
- Das Sammeln von Informationen beim Durchlaufen des Baumes nach unten.
- Das Treffen von Entscheidungen vor der Verarbeitung der Kinder (wie die Entscheidung, sie zu überspringen, siehe Traversal-Optimierung).
- Potenzielle Änderungen am Knoten vor dem Besuch der Kinder (seltener).
leave(Node $node)
: Diese Funktion wird für jeden Knoten ausgeführt, nachdem alle seine Kinder (und
ihre gesamten Unterbäume) vollständig besucht wurden (sowohl Eintritt als auch Austritt). Sie ist der häufigste Ort für:
Beide Visitor enter
und leave
können optional einen Wert zurückgeben, um den Traversierungsprozess
zu beeinflussen. Die Rückgabe von null
(oder nichts) setzt die Traversierung normal fort, die Rückgabe einer
Node
-Instanz ersetzt den aktuellen Knoten, und die Rückgabe spezieller Konstanten wie
NodeTraverser::RemoveNode
oder NodeTraverser::StopTraversal
modifiziert den Fluss, wie in den folgenden
Abschnitten erläutert.
Wie die Traversierung funktioniert
NodeTraverser
verwendet intern die Methode getIterator()
, die jede Node
-Klasse
implementieren muss (wie in Erstellung eigener
Tags diskutiert). Sie iteriert über die mit getIterator()
erhaltenen Kinder, ruft rekursiv
traverse()
auf ihnen auf und stellt sicher, dass die enter
- und leave
-Visitor in der
korrekten Tiefensuche-Reihenfolge für jeden über Iteratoren erreichbaren Knoten im Baum aufgerufen werden. Dies unterstreicht
erneut, warum ein korrekt implementierter getIterator()
in Ihren eigenen Tag-Knoten absolut notwendig für das
korrekte Funktionieren von Kompilierungsdurchläufen ist.
Schreiben wir einen einfachen Durchlauf, der zählt, wie oft der Tag {do}
(repräsentiert durch
Latte\Essential\Nodes\DoNode
) im Template verwendet wird.
In diesem Beispiel benötigten wir nur den enter
-Visitor, um den Typ jedes besuchten Knotens zu überprüfen.
Als Nächstes untersuchen wir, wie diese Visitor den AST tatsächlich modifizieren.
Modifikation des AST
Einer der Hauptzwecke von Kompilierungsdurchläufen ist die Modifikation des abstrakten Syntaxbaums. Dies ermöglicht
leistungsstarke Transformationen, Optimierungen oder die Durchsetzung von Regeln direkt an der Struktur des Templates, bevor
PHP-Code generiert wird. NodeTraverser
bietet mehrere Möglichkeiten, dies innerhalb der enter
- und
leave
-Visitor zu erreichen.
Wichtiger Hinweis: Die Modifikation des AST erfordert Vorsicht. Falsche Änderungen – wie das Entfernen wesentlicher Knoten oder das Ersetzen eines Knotens durch einen inkompatiblen Typ – können zu Fehlern während der Codegenerierung führen oder unerwartetes Verhalten zur Laufzeit verursachen. Testen Sie Ihre Modifikationsdurchläufe immer gründlich.
Ändern von Knoteneigenschaften
Die einfachste Möglichkeit, den Baum zu modifizieren, ist die direkte Änderung der öffentlichen Eigenschaften der Knoten, die während der Traversierung besucht werden. Alle Knoten speichern ihre geparsten Argumente, Inhalte oder Attribute in öffentlichen Eigenschaften.
Beispiel: Erstellen wir einen Durchlauf, der alle statischen Textknoten (TextNode
, die normalen HTML- oder
Text außerhalb von Latte-Tags repräsentieren) findet und ihren Inhalt direkt im AST in Großbuchstaben umwandelt.
In diesem Beispiel prüft der enter
-Visitor, ob der aktuelle $node
vom Typ TextNode
ist.
Wenn ja, aktualisieren wir direkt seine öffentliche Eigenschaft $content
mit mb_strtoupper()
. Dies
ändert direkt den Inhalt des statischen Textes, der im AST gespeichert ist, bevor PHP-Code generiert wird. Da wir das
Objekt direkt modifizieren, müssen wir nichts vom Visitor zurückgeben.
Effekt: Wenn das Template <p>Hello</p>{= $var }<span>World</span>
enthielt, repräsentiert
der AST nach diesem Durchlauf etwas wie: <p>HELLO</p>{= $var }<span>WORLD</span>
. Dies
beeinflusst NICHT den Inhalt von $var
.
Ersetzen von Knoten
Eine leistungsfähigere Modifikationstechnik ist das vollständige Ersetzen eines Knotens durch einen anderen. Dies geschieht
durch die Rückgabe einer neuen Node
-Instanz vom enter
- oder leave
-Visitor.
NodeTraverser
ersetzt dann den ursprünglichen Knoten durch den zurückgegebenen in der Struktur des übergeordneten
Knotens.
Beispiel: Erstellen wir einen Durchlauf, der alle Verwendungen der Konstante PHP_VERSION
(repräsentiert
durch ConstantFetchNode
) findet und sie direkt durch ein String-Literal (StringNode
) ersetzt, das die
tatsächliche PHP-Version enthält, die während der Kompilierung erkannt wurde. Dies ist eine Form der
Optimierung zur Kompilierzeit.
Hier identifiziert der leave
-Visitor den spezifischen ConstantFetchNode
für
PHP_VERSION
. Anschließend erstellt er einen völlig neuen StringNode
, der den Wert der Konstante
PHP_VERSION
zur Kompilierzeit enthält. Durch die Rückgabe dieses $newNode
weist er den
Traverser an, den ursprünglichen ConstantFetchNode
im AST zu ersetzen.
Effekt: Wenn das Template {= PHP_VERSION }
enthielt und die Kompilierung auf PHP 8.2.1 läuft, repräsentiert der
AST nach diesem Durchlauf effektiv {= '8.2.1' }
.
Wahl zwischen enter
und leave
zum Ersetzen:
- Verwenden Sie
leave
, wenn die Erstellung des neuen Knotens von den Ergebnissen der Verarbeitung der Kinder des alten Knotens abhängt oder wenn Sie einfach sicherstellen möchten, dass die Kinder vor dem Ersetzen besucht werden (übliche Praxis). - Verwenden Sie
enter
, wenn Sie einen Knoten ersetzen möchten, bevor seine Kinder überhaupt besucht werden.
Entfernen von Knoten
Sie können einen Knoten vollständig aus dem AST entfernen, indem Sie die spezielle Konstante
NodeTraverser::RemoveNode
vom Visitor zurückgeben.
Beispiel: Entfernen wir alle Template-Kommentare ({* ... *}
), die durch CommentNode
im vom
Latte-Kern generierten AST repräsentiert werden (obwohl sie typischerweise früher verarbeitet werden, dient dies als
Beispiel).
Warnung: Verwenden Sie RemoveNode
mit Vorsicht. Das Entfernen eines Knotens, der wesentlichen Inhalt
enthält oder die Struktur beeinflusst (wie das Entfernen des Inhaltsknotens einer Schleife), kann zu beschädigten Templates oder
ungültigem generiertem Code führen. Am sichersten ist es für Knoten, die tatsächlich optional oder eigenständig sind (wie
Kommentare oder Debugging-Tags) oder für leere strukturelle Knoten (z. B. kann ein leerer FragmentNode
in einigen
Kontexten sicher durch einen Bereinigungsdurchlauf entfernt werden).
Diese drei Methoden – Änderung von Eigenschaften, Ersetzen von Knoten und Entfernen von Knoten – bieten die grundlegenden Werkzeuge zur Manipulation des AST im Rahmen Ihrer Kompilierungsdurchläufe.
Optimierung der Traversierung
Der AST eines Templates kann ziemlich groß sein und potenziell Tausende von Knoten enthalten. Das Durchlaufen jedes einzelnen
Knotens kann unnötig sein und die Kompilierungsleistung beeinträchtigen, wenn Ihr Durchlauf nur an spezifischen Teilen des
Baumes interessiert ist. NodeTraverser
bietet Möglichkeiten zur Optimierung der Traversierung:
Überspringen von Kindern
Wenn Sie wissen, dass, sobald Sie auf einen bestimmten Knotentyp stoßen, keiner seiner Nachkommen die Knoten enthalten kann,
nach denen Sie suchen, können Sie dem Traverser sagen, dass er den Besuch seiner Kinder überspringen soll. Dies geschieht durch
die Rückgabe der Konstante NodeTraverser::DontTraverseChildren
vom enter
-Visitor. Dadurch werden
ganze Zweige bei der Traversierung ausgelassen, was potenziell erheblich Zeit sparen kann, insbesondere in Templates mit komplexen
PHP-Ausdrücken innerhalb von Tags.
Anhalten der Traversierung
Wenn Ihr Durchlauf nur das erste Vorkommen von etwas finden muss (ein spezifischer Knotentyp, die Erfüllung einer
Bedingung), können Sie den gesamten Traversierungsprozess vollständig anhalten, sobald Sie es gefunden haben. Dies wird
erreicht, indem die Konstante NodeTraverser::StopTraversal
vom enter
- oder leave
-Visitor
zurückgegeben wird. Die Methode traverse()
hört auf, weitere Knoten zu besuchen. Dies ist äußerst effizient, wenn
Sie nur die erste Übereinstimmung in einem potenziell sehr großen Baum benötigen.
Nützlicher Helfer NodeHelpers
Während NodeTraverser
eine feingranulare Kontrolle bietet, stellt Latte auch eine praktische Hilfsklasse zur
Verfügung, Latte\Compiler\NodeHelpers, die
NodeTraverser
für mehrere gängige Such- und Analyseaufgaben kapselt und oft weniger Boilerplate-Code erfordert.
find(Node $startNode, callable $filter): array
Diese statische Methode findet alle Knoten im Unterbaum, der bei $startNode
beginnt (einschließlich), die
dem Callback $filter
entsprechen. Sie gibt ein Array der übereinstimmenden Knoten zurück.
Beispiel: Finde alle Variablenknoten (VariableNode
) im gesamten Template.
findFirst(Node $startNode, callable $filter): ?Node
Ähnlich wie find
, aber stoppt die Traversierung sofort nach dem Finden des ersten Knotens, der dem
Callback $filter
entspricht. Gibt das gefundene Node
-Objekt oder null
zurück, wenn kein
übereinstimmender Knoten gefunden wird. Dies ist im Wesentlichen eine praktische Hülle um
NodeTraverser::StopTraversal
.
Beispiel: Finde den {parameters}
-Knoten (wie das manuelle Beispiel zuvor, aber kürzer).
toValue(ExpressionNode $node, bool $constants = false): mixed
Diese statische Methode versucht, einen ExpressionNode
zur Kompilierzeit auszuwerten und seinen
entsprechenden PHP-Wert zurückzugeben. Sie funktioniert zuverlässig nur für einfache Literal-Knoten (StringNode
,
IntegerNode
, FloatNode
, BooleanNode
, NullNode
) und Instanzen von
ArrayNode
, die nur solche auswertbaren Elemente enthalten.
Wenn $constants
auf true
gesetzt ist, versucht sie auch, ConstantFetchNode
und
ClassConstantFetchNode
durch Überprüfung von defined()
und Verwendung von constant()
aufzulösen.
Wenn der Knoten Variablen, Funktionsaufrufe oder andere dynamische Elemente enthält, kann er zur Kompilierzeit nicht
ausgewertet werden, und die Methode löst eine InvalidArgumentException
aus.
Anwendungsfall: Erhalten des statischen Werts eines Tag-Arguments während der Kompilierung für Entscheidungen zur Kompilierzeit.
toText(?Node $node): ?string
Diese statische Methode ist nützlich zum Extrahieren von einfachem Textinhalt aus einfachen Knoten. Sie funktioniert hauptsächlich mit:
TextNode
: Gibt seinen$content
zurück.FragmentNode
: Verkettet das Ergebnis vontoText()
für alle seine Kinder. Wenn ein Kind nicht in Text konvertierbar ist (z. B. einenPrintNode
enthält), gibt sienull
zurück.NopNode
: Gibt einen leeren String zurück.- Andere Knotentypen: Gibt
null
zurück.
Anwendungsfall: Erhalten des statischen Textinhalts des Werts eines HTML-Attributs oder eines einfachen HTML-Elements zur Analyse während eines Kompilierungsdurchlaufs.
NodeHelpers
kann Ihre Kompilierungsdurchläufe vereinfachen, indem es fertige Lösungen für gängige Aufgaben der
AST-Traversierung und -Analyse bietet.
Praktische Beispiele
Wenden wir die Konzepte der AST-Traversierung und -Modifikation an, um einige praktische Probleme zu lösen. Diese Beispiele demonstrieren gängige Muster, die in Kompilierungsdurchläufen verwendet werden.
Automatisches Hinzufügen von loading="lazy"
zu
<img>
Moderne Browser unterstützen natives Lazy Loading für Bilder mit dem Attribut loading="lazy"
. Erstellen wir
einen Durchlauf, der dieses Attribut automatisch zu allen <img>
-Tags hinzufügt, die noch kein
loading
-Attribut haben.
Erklärung:
- Der
enter
-Visitor sucht nachHtml\ElementNode
-Knoten mit dem Namenimg
. - Er iteriert durch die vorhandenen Attribute (
$node->attributes->children
) und prüft, ob dasloading
-Attribut bereits vorhanden ist. - Wenn es nicht gefunden wird, erstellt er einen neuen
Html\AttributeNode
, derloading="lazy"
repräsentiert.
Überprüfung von Funktionsaufrufen
Kompilierungsdurchläufe sind die Grundlage der Latte Sandbox. Obwohl die tatsächliche Sandbox ausgefeilt ist, können wir das Grundprinzip der Überprüfung verbotener Funktionsaufrufe demonstrieren.
Ziel: Die Verwendung der potenziell gefährlichen Funktion shell_exec
innerhalb von Template-Ausdrücken
verhindern.
Erklärung:
- Wir definieren eine Liste verbotener Funktionsnamen.
- Der
enter
-Visitor prüft aufFunctionCallNode
. - Wenn der Funktionsname (
$node->name
) ein statischerNameNode
ist, prüfen wir seine kleingeschriebene String-Repräsentation gegen unsere verbotene Liste. - Wenn eine verbotene Funktion gefunden wird, werfen wir eine
Latte\SecurityViolationException
, die klar die Verletzung der Sicherheitsregel anzeigt und die Kompilierung stoppt.
Diese Beispiele zeigen, wie Kompilierungsdurchläufe mit NodeTraverser
genutzt werden können, um Analyse,
automatische Modifikationen und die Durchsetzung von Sicherheitsbeschränkungen durch direkte Interaktion mit der AST-Struktur des
Templates zu erreichen.
Best Practices
Beachten Sie beim Schreiben von Kompilierungsdurchläufen diese Richtlinien, um robuste, wartbare und effiziente Erweiterungen zu erstellen:
- Reihenfolge ist wichtig: Seien Sie sich der Reihenfolge bewusst, in der Durchläufe ausgeführt werden. Wenn Ihr
Durchlauf von der AST-Struktur abhängt, die von einem anderen Durchlauf erstellt wurde (z. B. Latte-Kerndurchläufe oder ein
anderer benutzerdefinierter Durchlauf), oder wenn andere Durchläufe von Ihren Modifikationen abhängen könnten, verwenden Sie
den von
Extension::getPasses()
bereitgestellten Sortiermechanismus, um Abhängigkeiten zu definieren (before
/after
). Siehe Dokumentation zuExtension::getPasses()
für Details. - Einzelverantwortung: Streben Sie nach Durchläufen, die eine gut definierte Aufgabe erfüllen. Für komplexe Transformationen erwägen Sie, die Logik in mehrere Durchläufe aufzuteilen – vielleicht einen für die Analyse und einen weiteren für die Modifikation basierend auf den Analyseergebnissen. Dies verbessert die Übersichtlichkeit und Testbarkeit.
- Leistung: Denken Sie daran, dass Kompilierungsdurchläufe die Kompilierungszeit des Templates erhöhen (obwohl dies
normalerweise nur einmal geschieht, bis das Template geändert wird). Vermeiden Sie rechenintensive Operationen in Ihren
Durchläufen, wenn möglich. Nutzen Sie Traversierungsoptimierungen wie
NodeTraverser::DontTraverseChildren
undNodeTraverser::StopTraversal
, wann immer Sie wissen, dass Sie bestimmte Teile des AST nicht besuchen müssen. - Verwenden Sie
NodeHelpers
: Für gängige Aufgaben wie das Finden spezifischer Knoten oder die statische Auswertung einfacher Ausdrücke prüfen Sie, obLatte\Compiler\NodeHelpers
eine geeignete Methode bietet, bevor Sie Ihre eigeneNodeTraverser
-Logik schreiben. Dies kann Zeit sparen und die Menge an Boilerplate-Code reduzieren. - Fehlerbehandlung: Wenn Ihr Durchlauf einen Fehler oder einen ungültigen Zustand im AST des Templates erkennt, werfen
Sie eine
Latte\CompileException
(oderLatte\SecurityViolationException
für Sicherheitsprobleme) mit einer klaren Meldung und dem relevantenPosition
-Objekt (normalerweise$node->position
). Dies gibt dem Template-Entwickler nützliches Feedback. - Idempotenz (wenn möglich): Idealerweise sollte das mehrmalige Ausführen Ihres Durchlaufs auf demselben AST dasselbe Ergebnis liefern wie das einmalige Ausführen. Dies ist nicht immer machbar, vereinfacht aber das Debugging und das Nachdenken über Interaktionen zwischen Durchläufen, wenn es erreicht wird. Stellen Sie beispielsweise sicher, dass Ihr Modifikationsdurchlauf prüft, ob die Modifikation bereits angewendet wurde, bevor Sie sie erneut anwenden.
Durch Befolgen dieser Praktiken können Sie Kompilierungsdurchläufe effektiv nutzen, um die Fähigkeiten von Latte auf leistungsstarke und zuverlässige Weise zu erweitern, was zu sichereren, optimierteren oder funktionsreicheren Template-Verarbeitungen beiträgt.