Tworzenie własnych tagów

Ta strona to wyczerpujący przewodnik po tworzeniu własnych tagów w Latte. Omówimy wszystko, od prostych tagów po bardziej złożone scenariusze z zagnieżdżoną treścią i szczególnymi potrzebami parsowania, opierając się na Twoim zrozumieniu tego, jak Latte kompiluje szablony.

Własne tagi dają najwyższy poziom kontroli nad składnią szablonu i logiką renderowania, ale są też najbardziej złożonym punktem rozszerzenia. Zanim zdecydujesz się utworzyć własny tag, zawsze zastanów się, czy nie istnieje prostsze rozwiązanie albo czy odpowiedni tag nie znajduje się już w standardowym zestawie. Własnych tagów używaj tylko wtedy, gdy prostsze alternatywy nie wystarczają.

Zrozumieć proces kompilacji

Aby skutecznie tworzyć własne tagi, warto wyjaśnić, jak Latte przetwarza szablony. Zrozumienie tego procesu wyjaśnia, dlaczego tagi mają taką, a nie inną budowę i jak wpasowują się w całość.

Kompilacja szablonu w Latte obejmuje, w uproszczeniu, następujące kluczowe kroki:

  1. Lexing: Lexer czyta kod źródłowy szablonu (plik .latte) i rozkłada go na sekwencję niewielkich, wyraźnie oddzielonych kawałków zwanych tokenami (np. {, foreach, $variable, }, tekst HTML itd.).
  2. Parsowanie: Parser bierze ten strumień tokenów i buduje sensowną strukturę drzewiastą reprezentującą logikę i treść szablonu. To drzewo nazywa się drzewem składniowym (AST).
  3. Compiler passy: Przed wygenerowaniem kodu PHP Latte uruchamia compiler passy. To funkcje, które przechodzą przez całe AST i mogą je modyfikować albo zbierać informacje. Ten krok ma kluczowe znaczenie dla funkcji takich jak bezpieczeństwo (Sandbox) czy optymalizacje.
  4. Generowanie kodu: Na koniec kompilator przechodzi przez (być może zmodyfikowane) AST i generuje odpowiadający mu kod klasy PHP. To właśnie ten kod PHP renderuje szablon przy wykonaniu.
  5. Cache: Wygenerowany kod PHP jest zapisywany w cache na dysku, dzięki czemu kolejne renderowania są bardzo szybkie, bo kroki 1–4 są pomijane.

W rzeczywistości kompilacja jest trochę bardziej skomplikowana. Latte ma dwa lexery i parsery: jeden dla szablonu HTML i jeden dla kodu przypominającego PHP wewnątrz tagów. Ponadto parsowanie nie następuje po tokenizacji, lecz lexer i parser działają równolegle w dwóch “wątkach” i koordynują się. Uwierz mi, Davidowi Grudlowi: programowanie tego było jak rocket science :-)

Cały proces, od wczytania treści szablonu przez parsowanie po wygenerowanie pliku wynikowego, można rozpisać na kroki tym kodem, którym możesz poeksperymentować i zrzucić wyniki pośrednie:

$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);

Anatomia tagu

Utworzenie w pełni funkcjonalnego własnego tagu w Latte obejmuje kilka powiązanych ze sobą części. Zanim przejdziemy do implementacji, zrozumiejmy kluczowe pojęcia i terminologię, posługując się analogią do HTML i Document Object Model (DOM).

Tagi a węzły (analogia z HTML)

W HTML piszemy tagi, takie jak <p> czy <div>...</div>. Te tagi to składnia w kodzie źródłowym. Gdy przeglądarka parsuje taki HTML, tworzy w pamięci reprezentację zwaną Document Object Model (DOM). W DOM tagi HTML reprezentowane są przez węzły (konkretnie węzły Element w terminologii DOM JavaScriptu). Z tymi węzłami pracujemy programowo (np. document.getElementById(...) w JavaScripcie zwraca węzeł Element). Tag to tylko tekstowa reprezentacja w pliku źródłowym, a węzeł to reprezentacja obiektowa w drzewie logicznym.

Latte działa podobnie:

  • W pliku szablonu .latte piszesz tagi Latte, jak {foreach ...} i {/foreach}. To składnia, z którą pracujesz jako autor szablonu.
  • Gdy Latte parsuje szablon, buduje drzewo składniowe (AST). Drzewo to składa się z węzłów. Każdy tag Latte, element HTML, fragment tekstu albo wyrażenie w szablonie staje się w tym drzewie jednym lub kilkoma węzłami.
  • Klasą bazową dla wszystkich węzłów w AST jest Latte\Compiler\Node. Tak jak DOM ma różne typy węzłów (Element, Text, Comment), AST Latte ma rozmaite typy węzłów. Napotkasz Latte\Compiler\Nodes\TextNode dla tekstu statycznego, Latte\Compiler\Nodes\Html\ElementNode dla elementów HTML, Latte\Compiler\Nodes\Php\ExpressionNode dla wyrażeń wewnątrz tagów, a co kluczowe przy własnych tagach, węzły dziedziczące po Latte\Compiler\Nodes\StatementNode.

Dlaczego StatementNode?

Elementy HTML (Html\ElementNode) reprezentują przede wszystkim strukturę i treść. Wyrażenia PHP (Php\ExpressionNode) reprezentują wartości albo obliczenia. A co z tagami Latte, takimi jak {if}, {foreach} czy nasz własny {datetime}? Te tagi wykonują działania, sterują przebiegiem programu albo generują wynik na podstawie logiki. To jednostki funkcjonalne, które czynią z Latte potężny silnik szablonów, a nie tylko język znaczników.

W programowaniu takie jednostki wykonujące działania nazywa się często “instrukcjami” (statements). Dlatego węzły reprezentujące te funkcjonalne tagi Latte dziedziczą zwykle po Latte\Compiler\Nodes\StatementNode. Odróżnia je to od węzłów czysto strukturalnych (jak elementy HTML) i węzłów reprezentujących wartości (jak wyrażenia).

Kluczowe składniki

Wróćmy do głównych składników potrzebnych do utworzenia własnego tagu:

Funkcja parsująca tag

  • Ten callable PHP parsuje składnię tagu Latte ({...}) w źródle szablonu.
  • Informacje o tagu (jak jego nazwa, pozycja i to, czy jest n:atrybutem) otrzymuje przez obiekt Latte\Compiler\Tag, a jako drugi argument główny Latte\Compiler\TemplateParser. Jego pełna sygnatura to callable(Tag, TemplateParser): (Node|\Generator|void).
  • Głównym narzędziem do parsowania argumentów i wyrażeń wewnątrz ograniczników tagu jest obiekt Latte\Compiler\TagParser, dostępny przez $tag->parser (to inny parser niż ten parsujący cały szablon).
  • Dla tagów parzystych używa yield, aby zasygnalizować Latte, że ma sparsować treść wewnętrzną między tagiem otwierającym a zamykającym.
  • Ostatecznym celem funkcji parsującej jest utworzenie i zwrócenie instancji klasy węzła, która trafia do AST.
  • Zwyczajowo (choć nie jest to wymagane) funkcję parsującą implementuje się jako metodę statyczną (często nazwaną create) bezpośrednio w odpowiadającej klasie węzła. Trzyma to logikę parsowania i reprezentację węzła zgrabnie razem, w razie potrzeby pozwala sięgać po prywatne i chronione elementy klasy oraz poprawia organizację.

Klasa węzła

  • Reprezentuje funkcję logiczną Twojego tagu w drzewie składniowym (AST).
  • Przechowuje sparsowane informacje (jak argumenty czy treść) we właściwościach publicznych. Właściwości te często zawierają inne instancje Node (np. ExpressionNode dla sparsowanych argumentów, AreaNode dla sparsowanej treści).
  • Metoda print(PrintContext $context): string generuje kod PHP (instrukcję albo ich serię), który wykonuje działanie tagu podczas renderowania szablonu.
  • Metoda getIterator(): \Generator udostępnia węzły potomne do przechodzenia przez compiler passy. Musi zwracać referencje (&), aby passy mogły ewentualnie modyfikować albo zastępować podwęzły.
  • Po sparsowaniu całego szablonu do AST Latte uruchamia serię compiler passów. Passy te przechodzą przez całe AST, używając metody getIterator() udostępnianej przez każdy węzeł. Mogą badać węzły, zbierać informacje, a nawet modyfikować drzewo (np. zmieniając publiczne właściwości węzłów albo zastępując węzły w całości). Ten projekt, wymagający kompletnej metody getIterator(), ma kluczowe znaczenie. Pozwala potężnym funkcjom, jak Sandbox, analizować i ewentualnie zmieniać zachowanie dowolnej części szablonu, w tym Twoich własnych tagów, zapewniając bezpieczeństwo i spójność.

