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:
- 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.). - 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).
- 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.
- 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.
- 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
.lattepiszesz 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. NapotkaszLatte\Compiler\Nodes\TextNodedla tekstu statycznego,Latte\Compiler\Nodes\Html\ElementNodedla elementów HTML,Latte\Compiler\Nodes\Php\ExpressionNodedla wyrażeń wewnątrz tagów, a co kluczowe przy własnych tagach, węzły dziedziczące poLatte\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.ExpressionNodedla sparsowanych argumentów,AreaNodedla sparsowanej treści). - Metoda
print(PrintContext $context): stringgeneruje kod PHP (instrukcję albo ich serię), który wykonuje działanie tagu podczas renderowania szablonu. - Metoda
getIterator(): \Generatorudostę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 metodygetIterator(), 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.foojest 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, zwracaStringNode.parseArguments(): ArrayNode: Parsuje argumenty oddzielone przecinkami, ewentualnie z kluczami, jak10, name: 'John', true.parseModifier(): ModifierNode: Parsuje filtry, jak|upper|truncate:10.parseType(): ?SuperiorTypeNode: Parsuje deklaracje typów PHP, jakint,?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łaszaCompileException. 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 zwracanull. 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 są. 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:
AreaNodereprezentujący sparsowaną treść między tagiem otwierającym a zamykającym.- Obiekt
Tagreprezentują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: Zwracatrue, 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::PrefixInneralboTag::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("Na pewno?"). 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("Czy na pewno chcesz usunąć element 123?")">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 przyOutputRemoveIndentation– 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ć typuExpression\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ć obiektemPosition(alboRange), 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ć typuModifierNode. Generuje kod PHP, który stosuje do treści wewnętrznej filtry podane wModifierNode, 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()naAuxiliaryNode, wykona to podane domknięcie. Domknięcie otrzymujePrintContexti 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ówNode. Gdy Latte potrzebuje przejść po potomkachAuxiliaryNode(np. podczas compiler passów), jego metodagetIterator()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żenieLatte\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 implementujgetIterator()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 czystyn: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.