Nette Documentation Preview

syntax
Tworzenie compiler passów
*************************

.[perex]
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 |custom-tags#Zrozumieć proces kompilacji]. 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 |extending-latte#getPasses()]. 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.

```php
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()` |extending-latte#getPasses()].


Przykład AST
============

Aby lepiej wyobrazić sobie AST, dodajemy próbkę. Oto szablon źródłowy:

```latte
{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}
```

A oto jego reprezentacja w postaci AST:

/--pre
Latte\Compiler\Nodes\<b>TemplateNode</b>(
   Latte\Compiler\Nodes\<b>FragmentNode</b>(
      - Latte\Essential\Nodes\<b>ForeachNode</b>(
           expression: Latte\Compiler\Nodes\Php\Expression\<b>MethodCallNode</b>(
              object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$category')
              name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('getItems')
           )
           value: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item')
           content: Latte\Compiler\Nodes\<b>FragmentNode</b>(
              - Latte\Compiler\Nodes\<b>TextNode</b>('  ')
              - Latte\Compiler\Nodes\<b>Html\ElementNode</b>('li')(
                   content: Latte\Compiler\Nodes\<b>PrintNode</b>(
                      expression: Latte\Compiler\Nodes\Php\Expression\<b>PropertyFetchNode</b>(
                         object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item')
                         name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('name')
                      )
                      modifier: Latte\Compiler\Nodes\Php\<b>ModifierNode</b>(
                         filters:
                            - Latte\Compiler\Nodes\Php\<b>FilterNode</b>('upper')
                      )
                   )
                )
            )
            else: Latte\Compiler\Nodes\<b>FragmentNode</b>(
               - Latte\Compiler\Nodes\<b>TextNode</b>('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: [api:Latte\Compiler\NodeTraverser]. Klasa ta implementuje [wzorzec projektowy Visitor |https://pl.wikipedia.org/wiki/Odwiedzaj%C4%85cy], 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:

```php
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 |custom-tags#Implementacja getIterator() dla podwęzłó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`).

```php
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*.

```php
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.

```php
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ąć.

```php
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ą [api: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 .[method]
--------------------------------------------------------

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`).

```php
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  .[method]
--------------------------------------------------------------

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}`.

```php
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 .[method]
------------------------------------------------

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.

```php
use Latte\Compiler\NodeHelpers;

$copy = NodeHelpers::clone($node);
```


toValue(ExpressionNode $node, bool $constants = false): mixed .[method]
-----------------------------------------------------------------------

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.

```php
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 .[method]
--------------------------------------

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 wynik `toText()` dla wszystkich swoich potomków. Jeśli któryś potomek nie da się przekształcić w tekst (np. zawiera `PrintNode`), zwraca `null`.
- `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.

```php
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`.

```php
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 `enter` szuka węzłów `Html\ElementNode` o nazwie `img`.
- Przechodzi po istniejących atrybutach (`$node->attributes->children`), aby sprawdzić, czy atrybut `loading` już tam jest.
- Jeśli go nie znajdzie, tworzy nowy `Html\AttributeNode` reprezentujący `loading="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`.

```php
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 `enter` sprawdza `FunctionCallNode`.
- Jeśli nazwa funkcji (`$node->name`) jest statycznym `NameNode`, 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 dokumentacji [`Extension::getPasses()` |extending-latte#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::DontTraverseChildren` i `NodeTraverser::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ź, czy `Latte\Compiler\NodeHelpers` nie oferuje odpowiedniej metody, zanim napiszesz własną logikę na `NodeTraverser`. 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` (albo `Latte\SecurityViolationException` przy problemach bezpieczeństwa) z jasnym komunikatem i odpowiednim obiektem `Position` (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.

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 wynik toText() dla wszystkich swoich potomków. Jeśli któryś potomek nie da się przekształcić w tekst (np. zawiera PrintNode), zwraca null.
  • 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 enter szuka węzłów Html\ElementNode o nazwie img.
  • Przechodzi po istniejących atrybutach ($node->attributes->children), aby sprawdzić, czy atrybut loading już tam jest.
  • Jeśli go nie znajdzie, tworzy nowy Html\AttributeNode reprezentujący loading="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 enter sprawdza FunctionCallNode.
  • Jeśli nazwa funkcji ($node->name) jest statycznym NameNode, 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 dokumentacji Extension::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::DontTraverseChildren i NodeTraverser::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ź, czy Latte\Compiler\NodeHelpers nie oferuje odpowiedniej metody, zanim napiszesz własną logikę na NodeTraverser. 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 (albo Latte\SecurityViolationException przy problemach bezpieczeństwa) z jasnym komunikatem i odpowiednim obiektem Position (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.