Rejestracja przez rozszerzenie

  • Musisz powiedzieć Latte o swoim nowym tagu i o tym, której funkcji parsującej ma dla niego użyć. Dzieje się to w rozszerzeniu Latte.
  • W swojej klasie rozszerzenia implementujesz metodę getTags(): array. Metoda ta zwraca tablicę asocjacyjną, w której kluczami są nazwy tagów (np. 'mytag', 'n:myattribute'), a wartościami callable PHP reprezentujące odpowiadające im funkcje parsujące (np. MyNamespace\DatetimeNode::create(...)).

Podsumowując: funkcja parsująca tag zamienia kod źródłowy szablonu Twojego tagu w węzeł AST. Klasa węzła wie następnie, jak zamienić samą siebie w wykonywalny kod PHP skompilowanego szablonu, i udostępnia swoje podwęzły compiler passom przez getIterator(). Rejestracja przez rozszerzenie łączy nazwę tagu z funkcją parsującą i daje o niej znać Latte.

Przyjrzymy się teraz krok po kroku, jak zaimplementować te składniki.

Tworzenie prostego tagu

Zabierzmy się za tworzenie Twojego pierwszego własnego tagu Latte. Zaczniemy od bardzo prostego przykładu: tagu o nazwie {datetime}, który wypisuje bieżącą datę i czas. Na początku tag nie będzie przyjmował żadnych argumentów, ale rozbudujemy go później w sekcji Parsowanie argumentów tagu. Nie ma też treści wewnętrznej.

Ten przykład przeprowadzi Cię przez niezbędne kroki: zdefiniowanie klasy węzła, implementację jej metod print() i getIterator(), utworzenie funkcji parsującej i wreszcie rejestrację tagu.

Cel: Zaimplementować {datetime}, aby wypisywał bieżącą datę i czas za pomocą funkcji PHP date().

Utworzenie klasy węzła

Najpierw potrzebujemy klasy reprezentującej nasz tag w drzewie składniowym (AST). Jak omówiono wyżej, dziedziczymy po Latte\Compiler\Nodes\StatementNode.

Utwórz plik (np. DatetimeNode.php) i zdefiniuj klasę:

<?php

namespace App\Templating;

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DatetimeNode extends StatementNode
{
	/**
	 * Funkcja parsująca tag, wywoływana po napotkaniu {datetime}.
	 */
	public static function create(Tag $tag): self
	{
		// nasz tag wypisuje treść, więc zachowujemy otaczające wcięcie
		$tag->outputMode = $tag::OutputKeepIndentation;
		// nasz prosty tag na razie nie przyjmuje argumentów, więc nie musimy niczego parsować
		$node = $tag->node = new self;
		return $node;
	}

	/**
	 * Generuje kod PHP, który wykona się przy renderowaniu szablonu.
	 */
	public function print(PrintContext $context): string
	{
		return $context->format(
			'echo date(\'Y-m-d H:i:s\') %line;',
			$this->position,
		);
	}

	/**
	 * Udostępnia węzły potomne compiler passom Latte.
	 */
	public function &getIterator(): \Generator
	{
		false && yield;
	}
}

Gdy Latte napotka w szablonie {datetime}, wywoła funkcję parsującą create(). Jej zadaniem jest zwrócenie instancji DatetimeNode. Ustawiamy też $tag->outputMode na OutputKeepIndentation; ponieważ tag działa w domyślnym trybie OutputNone (wyjaśnionym w Tryby wyniku tagu), tag umieszczony przed pierwszym tekstem szablonu mógłby inaczej wypisać swój wynik w wygenerowanej metodzie prepare() zamiast main(). Ustawienie tego trybu sprawia, że wynik trafia tam, gdzie stoi tag.

Metoda print() generuje kod PHP, który wykona się przy renderowaniu szablonu. Wywołujemy metodę $context->format(), która składa wynikowy łańcuch kodu PHP dla skompilowanego szablonu. Pierwszy argument, 'echo date('Y-m-d H:i:s') %line;', to maska, w którą podstawiane są kolejne parametry. Symbol zastępczy %line mówi metodzie format(), aby wzięła następny argument, czyli $this->position, i wstawiła komentarz w rodzaju /* pos 15:1 */, który łączy wygenerowany kod PHP z pierwotnym wierszem szablonu, co ma kluczowe znaczenie przy debugowaniu.

Właściwość $this->position jest dziedziczona z klasy bazowej Node i ustawiana automatycznie przez parser Latte. Zawiera obiekt Latte\Compiler\Range (podklasę Position rozszerzoną o length w bajtach), wskazujący, gdzie tag znajduje się w źródłowym pliku .latte. Dla tagów parzystych zakres rozciąga się od tagu otwierającego do zamykającego, a potomkowie StatementNode udostępniają dodatkowo $this->tagRanges z listą Range każdego tagu składowego (otwierającego, pośredniego jak {else}/{case} oraz zamykającego).

Metoda getIterator() ma zasadnicze znaczenie dla compiler passów. Musi zwracać wszystkie węzły potomne, ale nasz prosty DatetimeNode nie ma na razie ani argumentów, ani treści, a więc żadnych węzłów potomnych. Metoda musi jednak istnieć i być generatorem, czyli słowo kluczowe yield musi jakoś pojawić się w jej ciele.

Rejestracja przez rozszerzenie

Na koniec daj Latte znać o nowym tagu. Utwórz klasę rozszerzenia (np. MyLatteExtension.php) i zarejestruj tag w jej metodzie getTags().

<?php

namespace App\Templating;

use Latte\Extension;

class MyLatteExtension extends Extension
{
	/**
	 * Zwraca listę tagów dostarczanych przez to rozszerzenie.
	 * @return array<string, callable> Mapa: 'tag-name' => parsing-function
	 */
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			// tutaj później zarejestrujemy kolejne tagi
		];
	}
}

Następnie zarejestruj to rozszerzenie w silniku Latte:

$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);

Utwórz szablon:

<p>Strona wygenerowana: {datetime}</p>

Oczekiwany wynik: <p>Strona wygenerowana: 2023-10-27 11:00:00</p>

Podsumowanie tego etapu

Udało nam się utworzyć podstawowy własny tag {datetime}. Zdefiniowaliśmy jego reprezentację w AST (DatetimeNode), obsłużyliśmy jego parsowanie (create()), określiliśmy, jak ma generować kod PHP (print()), zapewniliśmy możliwość przejścia po jego potomkach (getIterator()) i zarejestrowaliśmy go w Latte.

W kolejnej sekcji rozbudujemy ten tag tak, aby przyjmował argumenty, pokazując, jak parsować wyrażenia i zarządzać węzłami potomnymi.

Parsowanie argumentów tagu

Nasz prosty tag {datetime} działa, ale nie jest zbyt elastyczny. Rozbudujmy go tak, aby przyjmował opcjonalny argument: łańcuch formatu dla funkcji date(). Pożądana składnia to {datetime $format}.

Cel: Zmodyfikować {datetime} tak, aby przyjmował opcjonalne wyrażenie PHP jako argument, który zostanie użyty jako łańcuch formatu dla date().

Poznaj TagParser

Zanim zmodyfikujemy kod, warto zrozumieć narzędzie, którego użyjemy: Latte\Compiler\TagParser. Gdy główny parser Latte (TemplateParser) napotka tag Latte, jak {datetime ...}, albo n:atrybut, deleguje parsowanie treści wewnątrz tagu (części między { a } albo wartości atrybutu) wyspecjalizowanemu TagParser.

