Tworzenie compiler passów
Compiler passy to potężny mechanizm pozwalający analizować i modyfikować szablony Latte po sparsowaniu ich do drzewa składniowego (AST) i przed wygenerowaniem końcowego kodu PHP. Umożliwia to zaawansowane manipulacje szablonami, optymalizacje, kontrole bezpieczeństwa (jak Sandbox) i zbieranie informacji o szablonach. Ten przewodnik przeprowadzi Cię przez tworzenie własnych compiler passów.
Czym jest compiler pass?
Aby zrozumieć rolę compiler passów, zobacz proces kompilacji w Latte. Jak widać, compiler passy działają w kluczowym momencie i pozwalają głęboko ingerować między wstępnym parsowaniem a końcowym wynikiem w postaci kodu.
W swojej istocie compiler pass to po prostu callable PHP (funkcja, metoda statyczna albo metoda instancji), który przyjmuje
jeden argument: korzeń drzewa AST szablonu, zawsze będący instancją Latte\Compiler\Nodes\TemplateNode.
Głównym celem compiler passa jest zwykle jedno lub oba z poniższych:
- Analiza: przejście po AST i zebranie informacji o szablonie (np. znalezienie wszystkich zdefiniowanych bloków, sprawdzenie użycia konkretnych tagów, upewnienie się, że spełnione są określone wymogi bezpieczeństwa).
- Modyfikacja: zmiana struktury AST albo właściwości węzłów (np. automatyczne dodanie atrybutów HTML, optymalizacja pewnych kombinacji tagów, zastąpienie przestarzałych tagów nowymi, wprowadzenie reguł sandboxa).
Rejestracja
Compiler passy rejestruje się metodą getPasses() w rozszerzeniu. Metoda ta zwraca tablicę asocjacyjną, w
której kluczami są unikalne nazwy passów (używane wewnętrznie i do ustalania kolejności), a wartościami callable PHP
implementujące logikę passa.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... inne passy ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// implementacja...
}
}
Passy zarejestrowane przez rdzenne rozszerzenia Latte i przez Twoje własne rozszerzenia uruchamiają się kolejno.
Kolejność może być istotna, zwłaszcza gdy jeden pass opiera się na wynikach lub modyfikacjach innego. Latte udostępnia
mechanizm pomocniczy do sterowania tą kolejnością, gdy zajdzie potrzeba; szczegóły w dokumentacji Extension::getPasses().
Przykład AST
Aby lepiej wyobrazić sobie AST, dodajemy próbkę. Oto szablon źródłowy:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
A oto jego reprezentacja w postaci 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')
)
)
)
)
Przechodzenie po AST za pomocą NodeTraverser
Ręczne pisanie funkcji rekurencyjnych przechodzących przez złożoną strukturę AST jest żmudne i podatne na błędy. Latte udostępnia do tego dedykowane narzędzie: Latte\Compiler\NodeTraverser. Klasa ta implementuje wzorzec projektowy Visitor, dzięki czemu przechodzenie po AST staje się systematyczne i łatwe do ogarnięcia.
Podstawowe użycie polega na utworzeniu instancji NodeTraverser i wywołaniu jej metody traverse(),
przekazując korzeń AST oraz jeden lub dwa “wizytujące” callable:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// wizytator 'enter': wywoływany przy wejściu do węzła (przed jego potomkami)
enter: function (Node $node) {
echo "Entering node of type: " . $node::class . "\n";
// tutaj możesz zbadać węzeł
if ($node instanceof Nodes\TextNode) {
// echo "Found text: " . $node->content . "\n";
}
},
// wizytator 'leave': wywoływany przy wyjściu z węzła (po jego potomkach)
leave: function (Node $node) {
echo "Leaving node of type: " . $node::class . "\n";
// tutaj możesz wykonać akcje po przetworzeniu potomków
},
);
Możesz podać tylko wizytator enter, tylko leave albo oba, w zależności od potrzeb.
enter(Node $node): Ta funkcja wykonuje się dla każdego węzła przed odwiedzeniem przez traverser
jego potomków. Przydaje się do:
- zbierania informacji podczas schodzenia w głąb drzewa,
- podejmowania decyzji przed przetworzeniem potomków (jak decyzja o ich pominięciu, zobacz Optymalizacja przechodzenia),
- ewentualnej modyfikacji węzła przed odwiedzeniem potomków (rzadsze).
leave(Node $node): Ta funkcja wykonuje się dla każdego węzła po pełnym odwiedzeniu wszystkich
jego potomków (i całych ich poddrzew, zarówno wejściu, jak i wyjściu). To najczęstsze miejsce na:
- zastąpienie węzła po przetworzeniu jego potomków,
- usuwanie węzłów z AST,
- agregowanie informacji zebranych z całego poddrzewa.
Zarówno wizytator enter, jak i leave może opcjonalnie zwrócić wartość wpływającą na proces
przechodzenia. Zwrócenie null (albo niczego) kontynuuje przechodzenie normalnie, zwrócenie instancji
Node zastępuje bieżący węzeł, a zwrócenie specjalnych stałych, takich jak
NodeTraverser::RemoveNode czy NodeTraverser::StopTraversal, zmienia przebieg, co wyjaśniają kolejne
sekcje.
Jak działa przechodzenie
NodeTraverser wewnętrznie korzysta z metody getIterator(), którą musi implementować każda klasa
Node (jak omówiono w Tworzenie własnych tagów). Iteruje
po potomkach zwracanych przez getIterator(), rekurencyjnie wywołuje na nich traverse() i zapewnia, że
wizytatory enter i leave są wywoływane we właściwej kolejności w głąb dla każdego węzła w
drzewie dostępnego przez iteratory. To po raz kolejny pokazuje, dlaczego poprawnie zaimplementowana metoda
getIterator() w węzłach Twoich własnych tagów jest absolutnie niezbędna do prawidłowego działania compiler
passów.
Napiszmy prosty pass, który zliczy, ile razy w szablonie użyto tagu {do} (reprezentowanego przez
Latte\Essential\Nodes\DoNode).
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++;
}
},
// wizytator 'leave' nie jest do tego zadania potrzebny
);
echo "Found {do} tag $count times.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
W tym przykładzie potrzebowaliśmy tylko wizytatora enter, aby sprawdzić typ każdego napotkanego węzła.
Następnie przyjrzymy się, jak używać tych wizytatorów do faktycznej modyfikacji AST.
Modyfikowanie AST
Jednym z głównych zastosowań compiler passów jest modyfikowanie drzewa składniowego. Pozwala to na potężne
transformacje, optymalizacje albo wymuszanie reguł bezpośrednio na strukturze szablonu, zanim wygenerowany zostanie kod PHP.
NodeTraverser udostępnia kilka sposobów, aby to osiągnąć wewnątrz wizytatorów enter i
leave.
Ważna uwaga: Modyfikowanie AST wymaga ostrożności. Niepoprawne zmiany, jak usunięcie istotnych węzłów albo zastąpienie węzła niekompatybilnym typem, mogą prowadzić do błędów przy generowaniu kodu albo do nieoczekiwanego zachowania w czasie działania. Zawsze dokładnie testuj swoje passy modyfikujące.
Zmiana właściwości węzła
Najprostszym sposobem modyfikacji drzewa jest bezpośrednia zmiana właściwości publicznych węzłów napotkanych podczas przechodzenia. Wszystkie węzły przechowują swoje sparsowane argumenty, treść czy atrybuty we właściwościach publicznych.
Przykład: Utwórzmy pass, który znajdzie wszystkie statyczne węzły tekstowe (TextNode, reprezentujące
zwykły HTML albo tekst poza tagami Latte) i zamieni ich treść na wielkie litery bezpośrednio w AST.
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,
// możemy użyć 'enter', bo TextNode nie ma potomków do wcześniejszego przetworzenia
enter: function (Node $node) {
// czy ten węzeł to statyczny blok tekstu?
if ($node instanceof TextNode) {
// tak! modyfikujemy bezpośrednio jego publiczną właściwość 'content'
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// nie trzeba niczego zwracać; modyfikacja dzieje się w miejscu
},
);
}
W tym przykładzie wizytator enter sprawdza, czy bieżący $node jest typu TextNode.
Jeśli tak, bezpośrednio aktualizujemy jego publiczną właściwość $content za pomocą
mb_strtoupper(). Zmienia to bezpośrednio treść tekstu statycznego przechowywaną w AST przed wygenerowaniem
kodu PHP. Ponieważ modyfikujemy obiekt bezpośrednio, nie musimy niczego z wizytatora zwracać.
Efekt: Jeśli szablon zawierał <p>Hello</p>{= $var }<span>World</span>, po tym passie AST
będzie reprezentować coś w rodzaju: <p>HELLO</p>{= $var }<span>WORLD</span>. NIE wpływa
to na zawartość $var.
Zastępowanie węzłów
Potężniejszą techniką modyfikacji jest całkowite zastąpienie węzła innym. Robi się to przez zwrócenie nowej
instancji Node z wizytatora enter albo leave. NodeTraverser podstawi
wtedy zwrócony węzeł w miejsce pierwotnego w strukturze węzła nadrzędnego.
Przykład: Utwórzmy pass, który znajdzie wszystkie użycia stałej PHP_VERSION (reprezentowanej przez
ConstantFetchNode) i zastąpi je bezpośrednio literałem łańcuchowym (StringNode) zawierającym
rzeczywistą wersję PHP wykrytą podczas kompilacji. To forma optymalizacji w czasie kompilacji.
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,
// do zastępowania często używa się 'leave', co zapewnia wcześniejsze
// przetworzenie potomków (jeśli są), choć 'enter' też by tu zadziałał
leave: function (Node $node) {
// czy to węzeł dostępu do stałej o nazwie 'PHP_VERSION'?
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// tworzymy nowy StringNode z bieżącą wersją PHP
$newNode = new StringNode(PHP_VERSION);
// opcjonalne, ale dobra praktyka: kopiujemy informacje o pozycji
$newNode->position = $node->position;
// zwracamy nowy StringNode; traverser zastąpi nim
// pierwotny ConstantFetchNode
return $newNode;
}
// jeśli nie zwrócimy węzła, pierwotny $node zostaje zachowany
},
);
}
Tutaj wizytator leave rozpoznaje konkretny ConstantFetchNode dla PHP_VERSION. Następnie
tworzy zupełnie nowy StringNode zawierający wartość stałej PHP_VERSION w czasie
kompilacji. Zwracając ten $newNode, mówi traverserowi, aby zastąpił nim pierwotny
ConstantFetchNode w AST.
Efekt: Jeśli szablon zawierał {= PHP_VERSION }, a kompilacja odbywa się na PHP 8.2.1, AST po tym passie będzie
faktycznie reprezentować {= '8.2.1' }.
Wybór enter czy leave przy zastępowaniu:
- Użyj
leave, jeśli utworzenie nowego węzła zależy od wyników przetworzenia potomków starego węzła albo jeśli po prostu chcesz mieć pewność, że potomkowie zostaną odwiedzeni przed zastąpieniem (częsta praktyka). - Użyj
enter, jeśli chcesz zastąpić węzeł zanim jego potomkowie w ogóle zostaną odwiedzeni.
Usuwanie węzłów
Węzeł możesz całkowicie usunąć z AST, zwracając z wizytatora specjalną stałą
NodeTraverser::RemoveNode.
Przykład: Usuńmy z wyniku wszystkie komentarze HTML (<!-- ... -->). Komentarzy Latte
{* ... *} nie da się w ten sposób namierzyć, bo parser odrzuca ich treść i zastępuje je pustym
NopNode zamiast dedykowanym węzłem komentarza, ale komentarze HTML są zachowywane jako węzły
Html\CommentNode, więc możemy je tutaj usunąć.
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' w zupełności wystarczy, bo do usunięcia komentarza nie potrzebujemy potomków
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// sygnalizujemy traverserowi, aby usunął ten węzeł z AST
return NodeTraverser::RemoveNode;
}
},
);
}
Uwaga: Używaj RemoveNode ostrożnie. Usunięcie węzła zawierającego istotną treść albo
wpływającego na strukturę (jak usunięcie węzła z treścią pętli) może prowadzić do zepsutych szablonów albo
nieprawidłowego wygenerowanego kodu. Najbezpieczniejsze jest to przy węzłach naprawdę opcjonalnych lub samodzielnych (jak
komentarze czy tagi debugowe) albo przy pustych węzłach strukturalnych (np. pusty FragmentNode może w pewnych
kontekstach zostać bezpiecznie usunięty przez pass porządkujący).
Te trzy metody – modyfikowanie właściwości, zastępowanie węzłów i usuwanie węzłów – dają podstawowe narzędzia do manipulowania AST wewnątrz Twoich compiler passów.
Optymalizacja przechodzenia
Drzewa AST szablonów mogą być całkiem duże i zawierać nawet tysiące węzłów. Odwiedzanie każdego z nich bywa
zbędne i może odbić się na wydajności kompilacji, jeśli Twój pass interesuje się tylko określonymi częściami drzewa.
NodeTraverser oferuje sposoby optymalizacji przechodzenia:
Pomijanie potomków
Jeśli wiesz, że po napotkaniu węzła określonego typu żaden z jego potomków nie może zawierać szukanych węzłów,
możesz kazać traverserowi pominąć odwiedzanie jego potomków. Robi się to przez zwrócenie stałej
NodeTraverser::DontTraverseChildren z wizytatora enter. Odcinasz w ten sposób całe gałęzie
od ścieżki przechodzenia, co może oszczędzić sporo czasu, zwłaszcza w szablonach ze złożonymi wyrażeniami PHP
wewnątrz tagów.
Zatrzymanie przechodzenia
Jeśli Twój pass potrzebuje znaleźć tylko pierwsze wystąpienie czegoś (określonego typu węzła, spełnienia
warunku), możesz po znalezieniu całkowicie zatrzymać cały proces przechodzenia. Osiąga się to przez zwrócenie stałej
NodeTraverser::StopTraversal z wizytatora enter albo leave. Metoda traverse()
przestaje wtedy odwiedzać kolejne węzły. Jest to bardzo skuteczne, gdy potrzebujesz tylko pierwszego trafienia w potencjalnie
bardzo dużym drzewie.
Przydatna klasa NodeHelpers
NodeTraverser daje precyzyjną kontrolę, ale Latte udostępnia też wygodną klasę pomocniczą Latte\Compiler\NodeHelpers, która opakowuje
NodeTraverser dla kilku typowych zadań wyszukiwania i analizy, zwykle wymagając mniej powtarzalnego kodu.
find (Node $startNode, callable $filter): array
Ta statyczna metoda znajduje wszystkie węzły w poddrzewie zaczynającym się od $startNode (włącznie),
które spełniają callback $filter. Zwraca tablicę pasujących węzłów.
Przykład: Znajdź w całym szablonie wszystkie węzły zmiennych (VariableNode).
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
Podobna do find, ale zatrzymuje przechodzenie natychmiast po znalezieniu pierwszego węzła spełniającego
callback $filter. Zwraca znaleziony obiekt Node albo null, jeśli żaden pasujący węzeł
się nie znajdzie. To w istocie wygodna nakładka na NodeTraverser::StopTraversal.
Przykład: Znajdź węzeł {parameters}.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;
function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
return NodeHelpers::findFirst(
$templateNode->head, // dla wydajności szukamy tylko w sekcji head
fn($node) => $node instanceof ParametersNode,
);
}
clone (Latte\Compiler\Node $node): Node
Ta statyczna metoda tworzy głęboką kopię węzła i całego jego poddrzewa. Przydaje się, gdy potrzebujesz zduplikować gałąź AST, na przykład wstawić zmodyfikowaną kopię węzła, zostawiając oryginał nietknięty.
use Latte\Compiler\NodeHelpers;
$copy = NodeHelpers::clone($node);
toValue (ExpressionNode $node, bool $constants = false): mixed
Ta statyczna metoda próbuje obliczyć ExpressionNode w czasie kompilacji i zwrócić odpowiadającą mu
wartość PHP. Działa niezawodnie tylko dla prostych węzłów literałowych (StringNode, IntegerNode,
FloatNode, BooleanNode, NullNode) i instancji ArrayNode zawierających
wyłącznie takie obliczalne elementy.
Jeśli $constants zostanie ustawione na true, spróbuje też rozwiązać
ConstantFetchNode i ClassConstantFetchNode, sprawdzając defined() i używając
constant().
Jeśli węzeł zawiera zmienne, wywołania funkcji albo inne elementy dynamiczne, nie da się go obliczyć w czasie kompilacji
i metoda zgłosi InvalidArgumentException.
Zastosowanie: Uzyskanie statycznej wartości argumentu tagu podczas kompilacji, aby podjąć decyzje w czasie kompilacji.
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) {
// argument nie był statycznym literałem łańcuchowym
return null;
}
}
toText (?Node $node): ?string
Ta statyczna metoda przydaje się do wydobycia zwykłego tekstu z prostych węzłów. Działa przede wszystkim z:
TextNode: zwraca jego$content.FragmentNode: skleja wyniktoText()dla wszystkich swoich potomków. Jeśli któryś potomek nie da się przekształcić w tekst (np. zawieraPrintNode), zwracanull.NopNode: zwraca pusty łańcuch.- Pozostałe typy węzłów: zwraca
null.
Zastosowanie: Uzyskanie statycznej treści tekstowej wartości atrybutu HTML albo prostego elementu HTML do analizy podczas compiler passa.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// $attr->value to zwykle AreaNode (jak FragmentNode albo TextNode)
return NodeHelpers::toText($attr->value);
}
// przykład użycia w passie:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = $node->getAttribute('name');
// if ($nameAttrValue === 'description') { ... }
// }
NodeHelpers może uprościć Twoje compiler passy, dostarczając gotowe rozwiązania typowych zadań przechodzenia
po AST i jego analizy.
Praktyczne przykłady
Zastosujmy koncepcje przechodzenia i modyfikowania AST do rozwiązania kilku praktycznych problemów. Te przykłady pokazują typowe wzorce używane w compiler passach.
Automatyczne dodawanie loading="lazy" do
<img>
Nowoczesne przeglądarki obsługują natywne leniwe ładowanie obrazków przez atrybut loading="lazy". Utwórzmy
pass, który automatycznie doda ten atrybut do wszystkich tagów <img>, które nie mają jeszcze atrybutu
loading.
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,
// możemy użyć 'enter', bo modyfikujemy węzeł bezpośrednio
// i decyzja nie zależy od potomków
enter: function (Node $node) {
// czy to element HTML o nazwie 'img'?
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// sprawdzamy, czy atrybut 'loading' już istnieje (bez rozróżniania wielkości liter)
foreach ($node->attributes->children as $attrNode) {
if ($attrNode instanceof Html\AttributeNode
&& $attrNode->name instanceof Nodes\TextNode // statyczna nazwa atrybutu
&& strtolower($attrNode->name->content) === 'loading'
) {
return; // już istnieje, nic nie robimy
}
}
// jeśli atrybuty nie są puste, poprzedzamy spacją
if ($node->attributes->children) {
$node->attributes->children[] = new Nodes\TextNode(' ');
}
// tworzymy nowy węzeł atrybutu: loading="lazy"
$node->attributes->children[] = new Html\AttributeNode(
name: new Nodes\TextNode('loading'),
value: new Nodes\TextNode('lazy'),
quote: '"',
);
// modyfikacja wykonana w miejscu, nie trzeba niczego zwracać
}
},
);
}
Wyjaśnienie:
- Wizytator
enterszuka węzłówHtml\ElementNodeo nazwieimg. - Przechodzi po istniejących atrybutach (
$node->attributes->children), aby sprawdzić, czy atrybutloadingjuż tam jest. - Jeśli go nie znajdzie, tworzy nowy
Html\AttributeNodereprezentującyloading="lazy"i dodaje go (w razie potrzeby poprzedzając spacją).
Kontrola wywołań funkcji
Compiler passy stanowią fundament Sandboxa w Latte. Prawdziwy Sandbox jest wyrafinowany, ale możemy pokazać podstawową zasadę kontroli zabronionych wywołań funkcji.
Cel: Uniemożliwić użycie w wyrażeniach szablonu potencjalnie niebezpiecznej funkcji shell_exec.
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]; // prosta lista
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use ($forbiddenFunctions) {
// czy to węzeł bezpośredniego wywołania funkcji?
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,
);
}
},
);
}
Wyjaśnienie:
- Definiujemy listę zabronionych nazw funkcji.
- Wizytator
entersprawdzaFunctionCallNode. - Jeśli nazwa funkcji (
$node->name) jest statycznymNameNode, porównujemy jej reprezentację łańcuchową pisaną małymi literami z naszą listą zabronionych. - Jeśli znajdziemy zabronioną funkcję, zgłaszamy
Latte\SecurityViolationException, co jasno wskazuje na naruszenie reguły bezpieczeństwa i zatrzymuje kompilację.
Te przykłady pokazują, jak compiler passy z użyciem NodeTraverser można wykorzystać do analizy,
automatycznych modyfikacji i wymuszania ograniczeń bezpieczeństwa przez bezpośrednią pracę ze strukturą AST szablonu.
Dobre praktyki
Pisząc compiler passy, miej na uwadze te wskazówki, aby tworzyć solidne, łatwe w utrzymaniu i wydajne rozszerzenia:
- Kolejność ma znaczenie: Pamiętaj o kolejności, w jakiej uruchamiają się passy. Jeśli Twój pass opiera się na
strukturze AST utworzonej przez inny pass (np. rdzenne passy Latte albo inny własny pass) albo jeśli inne passy mogą zależeć
od Twoich modyfikacji, użyj mechanizmu porządkowania udostępnianego przez
Extension::getPasses()do zdefiniowania zależności (before/after). Szczegóły w dokumentacjiExtension::getPasses(). - Jedna odpowiedzialność: Dąż do tego, aby pass wykonywał jedno, dobrze określone zadanie. Przy złożonych transformacjach rozważ podzielenie logiki na kilka passów, na przykład jeden do analizy, a drugi do modyfikacji na podstawie jej wyników. Poprawia to przejrzystość i testowalność.
- Wydajność: Pamiętaj, że compiler passy wydłużają czas kompilacji szablonu (choć zwykle dzieje się to tylko
raz, do momentu zmiany szablonu). W miarę możliwości unikaj w passach operacji kosztownych obliczeniowo. Korzystaj
z optymalizacji przechodzenia, takich jak
NodeTraverser::DontTraverseChildreniNodeTraverser::StopTraversal, gdy tylko wiesz, że nie musisz odwiedzać pewnych części AST. - Używaj
NodeHelpers: Przy typowych zadaniach, takich jak znajdowanie konkretnych węzłów albo statyczne obliczanie prostych wyrażeń, sprawdź, czyLatte\Compiler\NodeHelpersnie oferuje odpowiedniej metody, zanim napiszesz własną logikę naNodeTraverser. Może to oszczędzić czas i ograniczyć powtarzalny kod. - Obsługa błędów: Jeśli Twój pass wykryje błąd albo nieprawidłowy stan w AST szablonu, zgłoś
Latte\CompileException(alboLatte\SecurityViolationExceptionprzy problemach bezpieczeństwa) z jasnym komunikatem i odpowiednim obiektemPosition(zwykle$node->position). Daje to pomocną informację zwrotną autorowi szablonu. - Idempotentność (jeśli to możliwe): Idealnie wielokrotne uruchomienie Twojego passa na tym samym AST powinno dawać ten sam wynik co uruchomienie jednorazowe. Nie zawsze da się to osiągnąć, ale gdy się uda, upraszcza to debugowanie i rozumowanie o wzajemnym oddziaływaniu passów. Na przykład zadbaj, aby Twój pass modyfikujący sprawdzał, czy modyfikacja nie została już zastosowana, zanim zastosuje ją ponownie.
Trzymając się tych praktyk, możesz skutecznie wykorzystywać compiler passy do rozszerzania możliwości Latte w potężny i niezawodny sposób, przyczyniając się do bezpieczniejszego, bardziej zoptymalizowanego i bogatszego w funkcje przetwarzania szablonów.