Przebiegi kompilacji
Przebiegi kompilacji stanowią potężny mechanizm do analizy i modyfikacji szablonów Latte po ich sparsowaniu do abstrakcyjnego drzewa składni (AST) i przed wygenerowaniem ostatecznego kodu PHP. Umożliwia to zaawansowaną manipulację szablonami, optymalizacje, kontrole bezpieczeństwa (takie jak Sandbox) oraz zbieranie informacji o szablonach. Ten przewodnik poprowadzi Cię przez tworzenie własnych przebiegów kompilacji.
Co to jest przebieg kompilacji?
Aby zrozumieć rolę przebiegów kompilacji, spójrz na proces kompilacji Latte. Jak możesz zobaczyć, przebiegi kompilacji działają w kluczowej fazie, umożliwiając głęboką interwencję między początkowym parsowaniem a ostatecznym wyjściem kodu.
W istocie przebieg kompilacji to po prostu PHP callable (jak funkcja, metoda statyczna lub metoda instancji), który przyjmuje
jeden argument: korzeniowy węzeł AST szablonu, który jest zawsze instancją Latte\Compiler\Nodes\TemplateNode
.
Głównym celem przebiegu kompilacji jest zwykle jeden lub oba z poniższych:
- Analiza: Przechodzenie przez AST i zbieranie informacji o szablonie (np. znalezienie wszystkich zdefiniowanych bloków, sprawdzenie użycia specyficznych tagów, zapewnienie spełnienia określonych ograniczeń bezpieczeństwa).
- Modyfikacja: Zmiana struktury AST lub atrybutów węzłów (np. automatyczne dodawanie atrybutów HTML, optymalizacja określonych kombinacji tagów, zastępowanie przestarzałych tagów nowymi, implementacja zasad sandboxu).
Rejestracja
Przebiegi kompilacji są rejestrowane za pomocą metody rozszerzenia getPasses()
. Ta metoda zwraca
tablicę asocjacyjną, gdzie klucze są unikalnymi nazwami przebiegów (używanymi wewnętrznie i do sortowania), a wartości to
PHP callable implementujące logikę przebiegu.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... inne przebiegi ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// Implementacja...
}
}
Przebiegi zarejestrowane przez podstawowe rozszerzenia Latte i Twoje własne rozszerzenia są uruchamiane sekwencyjnie.
Kolejność może być ważna, szczególnie jeśli jeden przebieg zależy od wyników lub modyfikacji innego. Latte dostarcza
pomocniczy mechanizm kontroli tej kolejności, jeśli jest to potrzebne; zobacz dokumentację Extension::getPasses()
dla szczegółów.
Przykład AST
Dla lepszego wyobrażenia o AST, dodajemy przykład. To jest szablon źródłowy:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
nie znaleziono elementów
{/foreach}
A to jest jego reprezentacja w formie 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('nie znaleziono elementów') ) ) ) )
Przechodzenie przez AST za pomocą NodeTraverser
Ręczne pisanie rekurencyjnych funkcji do przechodzenia przez złożoną strukturę AST jest męczące i podatne na błędy. Latte dostarcza specjalne narzędzie do tego celu: Latte\Compiler\NodeTraverser. Ta klasa implementuje wzorzec projektowy Visitor, dzięki któremu przechodzenie przez AST jest systematyczne i łatwe do opanowania.
Podstawowe użycie obejmuje utworzenie instancji NodeTraverser
i wywołanie jej metody traverse()
,
przekazanie korzeniowego węzła AST i jednego lub dwóch “visitor” callable:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// 'enter' visitor: Wywoływany przy wejściu do węzła (przed jego dziećmi)
enter: function (Node $node) {
echo "Wejście do węzła typu: " . $node::class . "\n";
// Tutaj możesz badać węzeł
if ($node instanceof Nodes\TextNode) {
// echo "Znaleziono tekst: " . $node->content . "\n";
}
},
// 'leave' visitor: Wywoływany przy opuszczeniu węzła (po jego dzieciach)
leave: function (Node $node) {
echo "Opuszczenie węzła typu: " . $node::class . "\n";
// Tutaj możesz wykonywać akcje po przetworzeniu dzieci
},
);
Możesz dostarczyć tylko visitor enter
, tylko visitor leave
lub oba, w zależności od Twoich
potrzeb.
enter(Node $node)
: Ta funkcja jest wykonywana dla każdego węzła przed tym, jak przechodzący
odwiedzi którekolwiek z dzieci tego węzła. Jest przydatna do:
- Zbierania informacji podczas przechodzenia przez drzewo w dół.
- Podejmowania decyzji przed przetworzeniem dzieci (jak decyzja o ich pominięciu, zobacz Optymalizacja przechodzenia).
- Potencjalnej modyfikacji węzła przed odwiedzeniem dzieci (rzadziej).
leave(Node $node)
: Ta funkcja jest wykonywana dla każdego węzła po tym, jak wszystkie jego dzieci
(i ich całe poddrzewa) zostały w pełni odwiedzone (zarówno wejście, jak i opuszczenie). Jest to najczęstsze
miejsce dla:
Oba visitory enter
i leave
mogą opcjonalnie zwracać wartość w celu wpływu na proces
przechodzenia. Zwrócenie null
(lub niczego) kontynuuje przechodzenie normalnie, zwrócenie instancji
Node
zastępuje bieżący węzeł, a zwrócenie specjalnych stałych takich jak
NodeTraverser::RemoveNode
lub NodeTraverser::StopTraversal
modyfikuje przepływ, jak wyjaśniono w
kolejnych sekcjach.
Jak działa przechodzenie
NodeTraverser
wewnętrznie używa metody getIterator()
, którą musi implementować każda klasa
Node
(jak omówiono w Tworzenie własnych tagów). Iteruje
przez dzieci uzyskane za pomocą getIterator()
, rekurencyjnie wywołuje na nich traverse()
i zapewnia,
że visitory enter
i leave
są wywoływane w prawidłowej kolejności wgłąb (depth-first) dla każdego
węzła w drzewie dostępnego przez iteratory. To ponownie podkreśla, dlaczego prawidłowo zaimplementowany
getIterator()
w Twoich własnych węzłach tagów jest absolutnie niezbędny do prawidłowego działania przebiegów
kompilacji.
Napiszmy prosty przebieg, który liczy, ile razy w szablonie użyty jest tag {do}
(reprezentowany 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++;
}
},
// visitor 'leave' nie jest potrzebny do tego zadania
);
echo "Znaleziono tag {do} $count razy.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
W tym przykładzie potrzebowaliśmy tylko visitora enter
, aby sprawdzić typ każdego odwiedzanego węzła.
Następnie zbadamy, jak te visitory faktycznie modyfikują AST.
Modyfikacja AST
Jednym z głównych celów przebiegów kompilacji jest modyfikacja abstrakcyjnego drzewa składni. Umożliwia to potężne
transformacje, optymalizacje lub wymuszanie reguł bezpośrednio na strukturze szablonu przed generowaniem kodu PHP.
NodeTraverser
dostarcza kilka sposobów, jak to osiągnąć w ramach visitorów enter
i
leave
.
Ważna uwaga: Modyfikacja AST wymaga ostrożności. Nieprawidłowe zmiany – jak usunięcie podstawowych węzłów lub zastąpienie węzła niekompatybilnym typem – mogą prowadzić do błędów podczas generowania kodu lub spowodować nieoczekiwane zachowanie podczas działania programu. Zawsze dokładnie testuj swoje przebiegi modyfikacyjne.
Zmiana właściwości węzłów
Najprostszym sposobem modyfikacji drzewa jest bezpośrednia zmiana publicznych właściwości węzłów odwiedzanych podczas przechodzenia. Wszystkie węzły przechowują swoje sparsowane argumenty, zawartość lub atrybuty w publicznych właściwościach.
Przykład: Stwórzmy przebieg, który znajduje wszystkie statyczne węzły tekstowe (TextNode
,
reprezentujące zwykły HTML lub tekst poza tagami Latte) i konwertuje ich zawartość 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', ponieważ TextNode nie ma żadnych dzieci do przetworzenia
enter: function (Node $node) {
// Czy ten węzeł jest statycznym blokiem tekstowym?
if ($node instanceof TextNode) {
// Tak! Bezpośrednio modyfikujemy jego publiczną właściwość 'content'.
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// Nie ma potrzeby niczego zwracać; zmiana jest stosowana bezpośrednio.
},
);
}
W tym przykładzie visitor 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()
. To bezpośrednio zmienia zawartość statycznego tekstu przechowywanego w AST przed
generowaniem kodu PHP. Ponieważ modyfikujemy obiekt bezpośrednio, nie musimy niczego zwracać z visitora.
Efekt: Jeśli szablon zawierał <p>Witaj</p>{= $var }<span>Świecie</span>
, po tym
przebiegu AST będzie reprezentować coś w rodzaju:
<p>WITAJ</p>{= $var }<span>ŚWIECIE</span>
. To NIE WPŁYNIE 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 visitora enter
lub leave
. NodeTraverser
następnie
zastępuje oryginalny węzeł zwróconym w strukturze węzła nadrzędnego.
Przykład: Stwórzmy przebieg, który znajduje wszystkie użycia stałej PHP_VERSION
(reprezentowanej przez
ConstantFetchNode
) i zastępuje je bezpośrednio literałem ciągu znaków (StringNode
) zawierającym
rzeczywistą wersję PHP wykrytą podczas kompilacji. Jest 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,
// 'leave' jest często używany do zastępowania, zapewnia, że dzieci (jeśli istnieją)
// są przetwarzane najpierw, chociaż tutaj działałby również 'enter'.
leave: function (Node $node) {
// Czy ten węzeł jest dostępem do stałej i nazwa stałej to 'PHP_VERSION'?
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// Tworzymy nowy StringNode zawierający aktualną 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
// oryginalny ConstantFetchNode tym $newNode.
return $newNode;
}
// Jeśli nie zwrócimy Node, oryginalny $node jest zachowany.
},
);
}
Tutaj visitor leave
identyfikuje specyficzny ConstantFetchNode
dla PHP_VERSION
.
Następnie tworzy całkowicie nowy StringNode
zawierający wartość stałej PHP_VERSION
w czasie
kompilacji. Zwracając ten $newNode
, mówi traverserowi, aby zastąpił oryginalny
ConstantFetchNode
w AST.
Efekt: Jeśli szablon zawierał {= PHP_VERSION }
i kompilacja przebiega na PHP 8.2.1, AST po tym przebiegu
będzie efektywnie reprezentować {= '8.2.1' }
.
Wybór enter
vs. leave
do zastąpienia:
- Użyj
leave
, jeśli utworzenie nowego węzła zależy od wyników przetwarzania dzieci starego węzła, lub jeśli chcesz po prostu zapewnić, że dzieci zostały odwiedzone przed zastąpieniem (powszechna praktyka). - Użyj
enter
, jeśli chcesz zastąpić węzeł przed tym, jak jego dzieci zostaną w ogóle odwiedzone.
Usuwanie węzłów
Możesz całkowicie usunąć węzeł z AST, zwracając specjalną stałą NodeTraverser::RemoveNode
z visitora.
Przykład: Usuńmy wszystkie komentarze szablonu ({* ... *}
), które są reprezentowane przez
CommentNode
w AST generowanym przez rdzeń Latte (chociaż typowo przetwarzane wcześniej, to służy jako
przykład).
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\CommentNode;
function removeCommentNodes(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// 'enter' jest tutaj w porządku, ponieważ nie potrzebujemy informacji o dzieciach do usunięcia komentarza
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// Sygnalizujemy traverserowi, aby usunął ten węzeł z AST
return NodeTraverser::RemoveNode;
}
},
);
}
Ostrzeżenie: Używaj RemoveNode
ostrożnie. Usunięcie węzła, który zawiera podstawową zawartość lub
wpływa na strukturę (jak usunięcie węzła zawartości pętli), może prowadzić do uszkodzonych szablonów lub
nieprawidłowego wygenerowanego kodu. Najbezpieczniejsze jest dla węzłów, które są rzeczywiście opcjonalne lub samodzielne
(jak komentarze lub tagi debugowania) lub dla pustych węzłów strukturalnych (np. pusty FragmentNode
może być w
niektórych kontekstach bezpiecznie usunięty przez przebieg czyszczący).
Te trzy metody – modyfikacja właściwości, zastępowanie węzłów i usuwanie węzłów – stanowią podstawowe narzędzia do manipulacji AST w ramach Twoich przebiegów kompilacji.
Optymalizacja przechodzenia
AST szablonów może być dość duży, potencjalnie zawierając tysiące węzłów. Przechodzenie przez każdy pojedynczy
węzeł może być niepotrzebne i wpływać na wydajność kompilacji, jeśli Twój przebieg interesuje się tylko specyficznymi
częściami drzewa. NodeTraverser
oferuje sposoby optymalizacji przechodzenia:
Pomijanie dzieci
Jeśli wiesz, że gdy napotkasz określony typ węzła, żaden z jego potomków nie może zawierać węzłów, których
szukasz, możesz powiedzieć traverserowi, aby pominął odwiedzanie jego dzieci. Robi się to przez zwrócenie stałej
NodeTraverser::DontTraverseChildren
z visitora enter
. W ten sposób pomijasz całe gałęzie
podczas przechodzenia, co potencjalnie oszczędza znaczący czas, zwłaszcza w szablonach ze złożonymi wyrażeniami PHP
wewnątrz tagów.
Zatrzymanie przechodzenia
Jeśli Twój przebieg potrzebuje znaleźć tylko pierwsze wystąpienie czegoś (specyficzny typ węzła, spełnienie
warunku), możesz całkowicie zatrzymać cały proces przechodzenia, gdy tylko to znajdziesz. Osiąga się to przez zwrócenie
stałej NodeTraverser::StopTraversal
z visitora enter
lub leave
. Metoda
traverse()
przestanie odwiedzać jakiekolwiek dalsze węzły. Jest to bardzo efektywne, jeśli potrzebujesz tylko
pierwszego dopasowania w potencjalnie bardzo dużym drzewie.
Użyteczny pomocnik NodeHelpers
Podczas gdy NodeTraverser
oferuje precyzyjną kontrolę, Latte dostarcza również praktyczną klasę pomocniczą,
Latte\Compiler\NodeHelpers, która enkapsuluje
NodeTraverser
dla kilku typowych zadań wyszukiwania i analizy, często wymagając mniej kodu przygotowawczego.
find (Node $startNode, callable $filter): array
Ta metoda statyczna 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ź wszystkie węzły zmiennych (VariableNode
) w całym szablonie.
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
Podobne do find
, ale zatrzymuje przechodzenie natychmiast po znalezieniu pierwszego węzła, który
spełnia callback $filter
. Zwraca znaleziony obiekt Node
lub null
, jeśli nie znaleziono
żadnego pasującego węzła. Jest to w zasadzie praktyczne opakowanie wokół NodeTraverser::StopTraversal
.
Przykład: Znajdź węzeł {parameters}
(to samo co ręczny przykład wcześniej, ale krócej).
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;
function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
return NodeHelpers::findFirst(
$templateNode->head, // Szukaj tylko w głównej sekcji dla efektywności
fn($node) => $node instanceof ParametersNode,
);
}
toValue (ExpressionNode $node, bool $constants = false): mixed
Ta metoda statyczna próbuje wyewaluować ExpressionNode
w czasie kompilacji i zwrócić jego
odpowiadającą wartość PHP. Działa niezawodnie tylko dla prostych węzłów literalnych (StringNode
,
IntegerNode
, FloatNode
, BooleanNode
, NullNode
) oraz instancji
ArrayNode
zawierających tylko takie ewaluowalne elementy.
Jeśli $constants
jest ustawione na true
, będzie również próbować rozwiązać
ConstantFetchNode
i ClassConstantFetchNode
, sprawdzając defined()
i używając
constant()
.
Jeśli węzeł zawiera zmienne, wywołania funkcji lub inne dynamiczne elementy, nie może być wyewaluowany w czasie
kompilacji, a metoda rzuci InvalidArgumentException
.
Przypadek użycia: Uzyskanie statycznej wartości argumentu tagu podczas kompilacji do podejmowania decyzji 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 ciągu znaków
return null;
}
}
toText (?Node $node): ?string
Ta metoda statyczna jest przydatna do ekstrakcji zwykłego tekstu z prostych węzłów. Działa głównie z:
TextNode
: Zwraca jego$content
.FragmentNode
: Konkatenuje wyniktoText()
dla wszystkich jego dzieci. Jeśli któreś dziecko nie jest konwertowalne na tekst (np. zawieraPrintNode
), zwracanull
.NopNode
: Zwraca pusty ciąg znaków.- Inne typy węzłów: Zwraca
null
.
Przypadek użycia: Uzyskanie statycznej zawartości tekstowej wartości atrybutu HTML lub prostego elementu HTML do analizy podczas przebiegu kompilacji.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// $attr->value jest zazwyczaj AreaNode (jak FragmentNode lub TextNode)
return NodeHelpers::toText($attr->value);
}
// Przykład użycia w przebiegu:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = getStaticAttributeValue($node->getAttributeNode('name'));
// if ($nameAttrValue === 'description') { ... }
// }
NodeHelpers
może uprościć Twoje przebiegi kompilacji, dostarczając gotowych rozwiązań dla typowych zadań
przechodzenia i analizy AST.
Praktyczne przykłady
Zastosujmy koncepcje przechodzenia i modyfikacji AST do rozwiązania niektórych praktycznych problemów. Te przykłady demonstrują typowe wzorce używane w przebiegach kompilacji.
Automatyczne dodawanie loading="lazy"
do
<img>
Nowoczesne przeglądarki obsługują natywne leniwe ładowanie obrazów za pomocą atrybutu loading="lazy"
.
Stwórzmy przebieg, który automatycznie dodaje ten atrybut do wszystkich tagów <img>
, które jeszcze nie
mają 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', ponieważ modyfikujemy węzeł bezpośrednio
// i nie zależy to od dzieci dla tej decyzji.
enter: function (Node $node) {
// Czy to element HTML o nazwie 'img'?
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// Upewniamy się, że węzeł atrybutów istnieje
$node->attributes ??= new Nodes\FragmentNode;
// Sprawdzamy, czy już istnieje atrybut 'loading' (bez względu na wielkość 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;
}
}
// Dołączamy spację, jeśli atrybuty nie są puste
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: '"',
);
// Zmiana jest stosowana bezpośrednio w obiekcie, nie trzeba niczego zwracać.
}
},
);
}
Wyjaśnienie:
- Visitor
enter
szuka węzłówHtml\ElementNode
o nazwieimg
. - Iteruje przez istniejące atrybuty (
$node->attributes->children
) i sprawdza, czy atrybutloading
jest już obecny. - Jeśli nie jest znaleziony, tworzy nowy
Html\AttributeNode
reprezentującyloading="lazy"
.
Sprawdzanie wywołań funkcji
Przebiegi kompilacji są podstawą Latte Sandbox. Chociaż prawdziwy Sandbox jest zaawansowany, możemy zademonstrować podstawową zasadę sprawdzania zabronionych wywołań funkcji.
Cel: Zapobieganie użyciu potencjalnie niebezpiecznej funkcji shell_exec
wewnątrz wyrażeń szablonu.
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
$traverser = new NodeTraverser;
(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(
"Funkcja {$node->name}() nie jest dozwolona.",
$node->position,
);
}
},
);
}
Wyjaśnienie:
- Definiujemy listę zabronionych nazw funkcji.
- Visitor
enter
sprawdzaFunctionCallNode
. - Jeśli nazwa funkcji (
$node->name
) jest statycznymNameNode
, sprawdzamy jej reprezentację tekstową w małych literach w naszej zabronionej liście. - Jeśli zostanie znaleziona zabroniona funkcja, rzucamy
Latte\SecurityViolationException
, która jasno wskazuje naruszenie reguły bezpieczeństwa i zatrzymuje kompilację.
Te przykłady pokazują, jak przebiegi kompilacji z użyciem NodeTraverser
mogą być wykorzystane do analizy,
automatycznych modyfikacji i egzekwowania ograniczeń bezpieczeństwa poprzez interakcję bezpośrednio ze strukturą AST
szablonu.
Dobre praktyki
Podczas pisania przebiegów kompilacji pamiętaj o tych wytycznych, aby tworzyć solidne, łatwe w utrzymaniu i efektywne rozszerzenia:
- Kolejność ma znaczenie: Bądź świadomy kolejności, w jakiej przebiegają przebiegi. Jeśli Twój przebieg zależy
od struktury AST utworzonej przez inny przebieg (np. podstawowe przebiegi Latte lub inny własny przebieg), lub jeśli inne
przebiegi mogą zależeć od Twoich modyfikacji, użyj mechanizmu sortowania dostarczanego przez
Extension::getPasses()
do definiowania zależności (before
/after
). Zobacz dokumentacjęExtension::getPasses()
dla szczegółów. - Pojedyncza odpowiedzialność: Dąż do przebiegów, które wykonują jedno dobrze zdefiniowane zadanie. Dla złożonych transformacji rozważ podzielenie logiki na wiele przebiegów – być może jeden do analizy i drugi do modyfikacji opartej na wynikach analizy. To poprawia przejrzystość i testowalność.
- Wydajność: Pamiętaj, że przebiegi kompilacji wydłużają czas kompilacji szablonu (chociaż zazwyczaj dzieje się
to tylko raz, dopóki szablon się nie zmieni). Unikaj operacji wymagających dużych zasobów obliczeniowych w swoich
przebiegach, jeśli to możliwe. Wykorzystuj optymalizacje przechodzenia, takie jak
NodeTraverser::DontTraverseChildren
iNodeTraverser::StopTraversal
, gdy tylko wiesz, że nie musisz odwiedzać określonych części AST. - Używaj
NodeHelpers
: Dla typowych zadań, takich jak znajdowanie specyficznych węzłów lub statyczne ewaluowanie prostych wyrażeń, sprawdź, czyLatte\Compiler\NodeHelpers
nie oferuje odpowiedniej metody przed pisaniem własnej logikiNodeTraverser
. Może to zaoszczędzić czas i zmniejszyć ilość kodu przygotowawczego. - Obsługa błędów: Jeśli Twój przebieg wykryje błąd lub nieprawidłowy stan w AST szablonu, rzuć
Latte\CompileException
(lubLatte\SecurityViolationException
dla problemów bezpieczeństwa) z jasną wiadomością i odpowiednim obiektemPosition
(zazwyczaj$node->position
). To dostarcza użytecznej informacji zwrotnej programiście szablonu. - Idempotencja (jeśli to możliwe): Idealnie, uruchomienie Twojego przebiegu wielokrotnie na tym samym AST powinno dać ten sam wynik, co jego jednorazowe uruchomienie. Nie zawsze jest to wykonalne, ale upraszcza debugowanie i rozumienie interakcji przebiegów, jeśli jest to osiągnięte. Na przykład, upewnij się, że Twój przebieg modyfikacyjny sprawdza, czy modyfikacja została już zastosowana, zanim zastosuje ją ponownie.
Przestrzegając tych praktyk, możesz efektywnie wykorzystać przebiegi kompilacji do rozszerzenia możliwości Latte w sposób potężny i niezawodny, przyczyniając się do bezpieczniejszego, bardziej zoptymalizowanego lub bogatszego funkcjonalnie przetwarzania szablonów.