Ten TagParser operuje wyłącznie na argumentach tagu. Jego zadaniem jest skonsumować tokeny reprezentujące te argumenty. Co kluczowe, musi sparsować całą przekazaną mu treść. Jeśli Twoja funkcja parsująca zakończy pracę, a TagParser nie dotrze do końca argumentów (sprawdzane przez $tag->parser->isEnd()), Latte zgłosi wyjątek, bo oznacza to, że wewnątrz tagu zostały nieoczekiwane tokeny. Odwrotnie, jeśli tag wymaga argumentów, powinieneś na początku swojej funkcji parsującej wywołać $tag->expectArguments(). Metoda ta sprawdza, czy argumenty są obecne, i zgłasza pomocny wyjątek, gdy tag został użyty bez nich.

TagParser oferuje przydatne metody do parsowania różnych rodzajów argumentów:

  • parseExpression(): ExpressionNode: Parsuje wyrażenie w stylu PHP (zmienne, literały, operatory, wywołania funkcji i metod itd.). Obsługuje lukier składniowy Latte, na przykład traktowanie prostych łańcuchów alfanumerycznych jak łańcuchów w cudzysłowach (np. foo jest parsowane tak, jakby było 'foo').
  • parseUnquotedStringOrExpression(): ExpressionNode: Parsuje albo standardowe wyrażenie, albo łańcuch bez cudzysłowów. Łańcuchy bez cudzysłowów to sekwencje dozwolone w Latte bez cudzysłowów, używane często na przykład do ścieżek plików (np. {include ../file.latte}). Jeśli sparsuje łańcuch bez cudzysłowów, zwraca StringNode.
  • parseArguments(): ArrayNode: Parsuje argumenty oddzielone przecinkami, ewentualnie z kluczami, jak 10, name: 'John', true.
  • parseModifier(): ModifierNode: Parsuje filtry, jak |upper|truncate:10.
  • parseType(): ?SuperiorTypeNode: Parsuje deklaracje typów PHP, jak int, ?string, array|Foo.

Przy bardziej złożonych albo niskopoziomowych potrzebach parsowania możesz pracować bezpośrednio ze strumieniem tokenów przez $tag->parser->stream. Obiekt ten udostępnia metody do badania i konsumowania pojedynczych tokenów:

  • $tag->parser->stream->is(...): bool: Sprawdza, czy bieżący token odpowiada któremuś z podanych typów (np. Token::Php_Variable) albo wartości literalnych (np. 'as'), nie konsumując go. Przydatne do patrzenia w przód.
  • $tag->parser->stream->consume(...): Token: Konsumuje bieżący token i przesuwa pozycję strumienia do przodu. Jeśli jako argumenty podano oczekiwane typy lub wartości tokenów, a bieżący token nie pasuje, zgłasza CompileException. Użyj tego, gdy oczekujesz konkretnego tokenu.
  • $tag->parser->stream->tryConsume(...): ?Token: Próbuje skonsumować bieżący token tylko wtedy, gdy odpowiada on jednemu z podanych typów lub wartości. Jeśli pasuje, konsumuje token i go zwraca. Jeśli nie pasuje, pozostawia pozycję strumienia bez zmian i zwraca null. Użyj tego przy tokenach opcjonalnych albo przy wyborze między różnymi wariantami składni.

Aktualizacja funkcji parsującej create()

Z tą wiedzą zmodyfikujmy metodę create() w DatetimeNode tak, aby parsowała opcjonalny argument formatu przy użyciu $tag->parser.

<?php

namespace App\Templating;

use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DatetimeNode extends StatementNode
{
	// dodajemy właściwość publiczną na sparsowany węzeł wyrażenia formatu
	public ?ExpressionNode $format = null;

	public static function create(Tag $tag): self
	{
		$node = $tag->node = new self;

		// sprawdzamy, czy są jakieś tokeny
		if (!$tag->parser->isEnd()) {
			// parsujemy argument jako wyrażenie w stylu PHP przy użyciu TagParsera
			$node->format = $tag->parser->parseExpression();
		}

		return $node;
	}

	// ... metody print() i getIterator() zaktualizujemy za chwilę ...
}

Dodaliśmy właściwość publiczną $format. W create() używamy teraz $tag->parser->isEnd(), aby sprawdzić, czy argumenty . Jeśli tak, $tag->parser->parseExpression() konsumuje tokeny wyrażenia. Ponieważ TagParser musi skonsumować wszystkie swoje tokeny wejściowe, Latte automatycznie zgłosi błąd, jeśli użytkownik napisze coś nieoczekiwanego po wyrażeniu formatu (np. {datetime 'Y-m-d', unexpected}).

Aktualizacja metody print()

Zmodyfikujmy teraz metodę print() tak, aby używała sparsowanego wyrażenia formatu zapisanego w $this->format. Jeśli formatu nie podano ($this->format jest null), powinniśmy użyć domyślnego łańcucha formatu, na przykład 'Y-m-d H:i:s'.

	public function print(PrintContext $context): string
	{
		$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');

		// %node wypisuje reprezentację kodu PHP węzła $formatNode
		return $context->format(
			'echo date(%node) %line;',
			$formatNode,
			$this->position
		);
	}

Do zmiennej $formatNode zapisujemy węzeł AST reprezentujący łańcuch formatu dla funkcji PHP date(). Używamy tu operatora łączenia z null (??). Jeśli użytkownik podał w szablonie argument (np. {datetime 'd.m.Y'}), właściwość $this->format zawiera odpowiedni węzeł (w tym przypadku StringNode o wartości 'd.m.Y') i to on zostanie użyty. Jeśli użytkownik argumentu nie podał (napisał samo {datetime}), właściwość $this->format jest null, a my tworzymy zamiast tego nowy StringNode z domyślnym formatem 'Y-m-d H:i:s'. Zapewnia to, że $formatNode zawsze zawiera poprawny węzeł AST formatu.

W masce 'echo date(%node) %line;' użyty jest nowy symbol zastępczy %node, który mówi metodzie format(), aby wzięła pierwszy kolejny argument (czyli nasz $formatNode), wywołała jego metodę print() (zwracającą reprezentację jego kodu PHP) i wstawiła wynik w miejsce symbolu.

Implementacja getIterator() dla podwęzłów

Nasz DatetimeNode ma teraz węzeł potomny: wyrażenie $format. Musimy udostępnić ten węzeł compiler passom, zwracając go w metodzie getIterator(). Pamiętaj, aby zwracać referencję (&), co pozwoli passom ewentualnie zastąpić węzeł.

	public function &getIterator(): \Generator
	{
		if ($this->format) {
			yield $this->format;
		}
	}

Dlaczego to takie ważne? Wyobraź sobie pass Sandboxa, który musi sprawdzić, czy argument $format nie zawiera zabronionego wywołania funkcji (np. {datetime dangerousFunction()}). Gdyby getIterator() nie zwracał $this->format, pass Sandboxa nigdy nie zobaczyłby wywołania dangerousFunction() wewnątrz argumentu naszego tagu, tworząc potencjalną lukę bezpieczeństwa. Zwracając go, pozwalamy Sandboxowi (i innym passom) zbadać, a w razie potrzeby zmodyfikować węzeł wyrażenia $format.

Użycie rozbudowanego tagu

Tag poprawnie obsługuje teraz opcjonalny argument:

Default format: {datetime}
Custom format: {datetime 'd.m.Y'}
Using variable: {datetime $userDateFormatPreference}

{* to spowodowałoby błąd po sparsowaniu 'd.m.Y', bo ", foo" jest nieoczekiwane *}
{* {datetime 'd.m.Y', foo} *}

Następnie przyjrzymy się tworzeniu tagów parzystych, które przetwarzają treść między sobą.

Obsługa tagów parzystych

Jak dotąd nasz tag {datetime} jest (koncepcyjnie) samozamykający. Nie ma żadnej treści między tagiem otwierającym a zamykającym. Wiele przydatnych tagów działa jednak na bloku treści szablonu. Nazywamy je tagami parzystymi. Przykładami są {if}...{/if}, {block}...{/block} albo własny tag, który teraz zbudujemy: {debug}...{/debug}.

Ten tag pozwoli nam umieszczać w szablonach informacje debugowe, które mają być widoczne tylko podczas tworzenia aplikacji.

Cel: Utworzyć tag parzysty {debug}, którego treść renderuje się tylko wtedy, gdy aktywna jest konkretna flaga “trybu deweloperskiego”.

Poznaj providery

Czasem Twoje tagi potrzebują dostępu do danych albo usług, które nie są przekazywane bezpośrednio jako parametry szablonu. Na przykład ustalenia, czy aplikacja działa w trybie deweloperskim, dostępu do obiektu użytkownika albo pobrania wartości konfiguracyjnych. Latte udostępnia do tego mechanizm zwany providerami.

Providery rejestruje się w Twoim rozszerzeniu metodą getProviders(). Metoda ta zwraca tablicę asocjacyjną, w której kluczami są nazwy, pod którymi providery będą dostępne w kodzie uruchomieniowym szablonu, a wartościami rzeczywiste dane albo obiekty.

Wewnątrz kodu PHP wygenerowanego przez metodę print() Twojego tagu możesz sięgnąć po te providery przez specjalną właściwość obiektu $this->global. Ponieważ właściwość ta jest współdzielona przez wszystkie rozszerzenia, dobrą praktyką jest poprzedzanie nazw providerów prefiksem, aby uniknąć ewentualnych kolizji nazw z rdzennymi providerami Latte albo providerami innych rozszerzeń. Powszechną konwencją jest krótki, unikalny prefiks związany z Twoim dostawcą albo nazwą rozszerzenia. W naszym przykładzie użyjmy prefiksu app, a flaga trybu deweloperskiego będzie dostępna jako $this->global->appDevMode.

Słowo kluczowe yield do parsowania treści

Jak powiedzieć parserowi Latte, aby przetworzył treść między {debug} a {/debug}? Tu do gry wchodzi słowo kluczowe yield.

Gdy yield zostanie użyte w funkcji create(), funkcja staje się generatorem PHP. Jej wykonanie zostaje wstrzymane, a sterowanie wraca do głównego TemplateParser. TemplateParser parsuje wtedy dalej treść szablonu, dopóki nie napotka odpowiadającego tagu zamykającego (w naszym przypadku {/debug}).

Po znalezieniu tagu zamykającego TemplateParser wznawia wykonanie naszej funkcji create() tuż za instrukcją yield. Wartością zwróconą przez yield jest tablica zawierająca dwa elementy:

  1. AreaNode reprezentujący sparsowaną treść między tagiem otwierającym a zamykającym.
  2. Obiekt Tag reprezentujący tag zamykający (np. {/debug}).

Utwórzmy klasę DebugNode i jej metodę create z użyciem yield.

<?php

namespace App\Templating;

use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DebugNode extends StatementNode
{
	// właściwość publiczna na sparsowaną treść wewnętrzną
	public AreaNode $content;

	/**
	 * Funkcja parsująca dla tagu parzystego {debug} ... {/debug}.
	 */
	public static function create(Tag $tag): \Generator // zwróć uwagę na typ zwracany
	{
		$node = $tag->node = new self;

		// wstrzymujemy parsowanie, po napotkaniu {/debug} dostajemy treść wewnętrzną i tag zamykający
		[$node->content, $endTag] = yield;

		return $node;
	}

	// ... print() i getIterator() zaimplementujemy za chwilę ...
}

Uwaga: $endTag jest null, jeśli tag został użyty jako n:atrybut, czyli <div n:debug>...</div>.

Tag parzysty można też zamknąć ukośnikiem, jak {debug/} (albo <div n:debug/>). Nie ma wtedy treści wewnętrznej: generator otrzymuje [$emptyFragmentNode, $startTag], gdzie drugim elementem jest sam tag otwierający, a nie null.

Implementacja print() dla renderowania warunkowego

Metoda print() musi teraz wygenerować kod PHP, który w czasie działania sprawdzi provider appDevMode i wykona kod treści wewnętrznej tylko wtedy, gdy flaga jest prawdziwa.

	public function print(PrintContext $context): string
	{
		// generujemy instrukcję 'if' PHP sprawdzającą provider w czasie działania
		return $context->format(
			<<<'XX'
				if ($this->global->appDevMode) %line {
					// w trybie dev wypisujemy treść wewnętrzną
					%node
				}

				XX,
			$this->position, // dla komentarza %line
			$this->content,  // węzeł zawierający AST treści wewnętrznej
		);
	}

To proste. Używamy PrintContext::format(), aby utworzyć standardową instrukcję if PHP. Wewnątrz if umieszczamy symbol zastępczy %node dla $this->content. Latte rekurencyjnie wywoła $this->content->print($context), aby wygenerować kod PHP wnętrza tagu, ale tylko wtedy, gdy $this->global->appDevMode w czasie działania okaże się prawdziwe.

Implementacja getIterator() dla treści

Tak jak przy węźle argumentu w poprzednim przykładzie, nasz DebugNode ma teraz węzeł potomny: AreaNode $content. Musimy udostępnić go do przechodzenia, zwracając go w getIterator():

	public function &getIterator(): \Generator
	{
		// zwracamy referencję do węzła treści
		yield $this->content;
	}

Pozwala to compiler passom zejść do treści naszego tagu {debug}, co jest ważne nawet wtedy, gdy treść renderuje się warunkowo. Na przykład Sandbox musi przeanalizować treść niezależnie od tego, czy appDevMode jest prawdziwe, czy fałszywe.

Rejestracja i użycie

Zarejestruj tag i provider w swoim rozszerzeniu:

class MyLatteExtension extends Extension
{
	// zakładamy, że $isDevelopmentMode jest gdzieś ustalane (np. z konfiguracji)
	public function __construct(
		private bool $isDevelopmentMode,
	) {
	}

	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...), // rejestrujemy nowy tag
		];
	}

	public function getProviders(): array
	{
		return [
			'appDevMode' => $this->isDevelopmentMode, // rejestrujemy provider
		];
	}
}

// przy rejestracji rozszerzenia:
$isDev = true; // ustal to na podstawie środowiska swojej aplikacji
$latte->addExtension(new MyLatteExtension($isDev));

I użyj go w szablonie:

<p>Zwykła treść, zawsze widoczna.</p>

{debug}
	<div class="debug-panel">
		ID bieżącego użytkownika: {$user->id}
		Czas żądania: {=time()}
	</div>
{/debug}

<p>Więcej zwykłej treści.</p>

Integracja z n:atrybutami

Latte oferuje dla wielu tagów parzystych wygodny skrót: n:atrybuty. Jeśli masz tag parzysty w rodzaju {tag}...{/tag} i chcesz, aby jego działanie dotyczyło bezpośrednio jednego elementu HTML, często możesz zapisać go zwięźlej jako atrybut n:tag tego elementu.

Dla większości standardowych tagów parzystych, które zdefiniujesz (jak nasz {debug}), Latte automatycznie włącza odpowiadającą wersję n:. Przy rejestracji nie musisz robić nic dodatkowego:

{* standardowe użycie tagu parzystego *}
{debug}<div>Informacje debugowania</div>{/debug}

{* równoważne użycie z n:atrybutem *}
<div n:debug>Informacje debugowania</div>

Oba wyrenderują <div> tylko wtedy, gdy $this->global->appDevMode jest prawdziwe. Prefiksy inner- i tag- również działają zgodnie z oczekiwaniami.

Czasem logika Twojego tagu może zachowywać się nieco inaczej w zależności od tego, czy został użyty jako zwykły tag parzysty, czy jako n:atrybut, albo czy użyto prefiksu w rodzaju n:inner-tag czy n:tag-tag. Obiekt Latte\Compiler\Tag przekazywany do Twojej funkcji parsującej create() dostarcza tych informacji:

  • $tag->isNAttribute(): bool: Zwraca true, jeśli tag jest parsowany jako n:atrybut
  • $tag->prefix: ?string: Zwraca prefiks użyty z n:atrybutem, którym może być null (nie jest to n:atrybut), Tag::PrefixNone, Tag::PrefixInner albo Tag::PrefixTag

Skoro rozumiemy już proste tagi, parsowanie argumentów, tagi parzyste, providery i n:atrybuty, zmierzmy się z bardziej złożonym scenariuszem, w którym tagi zagnieżdżają się w innych tagach, wychodząc od naszego tagu {debug}.

Tagi pośrednie

Niektóre tagi parzyste pozwalają, a nawet wymagają, aby przed końcowym tagiem zamykającym pojawiły się wewnątrz nich inne tagi. Nazywamy je tagami pośrednimi. Klasyczne przykłady to {if}...{elseif}...{else}...{/if} albo {switch}...{case}...{default}...{/switch}.

Rozszerzmy nasz tag {debug} o obsługę opcjonalnej klauzuli {else}, która wyrenderuje się, gdy aplikacja nie działa w trybie deweloperskim.

Cel: Zmodyfikować {debug} tak, aby obsługiwał opcjonalny tag pośredni {else}. Ostateczna składnia ma być {debug} ... {else} ... {/debug}.

Parsowanie tagów pośrednich za pomocą yield

Wiemy już, że yield wstrzymuje funkcję parsującą create() i zwraca sparsowaną treść wraz z tagiem zamykającym. yield daje jednak większą kontrolę: możesz przekazać mu tablicę nazw tagów pośrednich. Gdy parser napotka któryś z tych tagów na tym samym poziomie zagnieżdżenia (czyli jako bezpośredniego potomka tagu nadrzędnego, a nie wewnątrz innych bloków czy tagów w nim), również przerwie parsowanie treści.

Gdy parsowanie zostanie przerwane z powodu tagu pośredniego, kończy parsowanie treści, wznawia generator create() i przekazuje z powrotem częściowo sparsowaną treść oraz sam tag pośredni (zamiast końcowego tagu zamykającego). Nasza funkcja create() może wtedy obsłużyć ten tag pośredni (np. sparsować jego argumenty, jeśli jakieś miał) i ponownie wywołać yield, aby sparsować kolejną część treści aż do końcowego tagu zamykającego albo innego oczekiwanego tagu pośredniego.

Zmodyfikujmy DebugNode::create() tak, aby oczekiwał {else}:

<?php

namespace App\Templating;

use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DebugNode extends StatementNode
{
	// treść części {debug}
	public AreaNode $thenContent;
	// opcjonalna treść części {else}
	public ?AreaNode $elseContent = null;

	public static function create(Tag $tag): \Generator
	{
		$node = $tag->node = new self;

		// yield i oczekiwanie na {/debug} albo {else}
		[$node->thenContent, $nextTag] = yield ['else'];

		// sprawdzamy, czy tagiem, na którym się zatrzymaliśmy, był {else}
		if ($nextTag?->name === 'else') {
			// ponowny yield, aby sparsować treść między {else} a {/debug}
			[$node->elseContent, $endTag] = yield;
		}

		return $node;
	}

	// ... print() i getIterator() zaktualizujemy za chwilę ...
}

Teraz yield ['else'] mówi Latte, aby przerwało parsowanie nie tylko przy {/debug}, ale też przy {else}. Jeśli {else} zostanie napotkane, $nextTag będzie zawierać obiekt Tag dla {else}. Wywołujemy wtedy yield ponownie, bez argumentów, co oznacza, że oczekujemy już tylko końcowego tagu {/debug}, a wynik zapisujemy do $node->elseContent. Jeśli {else} nie zostało znalezione, $nextTag będzie obiektem Tag dla {/debug} (albo null, gdy użyto n:atrybutu), a $node->elseContent pozostanie null.

Implementacja print() z {else}

Metoda print() musi odzwierciedlić nową strukturę. Ma wygenerować instrukcję if/else PHP opartą na providerze appDevMode.

	public function print(PrintContext $context): string
	{
		return $context->format(
			<<<'XX'
				if ($this->global->appDevMode) %line {
					%node // kod gałęzi 'then' (treść {debug})
				} else {
					%node // kod gałęzi 'else' (treść {else})
				}

				XX,
			$this->position,    // numer wiersza dla warunku 'if'
			$this->thenContent, // pierwszy symbol %node
			$this->elseContent ?? new NopNode, // drugi symbol %node
		);
	}

To standardowa struktura if/else PHP. Używamy %node dwukrotnie; format() podstawia podane węzły po kolei. Używamy ?? new NopNode, aby uniknąć błędów, gdy $this->elseContent jest null – NopNode po prostu nic nie wypisuje.

Implementacja getIterator() dla obu treści

Mamy teraz potencjalnie dwa węzły treści potomnej ($thenContent i $elseContent). Musimy zwrócić oba, jeśli istnieją:

	public function &getIterator(): \Generator
	{
		yield $this->thenContent;
		if ($this->elseContent) {
			yield $this->elseContent;
		}
	}

Użycie rozbudowanego tagu

Tagu można teraz używać z opcjonalną klauzulą {else}:

{debug}
	<p>Informacje debugowania są pokazywane, bo devMode jest WŁĄCZONY.</p>
{else}
	<p>Informacje debugowania są ukryte, bo devMode jest WYŁĄCZONY.</p>
{/debug}

Obsługa stanu i zagnieżdżania

Nasze poprzednie przykłady ({datetime}, {debug}) były w swoich metodach print() stosunkowo bezstanowe. Albo bezpośrednio wypisywały treść, albo wykonywały proste sprawdzenie warunku na podstawie globalnego providera. Wiele tagów musi jednak zarządzać jakąś formą stanu podczas renderowania albo obliczać wyrażenia podane przez użytkownika, które ze względu na wydajność lub poprawność powinny wykonać się tylko raz. Ponadto musimy pomyśleć o tym, co się dzieje, gdy nasze własne tagi zostaną zagnieżdżone.

Zilustrujmy te koncepcje, tworząc tag {repeat $count}...{/repeat}. Tag ten powtórzy swoją treść wewnętrzną $count razy.

Cel: Zaimplementować {repeat $count}, który powtarza swoją treść podaną liczbę razy.

Potrzeba zmiennych tymczasowych i unikalnych

Wyobraź sobie, że użytkownik pisze:

{repeat rand(1, 5)} Content {/repeat}

Gdybyśmy naiwnie wygenerowali w naszej metodzie print() taką pętlę for w PHP:

// uproszczony, NIEPOPRAWNY wygenerowany kod
for ($i = 0; $i < rand(1, 5); $i++) {
	// print content
}

Byłoby to błędne! Wyrażenie rand(1, 5) byłoby obliczane ponownie przy każdej iteracji pętli, co dałoby nieprzewidywalną liczbę powtórzeń. Musimy obliczyć wyrażenie $count raz, zanim pętla się zacznie, i zapisać jego wynik.

Wygenerujemy kod PHP, który najpierw obliczy wyrażenie z liczbą powtórzeń i zapisze je do tymczasowej zmiennej uruchomieniowej. Aby uniknąć kolizji ze zmiennymi zdefiniowanymi przez użytkownika szablonu oraz z wewnętrznymi zmiennymi Latte (jak $ʟ_...), dla naszych zmiennych tymczasowych użyjemy konwencji prefiksu $__ (podwójne podkreślenie).

Wygenerowany kod wyglądałby wtedy tak:

$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
	// print content
}

Rozważ teraz zagnieżdżanie:

{repeat $countA}       {* pętla zewnętrzna *}
	{repeat $countB}   {* pętla wewnętrzna *}
		...
	{/repeat}
{/repeat}

Gdyby zewnętrzny i wewnętrzny tag {repeat} wygenerowały kod z tymi samymi nazwami zmiennych tymczasowych (np. $__count i $__i), pętla wewnętrzna nadpisałaby zmienne pętli zewnętrznej i logika przestałaby działać.

Musimy zapewnić, aby zmienne tymczasowe generowane dla każdego wystąpienia tagu {repeat} były unikalne. Osiągamy to metodą PrintContext::generateId(). Zwraca ona podczas fazy kompilacji unikalną liczbę całkowitą. Możemy dołączyć to ID do nazw naszych zmiennych tymczasowych.

Zamiast $__count wygenerujemy więc nazwę z unikalnym przyrostkiem liczbowym, na przykład $__count_0, i podobnie dla licznika pętli, np. $__i_0. Konkretne liczby pochodzą z licznika obowiązującego dla całej kompilacji, wspólnego dla wszystkich węzłów, więc gwarantowana jest tylko ich unikalność, a nie to, że utworzą sekwencję w obrębie tagu.

Implementacja RepeatNode

Utwórzmy klasę węzła.

<?php

namespace App\Templating;

use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class RepeatNode extends StatementNode
{
	public ExpressionNode $count;
	public AreaNode $content;

	/**
	 * Funkcja parsująca dla {repeat $count} ... {/repeat}
	 */
	public static function create(Tag $tag): \Generator
	{
		$tag->expectArguments(); // upewniamy się, że $count został podany
		$node = $tag->node = new self;
		// parsujemy wyrażenie z liczbą powtórzeń
		$node->count = $tag->parser->parseExpression();
		// pobieramy treść wewnętrzną
		[$node->content] = yield;
		return $node;
	}

	/**
	 * Generuje pętlę 'for' PHP z unikalnymi nazwami zmiennych.
	 */
	public function print(PrintContext $context): string
	{
		// generujemy unikalne nazwy zmiennych
		$id = $context->generateId();
		$countVar = '$__count_' . $id; // unikalna nazwa, np. $__count_0
		$iteratorVar = '$__i_' . $id;  // unikalna nazwa, np. $__i_0

		return $context->format(
			<<<'XX'
				// obliczamy wyrażenie z liczbą powtórzeń *raz* i je zapisujemy
				%raw = (int) (%node);
				// pętla używająca zapisanej liczby i unikalnej zmiennej iteratora
				for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
					%node // renderujemy treść wewnętrzną
				}

				XX,
			$countVar,          // %0 - zmienna na liczbę powtórzeń
			$this->count,       // %1 - węzeł wyrażenia z liczbą powtórzeń
			$iteratorVar,       // %2 - nazwa zmiennej iteratora pętli
			$this->position,    // %3 - komentarz z numerem wiersza dla samej pętli
			$this->content      // %4 - węzeł treści wewnętrznej
		);
	}

	/**
	 * Zwraca węzły potomne (wyrażenie z liczbą powtórzeń i treść).
	 */
	public function &getIterator(): \Generator
	{
		yield $this->count;
		yield $this->content;
	}
}

Metoda create() parsuje wymagane wyrażenie $count przy użyciu parseExpression(). Najpierw wywoływane jest $tag->expectArguments(). Zapewnia to, że użytkownik podał cokolwiek po {repeat}. $tag->parser->parseExpression() też by zawiodło, gdyby nic nie podano, ale komunikat o błędzie mówiłby o nieoczekiwanej składni. Użycie expectArguments() daje o wiele czytelniejszy błąd, jasno wskazujący, że tagowi {repeat} brakuje argumentów.

Metoda print() generuje kod PHP odpowiedzialny za wykonanie logiki powtarzania w czasie działania. Zaczyna od wygenerowania unikalnych nazw tymczasowych zmiennych PHP, których będzie potrzebować.

Metoda $context->format() wywoływana jest z nowym symbolem zastępczym %raw, który wstawia surowy łańcuch podany jako odpowiadający mu argument. Tutaj wstawia unikalną nazwę zmiennej zapisaną w $countVar (np. $__count_1). A co z %0.raw i %2.raw? To pokaz symboli pozycyjnych. Zamiast zwykłego %raw, który bierze następny dostępny argument surowy, %2.raw bierze jawnie argument o indeksie 2 (czyli $iteratorVar) i wstawia jego surową wartość łańcuchową. Pozwala nam to wielokrotnie użyć łańcucha $iteratorVar bez przekazywania go kilka razy na liście argumentów format().

To starannie skonstruowane wywołanie format() generuje wydajną i bezpieczną pętlę PHP, która poprawnie obsługuje wyrażenie z liczbą powtórzeń i unika kolizji nazw zmiennych nawet przy zagnieżdżonych tagach {repeat}.

Rejestracja i użycie

Zarejestruj tag w swoim rozszerzeniu:

use App\Templating\RepeatNode;

class MyLatteExtension extends Extension
{
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...),
			'repeat' => RepeatNode::create(...), // rejestrujemy tag repeat
		];
	}
}

Użyj go w szablonie, także z zagnieżdżeniem:

{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}

{repeat $rows}
	<tr>
		{repeat $cols}
			<td>Pętla wewnętrzna</td>
		{/repeat}
	</tr>
{/repeat}

Ten przykład pokazuje, jak radzić sobie ze stanem (licznikami pętli) i potencjalnymi problemami z zagnieżdżaniem, używając zmiennych tymczasowych z prefiksem $__ i uczynionych unikalnymi przez ID z PrintContext::generateId().

Czyste n:atrybuty

Wiele n:atrybutów, jak n:if czy n:foreach, służy jako wygodny skrót dla odpowiadających im tagów parzystych ({if}...{/if}, {foreach}...{/foreach}), ale Latte pozwala też definiować tagi istniejące wyłącznie w postaci n:atrybutu. Używa się ich często do modyfikowania atrybutów albo zachowania elementu HTML, do którego są dołączone.

Standardowe przykłady wbudowane w Latte to n:class, który pomaga dynamicznie budować atrybut class, oraz n:attr, który potrafi ustawić wiele dowolnych atrybutów.

Utwórzmy własny czysty n:atrybut: n:confirm, który przed wykonaniem akcji (jak przejście przez odnośnik albo wysłanie formularza) wyświetli okno potwierdzenia w JavaScripcie.

Cel: Zaimplementować n:confirm="'Czy na pewno?'", który dodaje handler onclick uniemożliwiający domyślną akcję, gdy użytkownik anuluje okno potwierdzenia.

Implementacja ConfirmNode

Potrzebujemy klasy węzła i funkcji parsującej.

<?php

namespace App\Templating;

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;

class ConfirmNode extends StatementNode
{
	public ExpressionNode $message;

	public static function create(Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->message = $tag->parser->parseExpression();
		return $node;
	}

	/**
	 * Generuje kod atrybutu 'onclick' z poprawnym escapowaniem.
	 */
	public function print(PrintContext $context): string
	{
		// zapewnia poprawne escapowanie zarówno dla JavaScriptu, jak i kontekstu atrybutu HTML
		return $context->format(
			<<<'XX'
				echo ' onclick="', LR\HtmlHelpers::escapeAttr('return confirm(' . LR\Helpers::escapeJs(%node) . ')'), '"' %line;
				XX,
			$this->message,
			$this->position,
		);
	}

	public function &getIterator(): \Generator
	{
		yield $this->message;
	}
}

Metoda print() generuje kod PHP, który przy renderowaniu szablonu ostatecznie wypisze atrybut HTML onclick="...". Obsługa zagnieżdżonych kontekstów (JavaScript wewnątrz atrybutu HTML) wymaga starannego escapowania. Pomocnik LR\Helpers::escapeJs(%node) jest wywoływany w czasie działania i escapuje komunikat poprawnie do użycia wewnątrz JavaScriptu (wynikiem byłoby coś w rodzaju "Na pewno?"). Następnie pomocnik LR\HtmlHelpers::escapeAttr(...) escapuje znaki mające specjalne znaczenie w atrybutach HTML, dzięki czemu z wyniku powstanie return confirm(&quot;Na pewno?&quot;). To dwuetapowe escapowanie w czasie działania zapewnia, że komunikat jest bezpieczny dla JavaScriptu, a powstały kod JavaScriptu jest bezpieczny do osadzenia w atrybucie HTML onclick.

Rejestracja i użycie

Zarejestruj n:atrybut w swoim rozszerzeniu. Pamiętaj o prefiksie n: w kluczu:

class MyLatteExtension extends Extension
{
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...),
			'repeat' => RepeatNode::create(...),
			'n:confirm' => ConfirmNode::create(...), // rejestrujemy n:confirm
		];
	}
}

Teraz możesz używać n:confirm na odnośnikach, przyciskach albo elementach formularza:

<a href="delete.php?id=123" n:confirm='"Czy na pewno chcesz usunąć element {$id}?"'>Usuń</a>

Wygenerowany HTML:

<a href="delete.php?id=123" onclick="return confirm(&quot;Czy na pewno chcesz usunąć element 123?&quot;)">Usuń</a>

Gdy użytkownik kliknie odnośnik, przeglądarka wykona kod onclick, wyświetli okno potwierdzenia i przejdzie do delete.php tylko wtedy, gdy użytkownik kliknie “OK”.

Ten przykład pokazuje, jak czysty n:atrybut może modyfikować zachowanie albo atrybuty elementu HTML, do którego należy, generując odpowiedni kod PHP w swojej metodzie print(). Pamiętaj o podwójnym escapowaniu, które często jest konieczne: raz dla kontekstu docelowego (w tym przypadku JavaScriptu) i ponownie dla kontekstu atrybutu HTML.

Przy pisaniu czystych n:atrybutów przydają się jeszcze dwa elementy obiektu Tag: $tag->htmlElement daje dostęp do otaczającego elementu HTML (obiektu ElementNode), więc możesz go zbadać albo dostosować, a $tag->replaceNAttribute($node) pozwala zamienić atrybut na węzeł, który zbudujesz. Węzeł zwrócony z metody create() czystego n:atrybutu i tak automatycznie zastępuje atrybut na jego elemencie.

Tematy zaawansowane

Poprzednie sekcje obejmują podstawowe koncepcje, ale oto jeszcze kilka zaawansowanych tematów, z którymi możesz zetknąć się przy tworzeniu własnych tagów Latte.

Tryby wyniku tagu

Obiekt Tag przekazywany do Twojej funkcji create() ma właściwość outputMode. Właściwość ta wpływa na to, jak Latte obchodzi się z otaczającymi białymi znakami i wcięciami, zwłaszcza gdy tag stoi w wierszu sam. Możesz zmienić tę właściwość wewnątrz swojej funkcji create().

  • Tag::OutputNone (domyślny dla każdego tagu, zachowywany przez struktury sterujące, takie jak {if} czy {foreach}): białe znaki wokół tagu traktowane są dokładnie tak jak przy OutputRemoveIndentation – usuwane są wiodące wcięcie i jeden końcowy znak nowego wiersza. Prawdziwa różnica jest wewnętrzna: ten tryb utrzymuje parser szablonu w trybie “nagłówka” szablonu. Nadaje się do tagów deklaracyjnych albo konfiguracyjnych, takich jak {var} czy {default}, które nie produkują bezpośredniego wyniku.
  • Tag::OutputRemoveIndentation (ustawiany jawnie przez tagi blokowe {block}, {embed}, {include} i {sandbox}): usuwa wiodące wcięcie przed tagiem i jeden końcowy znak nowego wiersza. Pomaga to utrzymać wygenerowany kod PHP czystszym i unika dodatkowych pustych wierszy w wyniku HTML spowodowanych przez sam tag.
  • Tag::OutputKeepIndentation (ustawiany jawnie przez tagi wypisujące, takie jak {=...}): Latte stara się zachować wcięcie przed tagiem; znaki nowego wiersza po tagu są zwykle zachowywane. Nadaje się to do tagów wypisujących treść w linii – zobacz powyższy przykład {datetime}, który ustawia ten tryb dokładnie z tego powodu.

Wybierz tryb, który najlepiej pasuje do przeznaczenia Twojego tagu. Ponieważ domyślny jest OutputNone, tagi sterujące przepływem i deklaracyjne nie wymagają zmiany; OutputKeepIndentation ustaw dla tagów, które wypisują treść w osobnym wierszu.

Dostęp do tagów nadrzędnych i najbliższych

Czasem zachowanie tagu musi zależeć od kontekstu, w którym został użyty, a konkretnie od tego, wewnątrz których tagów nadrzędnych się znajduje. Obiekt Tag przekazywany do Twojej funkcji create() udostępnia dokładnie w tym celu metodę closestTag(array $classes, ?callable $condition = null): ?Tag.

Metoda ta przeszukuje w górę hierarchię aktualnie otwartych tagów Latte (łańcuch $tag->parent; otaczające elementy HTML nie są jego częścią) i zwraca obiekt Tag najbliższego przodka spełniającego określone kryteria. Jeśli pasujący przodek się nie znajdzie, zwraca null.

Tablica $classes określa, jakiego rodzaju tagów przodków szukasz. Sprawdza, czy klasa węzła powiązanego z tagiem przodka ($ancestorTag->node) jest dokładnie jedną z wymienionych klas; podklasy nie pasują.

function create(Tag $tag)
{
	// szukamy najbliższego tagu przodka, którego węzeł jest instancją ForeachNode
	$foreachTag = $tag->closestTag([ForeachNode::class]);
	if ($foreachTag) {
		// możemy sięgnąć po samą instancję ForeachNode:
		$foreachNode = $foreachTag->node;
	}
}

Zwróć uwagę na $foreachTag->node: działa to tylko dlatego, że w Latte konwencją przy tworzeniu tagów jest natychmiastowe przypisanie utworzonego węzła do $tag->node w metodzie create(), tak jak robiliśmy to zawsze.

Czasem samo dopasowanie typu węzła nie wystarcza. Możesz potrzebować sprawdzić konkretną właściwość potencjalnego tagu przodka albo jego węzła. Opcjonalnym drugim argumentem closestTag() jest callable, który otrzymuje potencjalny obiekt Tag przodka i ma zwrócić, czy jest to poprawne dopasowanie.

function create(Tag $tag)
{
	$dynamicBlockTag = $tag->closestTag(
		[BlockNode::class],
		// warunek: blok musi być dynamiczny
		fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
	);
}

Użycie closestTag() pozwala tworzyć tagi świadome kontekstu i wymuszać poprawne użycie w strukturze Twojego szablonu, co prowadzi do solidniejszych i bardziej zrozumiałych szablonów.

Symbole zastępcze PrintContext::format()

Metody PrintContext::format() używaliśmy często do generowania kodu PHP w metodach print() naszych węzłów. Przyjmuje ona łańcuch maski i kolejne argumenty zastępujące symbole w masce. Oto podsumowanie dostępnych symboli:

  • %node: Argument musi być instancją Node. Wywołuje metodę print() węzła i wstawia powstały łańcuch kodu PHP.
  • %dump: Argumentem jest dowolna wartość PHP. Eksportuje wartość do poprawnego kodu PHP. Nadaje się do skalarów, tablic, null.
    • $context->format('echo %dump;', 'Hello')echo 'Hello';
    • $context->format('$arr = %dump;', [1, 2])$arr = [1, 2];
  • %raw: Wstawia argument bezpośrednio do wynikowego kodu PHP bez żadnego escapowania ani modyfikacji. Używaj ostrożnie, przede wszystkim do wstawiania wcześniej wygenerowanych fragmentów kodu PHP albo nazw zmiennych.
    • $context->format('%raw = 1;', '$variableName')$variableName = 1;
  • %args: Argument musi być typu Expression\ArrayNode. Wypisuje elementy tablicy sformatowane jako argumenty wywołania funkcji albo metody (oddzielone przecinkami, z obsługą argumentów nazwanych, jeśli występują).
    • $argsNode = new ArrayNode([...]);
    • $context->format('myFunc(%args);', $argsNode)myFunc(1, name: 'Joe');
  • %line: Argument musi być obiektem Position (albo Range), zwykle $this->position. Wstawia komentarz PHP /* pos X:Y */ wskazujący wiersz i kolumnę w źródle.
    • $context->format('echo "Hi" %line;', $this->position)echo "Hi" /* pos 42:1 */;
  • %escape(...): Generuje kod PHP, który w czasie działania zescapuje wewnętrzne wyrażenie zgodnie z bieżącymi regułami escapowania świadomego kontekstu.
    • $context->format('echo %escape(%node);', $variableNode)
  • %modify(...): Argument musi być typu ModifierNode. Generuje kod PHP, który stosuje do treści wewnętrznej filtry podane w ModifierNode, wraz z escapowaniem świadomym kontekstu, o ile nie zostało wyłączone przez |noescape.
    • $context->format('%modify(%node);', $modifierNode, $variableNode)
  • %modifyContent(...): Podobne do %modify, ale przeznaczone do modyfikowania bloków przechwyconej treści (zwykle HTML).

Możesz jawnie odwoływać się do argumentów przez ich indeks liczony od zera: %0.node, %1.dump, %2.raw itd. Pozwala to wielokrotnie użyć argumentu w masce bez powtarzania go w wywołaniu format(). Zobacz przykład tagu {repeat}, gdzie użyto %0.raw i %2.raw.

Przykład złożonego parsowania argumentów

parseExpression(), parseArguments() itd. pokrywają wiele przypadków, ale czasem potrzebujesz bardziej zawiłej logiki parsowania z użyciem niskopoziomowego TokenStream dostępnego przez $tag->parser->stream.

Cel: Utworzyć tag {embedYoutube $videoID, width: 640, height: 480}. Chcemy sparsować wymagane ID wideo (łańcuch albo zmienną), po którym następują opcjonalne pary klucz-wartość z wymiarami.

<?php
namespace App\Templating;

use Latte\CompileException;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Tag;
use Latte\Compiler\Token;

class YoutubeNode extends StatementNode
{
	public ExpressionNode $videoId;
	public ?ExpressionNode $width = null;
	public ?ExpressionNode $height = null;

	public static function create(Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		// parsujemy wymagane ID wideo
		$node->videoId = $tag->parser->parseExpression();

		// parsujemy opcjonalne pary klucz-wartość
		$stream = $tag->parser->stream; // pobieramy strumień tokenów
		while ($stream->tryConsume(',')) { // wymaga oddzielenia przecinkiem
			// oczekujemy identyfikatora 'width' albo 'height'
			$keyToken = $stream->consume(Token::Php_Identifier);
			$key = strtolower($keyToken->text);

			$stream->consume(':'); // oczekujemy separatora w postaci dwukropka

			$value = $tag->parser->parseExpression(); // parsujemy wyrażenie wartości

			if ($key === 'width') {
				$node->width = $value;
			} elseif ($key === 'height') {
				$node->height = $value;
			} else {
				throw new CompileException("Unknown argument '$key'. Expected 'width' or 'height'.", $keyToken->position);
			}
		}

		return $node;
	}

	// ... print() i getIterator() ...
}

Taki poziom kontroli pozwala definiować bardzo specyficzne i złożone składnie własnych tagów przez bezpośrednią pracę ze strumieniem tokenów.

Użycie AuxiliaryNode

Latte udostępnia ogólne węzły “pomocnicze” na szczególne sytuacje przy generowaniu kodu albo wewnątrz compiler passów. Są to AuxiliaryNode i Php\Expression\AuxiliaryNode.

Traktuj AuxiliaryNode jak elastyczny węzeł kontenerowy, który deleguje swoje kluczowe funkcje – generowanie kodu i udostępnianie węzłów potomnych – do argumentów podanych w konstruktorze:

  • Delegacja print(): Pierwszym argumentem konstruktora jest domknięcie PHP. Gdy Latte wywoła metodę print() na AuxiliaryNode, wykona to podane domknięcie. Domknięcie otrzymuje PrintContext i wszystkie węzły przekazane w drugim argumencie konstruktora, co pozwala Ci zdefiniować w locie całkowicie własną logikę generowania kodu PHP.
  • Delegacja getIterator(): Drugim argumentem konstruktora jest tablica obiektów Node. Gdy Latte potrzebuje przejść po potomkach AuxiliaryNode (np. podczas compiler passów), jego metoda getIterator() po prostu zwraca węzły z tej tablicy.

Przykład:

$node = new AuxiliaryNode(
    // 1. to domknięcie staje się ciałem print()
    fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),

    // 2. te węzły są zwracane przez getIterator() i przekazywane do powyższego domknięcia
    [$argumentNode1, $argumentNode2]
);

Latte udostępnia dwa różne typy, zależnie od tego, gdzie potrzebujesz wstawić wygenerowany kod:

  • Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: Użyj go, gdy potrzebujesz wygenerować fragment kodu PHP reprezentujący wyrażenie
  • Latte\Compiler\Nodes\AuxiliaryNode: Użyj go do ogólniejszych celów, gdy potrzebujesz wstawić blok kodu PHP reprezentujący jedną lub więcej instrukcji

Ważnym powodem, aby użyć AuxiliaryNode zamiast standardowych węzłów (jak StaticMethodCallNode) wewnątrz metody print() albo compiler passa, jest kontrola widoczności dla kolejnych compiler passów, zwłaszcza tych związanych z bezpieczeństwem, jak Sandbox.

Rozważ taki scenariusz: Twój compiler pass musi opakować wyrażenie podane przez użytkownika ($userExpr) wywołaniem konkretnej, zaufanej funkcji pomocniczej myInternalSanitize($userExpr). Jeśli utworzysz standardowy węzeł new FunctionCallNode('myInternalSanitize', [$userExpr]), będzie on w pełni widoczny dla traversera AST. Jeśli później uruchomi się pass Sandboxa, a myInternalSanitize nie będzie na jego liście dozwolonych, Sandbox może to wywołanie zablokować albo zmodyfikować, potencjalnie psując wewnętrzną logikę Twojego tagu, mimo że Ty, autor tagu, wiesz, że to konkretne wywołanie jest bezpieczne i konieczne. Możesz więc wygenerować to wywołanie bezpośrednio wewnątrz domknięcia AuxiliaryNode.

use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;

// ... wewnątrz print() albo compiler passa ...
$wrappedNode = new AuxiliaryNode(
	fn(PrintContext $context, $userExpr) => $context->format(
		'myInternalSanitize(%node)', // bezpośrednie generowanie kodu PHP
		$userExpr,
	),
	// WAŻNE: mimo to przekaż tutaj pierwotny węzeł wyrażenia użytkownika!
	[$userExpr],
);

W tym przypadku pass Sandboxa widzi AuxiliaryNode, ale nie analizuje kodu PHP wygenerowanego przez jego domknięcie. Nie może bezpośrednio zablokować wywołania myInternalSanitize wygenerowanego wewnątrz domknięcia.

Sam wygenerowany kod PHP jest wprawdzie ukryty przed passami, ale wejścia tego kodu (węzły reprezentujące dane albo wyrażenia użytkownika) nadal muszą być dostępne do przechodzenia. Dlatego drugi argument konstruktora AuxiliaryNode ma kluczowe znaczenie. Musisz przekazać tablicę zawierającą wszystkie pierwotne węzły (jak $userExpr w powyższym przykładzie), których używa Twoje domknięcie. Metoda getIterator() klasy AuxiliaryNode zwróci te węzły, pozwalając compiler passom, jak Sandbox, przeanalizować je pod kątem potencjalnych problemów.

Dobre praktyki

  • Jasny cel: Zadbaj, aby Twój tag miał jasny i uzasadniony cel. Nie twórz tagów do zadań, które łatwo rozwiążą filtry albo funkcje.
  • Poprawnie implementuj getIterator(): Zawsze implementuj getIterator() i zwracaj referencje (&) do wszystkich węzłów potomnych (argumentów, treści) sparsowanych z szablonu. Jest to niezbędne dla compiler passów, bezpieczeństwa (Sandbox) i ewentualnych przyszłych optymalizacji.
  • Właściwości publiczne na węzły: Właściwości przechowujące węzły potomne czyń publicznymi, aby compiler passy mogły je w razie potrzeby modyfikować.
  • Używaj PrintContext::format(): Do generowania kodu PHP wykorzystuj metodę format(). Obsługuje cudzysłowy, poprawnie escapuje symbole zastępcze i automatycznie dodaje komentarze z numerami wierszy.
  • Zmienne tymczasowe ($__): Gdy generujesz kod PHP wymagający zmiennych tymczasowych (np. do przechowania wyników pośrednich, liczników pętli), używaj konwencji prefiksu $__, aby uniknąć kolizji ze zmiennymi użytkownika i wewnętrznymi zmiennymi Latte $ʟ_.
  • Zagnieżdżanie i unikalne ID: Jeśli Twój tag może być zagnieżdżany albo potrzebuje w czasie działania stanu właściwego dla instancji, użyj w metodzie print() $context->generateId(), aby utworzyć unikalne przyrostki zmiennych tymczasowych $__.
  • Providery do danych zewnętrznych: Do sięgania po dane albo usługi w czasie działania ($this->global->…) używaj providerów (rejestrowanych przez Extension::getProviders()), zamiast zapisywać wartości na sztywno albo polegać na stanie globalnym. W nazwach providerów używaj prefiksów dostawcy.
  • Pomyśl o n:atrybutach: Jeśli Twój tag parzysty działa logicznie na jednym elemencie HTML, Latte prawdopodobnie zapewni automatyczną obsługę n:atrybutu. Miej to na uwadze dla wygody użytkowników. Tworząc tag modyfikujący atrybuty, zastanów się, czy najodpowiedniejszą formą nie jest czysty n:atrybut.
  • Testowanie: Pisz testy swoich tagów, obejmujące zarówno parsowanie różnych wariantów składni, jak i poprawność wyniku wygenerowanego kodu PHP.

Trzymając się tych wskazówek, możesz tworzyć potężne, solidne i łatwe w utrzymaniu własne tagi, które płynnie integrują się z silnikiem szablonów Latte.

Studiowanie klas węzłów wchodzących w skład Latte to najlepszy sposób, aby poznać wszystkie zawiłości procesu parsowania.

wersja: 3.x