Tworzenie własnych tagów
Ta strona zawiera kompleksowy przewodnik dotyczący tworzenia własnych tagów w Latte. Omówimy wszystko, od prostych tagów po bardziej złożone scenariusze z zagnieżdżoną zawartością i specyficznymi potrzebami parsowania, opierając się na zrozumieniu, jak Latte kompiluje szablony.
Własne tagi zapewniają najwyższy poziom kontroli nad składnią szablonu i logiką renderowania, ale są również najbardziej złożonym punktem rozszerzenia. Zanim zdecydujesz się stworzyć własny tag, zawsze rozważ, czy nie istnieje prostsze rozwiązanie lub czy odpowiedni tag nie istnieje już w standardowym zestawie. Używaj własnych tagów tylko wtedy, gdy prostsze alternatywy nie są wystarczające dla Twoich potrzeb.
Zrozumienie procesu kompilacji
Aby efektywnie tworzyć własne tagi, warto wyjaśnić, jak Latte przetwarza szablony. Zrozumienie tego procesu wyjaśnia, dlaczego tagi są skonstruowane w ten sposób i jak pasują do szerszego kontekstu.
Kompilacja szablonu w Latte, w uproszczeniu, obejmuje następujące kluczowe kroki:
- Analiza leksykalna: Lekser odczytuje kod źródłowy szablonu (plik
.latte
) i dzieli go na sekwencję małych, odrębnych części zwanych tokenami (np.{
,foreach
,$variable
,}
, tekst HTML, itp.). - Parsowanie: Parser bierze ten strumień tokenów i konstruuje z niego sensowną strukturę drzewiastą reprezentującą logikę i zawartość szablonu. To drzewo nazywa się abstrakcyjnym drzewem składniowym (AST).
- Przejścia kompilacji: Przed wygenerowaniem kodu PHP Latte uruchamia przejścia kompilacji. Są to funkcje, które przechodzą przez całe AST i mogą je modyfikować lub zbierać informacje. Ten krok jest kluczowy dla funkcji takich jak bezpieczeństwo (Sandbox) czy optymalizacje.
- Generowanie kodu: Na koniec kompilator przechodzi przez (potencjalnie zmodyfikowane) AST i generuje odpowiedni kod klasy PHP. Ten kod PHP jest tym, co faktycznie renderuje szablon podczas uruchomienia.
- Caching: Wygenerowany kod PHP jest zapisywany na dysku, co sprawia, że kolejne renderowania są bardzo szybkie, ponieważ kroki 1–4 są pomijane.
W rzeczywistości kompilacja jest nieco bardziej skomplikowana. Latte ma dwa leksery i parsery: jeden dla szablonu HTML i drugi dla kodu podobnego do PHP wewnątrz tagów. A także parsowanie nie odbywa się dopiero po tokenizacji, ale lekser i parser działają równolegle w dwóch “wątkach” i koordynują się. Uwierzcie mi, zaprogramowanie tego było jak lot w kosmos :-)
Cały proces, od załadowania zawartości szablonu, przez parsowanie, aż po wygenerowanie wynikowego pliku, można zsekwencjonować za pomocą tego kodu, z którym można eksperymentować i wypisywać 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
Stworzenie w pełni funkcjonalnego własnego tagu w Latte obejmuje kilka powiązanych części. Zanim przejdziemy do implementacji, zrozummy podstawowe koncepcje i terminologię, wykorzystując analogię do HTML i Document Object Model (DOM).
Tagi vs. Węzły (Analogia z HTML)
W HTML piszemy tagi takie jak <p>
lub <div>...</div>
. Te tagi są składnią
w kodzie źródłowym. Kiedy przeglądarka parsuje ten HTML, tworzy reprezentację w pamięci zwaną Document Object Model
(DOM). W DOM tagi HTML są reprezentowane przez węzły (konkretnie węzły Element
w terminologii
JavaScriptowego DOM). Z tymi węzłami pracujemy programowo (np. za pomocą JavaScriptowego
document.getElementById(...)
zwracany jest węzeł Element). Tag jest tylko tekstową reprezentacją w pliku
źródłowym; węzeł jest obiektową reprezentacją w logicznym drzewie.
Latte działa podobnie:
- W pliku szablonu
.latte
piszesz tagi Latte, takie jak{foreach ...}
i{/foreach}
. Jest to składnia, z którą pracujesz jako autor szablonu. - Kiedy Latte parsuje szablon, buduje Abstract Syntax Tree (AST). To drzewo składa się z węzłów. Każdy tag Latte, element HTML, fragment tekstu lub wyrażenie w szablonie staje się jednym lub więcej węzłami w tym drzewie.
- Podstawową klasą dla wszystkich węzłów w AST jest
Latte\Compiler\Node
. Podobnie jak DOM ma różne typy węzłów (Element, Text, Comment), AST Latte ma różne typy węzłów. SpotkaszLatte\Compiler\Nodes\TextNode
dla statycznego tekstu,Latte\Compiler\Nodes\Html\ElementNode
dla elementów HTML,Latte\Compiler\Nodes\Php\ExpressionNode
dla wyrażeń wewnątrz tagów i, co kluczowe dla własnych tagów, węzły dziedziczące zLatte\Compiler\Nodes\StatementNode
.
Dlaczego StatementNode
?
Elementy HTML (Html\ElementNode
) głównie reprezentują strukturę i zawartość. Wyrażenia PHP
(Php\ExpressionNode
) reprezentują wartości lub obliczenia. Ale co z tagami Latte takimi jak {if}
,
{foreach}
lub naszym własnym {datetime}
? Te tagi wykonują akcje, kontrolują przepływ
programu lub generują wyjście na podstawie logiki. Są to jednostki funkcjonalne, które czynią Latte potężnym
silnikiem szablonów, a nie tylko językiem znaczników.
W programowaniu takie jednostki wykonujące akcje często nazywane są “statements” (instrukcjami). Dlatego węzły
reprezentujące te funkcjonalne tagi Latte zazwyczaj dziedziczą z Latte\Compiler\Nodes\StatementNode
. To odróżnia
je od czysto strukturalnych węzłów (jak elementy HTML) lub węzłów reprezentujących wartości (jak wyrażenia).
Kluczowe komponenty
Przejdźmy przez główne komponenty potrzebne do stworzenia własnego tagu:
Funkcja parsowania tagu
- Ta funkcja PHP typu callable parsuje składnię tagu Latte (
{...}
) w szablonie źródłowym. - Otrzymuje informacje o tagu (takie jak jego nazwa, pozycja i czy jest to n:atrybut) za pośrednictwem obiektu Latte\Compiler\Tag.
- Jej 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
(jest to inny parser niż ten, który parsuje cały szablon). - Dla tagów parzystych używa
yield
do sygnalizowania Latte, aby sparsowało wewnętrzną zawartość między tagiem początkowym a końcowym. - Ostatecznym celem funkcji parsowania jest utworzenie i zwrócenie instancji klasy węzła, która jest dodawana do AST.
- Zwyczajem (choć nie jest to wymagane) jest implementowanie funkcji parsowania jako metody statycznej (często nazywanej
create
) bezpośrednio w odpowiedniej klasie węzła. Utrzymuje to logikę parsowania i reprezentację węzła schludnie w jednym pakiecie, umożliwia dostęp do prywatnych/chronionych elementów klasy, jeśli jest to potrzebne, i poprawia organizację.
Klasa węzła
- Reprezentuje logiczną funkcję Twojego tagu w Abstract Syntax Tree (AST).
- Zawiera sparsowane informacje (takie jak argumenty lub zawartość) jako publiczne właściwości. Te właściwości często
zawierają inne instancje
Node
(np.ExpressionNode
dla sparsowanych argumentów,AreaNode
dla sparsowanej zawartości). - Metoda
print(PrintContext $context): string
generuje kod PHP (instrukcję lub serię instrukcji), który wykonuje akcję tagu podczas renderowania szablonu. - Metoda
getIterator(): \Generator
udostępnia węzły potomne (argumenty, zawartość) do przechodzenia przez przejścia kompilacji. Musi dostarczać referencje (&
), aby umożliwić przejściom potencjalne modyfikowanie lub zastępowanie podwęzłów. - Po tym, jak cały szablon zostanie sparsowany do AST, Latte uruchamia serię przejść kompilacji. Te przejścia przechodzą przez całe AST za
pomocą metody
getIterator()
dostarczonej przez każdy węzeł. Mogą one sprawdzać węzły, zbierać informacje, a nawet modyfikować drzewo (np. zmieniając publiczne właściwości węzłów lub całkowicie zastępując węzły). Ten projekt, wymagający kompleksowegogetIterator()
, jest kluczowy. Umożliwia potężnym funkcjom, takim jak Sandbox, analizowanie i potencjalne zmienianie zachowania każdej części szablonu, w tym Twoich własnych tagów, zapewniając bezpieczeństwo i spójność.
Rejestracja przez rozszerzenie
- Musisz poinformować Latte o swoim nowym tagu i która funkcja parsowania ma być dla niego użyta. Odbywa się to w ramach rozszerzenia Latte.
- Wewnątrz swojej klasy rozszerzenia implementujesz metodę
getTags(): array
. Ta metoda zwraca tablicę asocjacyjną, gdzie klucze są nazwami tagów (np.'mytag'
,'n:myattribute'
), a wartości są funkcjami PHP typu callable reprezentującymi ich odpowiednie funkcje parsowania (np.MyNamespace\DatetimeNode::create(...)
).
Podsumowanie: Funkcja parsowania tagu przekształca kod źródłowy szablonu Twojego tagu w węzeł AST.
Klasa węzła następnie potrafi przekształcić siebie w wykonywalny kod PHP dla skompilowanego szablonu
i udostępnia swoje podwęzły dla przejść kompilacji przez getIterator()
. Rejestracja przez
rozszerzenie łączy nazwę tagu z funkcją parsowania i informuje o nim Latte.
Teraz zbadamy, jak zaimplementować te komponenty krok po kroku.
Tworzenie prostego tagu
Zajmijmy się tworzeniem Twojego pierwszego własnego tagu Latte. Zaczniemy od bardzo prostego przykładu: tagu o nazwie
{datetime}
, który wypisuje aktualną datę i czas. Początkowo ten tag nie będzie przyjmował żadnych
argumentów, ale ulepszymy go później w sekcji “Parsowanie argumentów tagu”.
Nie ma również żadnej wewnętrznej zawartości.
Ten przykład przeprowadzi Cię przez podstawowe kroki: zdefiniowanie klasy węzła, implementację jej metod
print()
i getIterator()
, utworzenie funkcji parsowania i wreszcie rejestrację tagu.
Cel: Zaimplementować {datetime}
do wypisywania aktualnej daty i czasu za pomocą funkcji PHP
date()
.
Tworzenie klasy węzła
Najpierw potrzebujemy klasy, która będzie reprezentować nasz tag w Abstract Syntax Tree (AST). Jak omówiono powyżej,
dziedziczymy z Latte\Compiler\Nodes\StatementNode
.
Utwórz plik (np. DatetimeNode.php
) i zdefiniuj klasę:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Funkcja parsowania tagu, wywoływana, gdy zostanie znaleziony {datetime}.
*/
public static function create(Tag $tag): self
{
// Nasz prosty tag obecnie nie przyjmuje żadnych argumentów, więc nie musimy niczego parsować
$node = $tag->node = new self;
return $node;
}
/**
* Generuje kod PHP, który zostanie uruchomiony podczas renderowania szablonu.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Zapewnia dostęp do węzłów potomnych dla przejść kompilacji Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Kiedy Latte napotka {datetime}
w szablonie, wywoła funkcję parsowania create()
. Jej zadaniem jest
zwrócenie instancji DatetimeNode
.
Metoda print()
generuje kod PHP, który zostanie uruchomiony podczas renderowania szablonu. Wywołujemy metodę
$context->format()
, która buduje wynikowy ciąg kodu PHP dla skompilowanego szablonu. Pierwszy argument,
'echo date('Y-m-d H:i:s') %line;'
, jest maską, do której są uzupełniane następujące parametry. Symbol
zastępczy %line
mówi metodzie format()
, aby użyła drugiego argumentu, którym jest
$this->position
, i wstawiła komentarz jak /* line 15 */
, który łączy wygenerowany kod PHP
z powrotem do oryginalnego wiersza szablonu, co jest kluczowe dla debugowania.
Właściwość $this->position
jest dziedziczona z klasy bazowej Node
i jest automatycznie
ustawiana przez parser Latte. Zawiera obiekt Latte\Compiler\Position, który wskazuje, gdzie tag
został znaleziony w pliku źródłowym .latte
.
Metoda getIterator()
jest kluczowa dla przejść kompilacji. Musi dostarczać wszystkie węzły potomne, ale nasz
prosty DatetimeNode
obecnie nie ma żadnych argumentów ani zawartości, a więc żadnych węzłów potomnych.
Niemniej jednak metoda musi nadal istnieć i być generatorem, tj. słowo kluczowe yield
musi być w jakiś sposób
obecne w ciele metody.
Rejestracja przez rozszerzenie
Na koniec poinformujmy Latte o nowym tagu. Utwórz klasę rozszerzenia (np.
MyLatteExtension.php
) i zarejestruj tag w jej metodzie getTags()
.
<?php
namespace App\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Zwraca listę tagów dostarczanych przez to rozszerzenie.
* @return array<string, callable> Mapa: 'nazwa-tagu' => funkcja-parsowania
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Później zarejestruj tutaj więcej tagów
];
}
}
Następnie zarejestruj to rozszerzenie w Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Utwórz szablon:
<p>Strona wygenerowana: {datetime}</p>
Oczekiwane wyjście: <p>Strona wygenerowana: 2023-10-27 11:00:00</p>
Podsumowanie tej fazy
Pomyślnie stworzyliśmy podstawowy własny tag {datetime}
. Zdefiniowaliśmy jego reprezentację w AST
(DatetimeNode
), obsłużyliśmy jego parsowanie (create()
), określiliśmy, jak powinien generować kod
PHP (print()
), zapewniliśmy, że jego dzieci są dostępne do przechodzenia (getIterator()
),
i zarejestrowaliśmy go w Latte.
W następnej sekcji ulepszymy ten tag tak, aby przyjmował argumenty, i pokażemy, 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. Ulepszmy go, aby przyjmował opcjonalny
argument: ciąg formatujący dla funkcji date()
. Wymagana składnia będzie {datetime $format}
.
Cel: Zmodyfikować {datetime}
tak, aby przyjmował opcjonalne wyrażenie PHP jako argument, które zostanie
użyte jako ciąg formatujący dla date()
.
Wprowadzenie TagParser
Zanim zmodyfikujemy kod, ważne jest, aby zrozumieć narzędzie, którego będziemy używać: Latte\Compiler\TagParser. Kiedy główny parser Latte
(TemplateParser
) napotka tag Latte, taki jak {datetime ...}
lub n:atrybut, deleguje parsowanie
zawartości wewnątrz tagu (część między {
a }
lub wartość atrybutu) do wyspecjalizowanego
TagParser
.
Ten TagParser
pracuje wyłącznie z argumentami tagu. Jego zadaniem jest przetwarzanie tokenów
reprezentujących te argumenty. Kluczowe jest, że musi przetworzyć całą zawartość, która jest mu dostarczona. Jeśli
Twoja funkcja parsowania zakończy się, ale TagParser
nie osiągnął końca argumentów (sprawdzane przez
$tag->parser->isEnd()
), Latte rzuci wyjątek, ponieważ wskazuje to, że wewnątrz tagu pozostały
nieoczekiwane tokeny. Odwrotnie, jeśli tag wymaga argumentów, powinieneś na początku swojej funkcji parsowania
wywołać $tag->expectArguments()
. Ta metoda sprawdza, czy argumenty są obecne, i rzuca pomocny wyjątek, jeśli
tag został użyty bez żadnych argumentów.
TagParser
oferuje przydatne metody do parsowania różnych rodzajów argumentów:
parseExpression(): ExpressionNode
: Parsuje wyrażenie podobne do PHP (zmienne, literały, operatory, wywołania funkcji/metod, itp.). Obsługuje cukier syntaktyczny Latte, taki jak traktowanie prostych ciągów alfanumerycznych jako ciągów w cudzysłowach (np.foo
jest parsowane, jakby było'foo'
).parseUnquotedStringOrExpression(): ExpressionNode
: Parsuje albo standardowe wyrażenie, albo niecytowany ciąg. Niecytowane ciągi to sekwencje dozwolone przez Latte bez cudzysłowów, często używane do rzeczy takich jak ścieżki plików (np.{include ../file.latte}
). Jeśli parsuje niecytowany ciąg, zwracaStringNode
.parseArguments(): ArrayNode
: Parsuje argumenty oddzielone przecinkami, potencjalnie z kluczami, jak10, name: 'John', true
.parseModifier(): ModifierNode
: Parsuje filtry jak|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Parsuje podpowiedzi typów PHP jakint
,?string
,array|Foo
.
Dla bardziej złożonych lub niższych poziomów potrzeb parsowania, możesz bezpośrednio interagować ze strumieniem tokenów przez
$tag->parser->stream
. Ten obiekt dostarcza metody do sprawdzania i przetwarzania pojedynczych tokenów:
$tag->parser->stream->is(...): bool
: Sprawdza, czy bieżący token odpowiada któremukolwiek z określonych typów (np.Token::Php_Variable
) lub wartościom literałowym (np.'as'
) bez jego konsumowania. 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/wartości tokenów, a bieżący token nie pasuje, rzucaCompileException
. Użyj tego, gdy oczekujesz określonego tokenu.$tag->parser->stream->tryConsume(...): ?Token
: Próbuje skonsumować bieżący token tylko jeśli pasuje do jednego z określonych typów/wartości. Jeśli pasuje, konsumuje token i zwraca go. Jeśli nie pasuje, pozostawia pozycję strumienia niezmienioną i zwracanull
. Użyj tego dla opcjonalnych tokenów lub gdy wybierasz między różnymi ścieżkami składniowymi.
Aktualizacja funkcji parsowania create()
Z tym zrozumieniem zmodyfikujmy metodę create()
w DatetimeNode
tak, aby parsowała opcjonalny
argument formatu za pomocą $tag->parser
.
<?php
namespace App\Latte;
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 publiczną właściwość do przechowywania sparsowanego węzła wyrażenia formatu
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Sprawdzamy, czy istnieją jakiekolwiek tokeny
if (!$tag->parser->isEnd()) {
// Parsujemy argument jako wyrażenie podobne do PHP za pomocą TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... metody print() i getIterator() zostaną zaktualizowane dalej ...
}
Dodaliśmy publiczną właściwość $format
. W create()
teraz używamy
$tag->parser->isEnd()
do sprawdzenia, czy istnieją argumenty. Jeśli tak,
$tag->parser->parseExpression()
przetwarza tokeny dla wyrażenia. Ponieważ TagParser
musi
przetworzyć wszystkie tokeny wejściowe, Latte automatycznie rzuci błąd, jeśli użytkownik napisze coś nieoczekiwanego po
wyrażeniu formatu (np. {datetime 'Y-m-d', unexpected}
).
Aktualizacja metody print()
Teraz zmodyfikujmy metodę print()
tak, aby używała sparsowanego wyrażenia formatu zapisanego w
$this->format
. Jeśli nie podano formatu ($this->format
jest null
), powinniśmy
użyć domyślnego ciągu formatującego, 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 wydrukuje reprezentację kodu PHP $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
Do zmiennej $formatNode
zapisujemy węzeł AST reprezentujący ciąg formatujący dla funkcji PHP
date()
. Używamy tutaj operatora koalescencji null (??
). Jeśli użytkownik podał argument w szablonie
(np. {datetime 'd.m.Y'}
), to właściwość $this->format
zawiera odpowiedni węzeł (w tym przypadku
StringNode
z wartością 'd.m.Y'
), i ten węzeł jest używany. Jeśli użytkownik nie podał
argumentu (napisał tylko {datetime}
), właściwość $this->format
jest null
,
i zamiast tego tworzymy nowy StringNode
z domyślnym formatem 'Y-m-d H:i:s'
. To zapewnia, że
$formatNode
zawsze zawiera prawidłowy węzeł AST dla formatu.
W masce 'echo date(%node) %line;'
użyto nowego symbolu zastępczego %node
, który mówi metodzie
format()
, aby wzięła pierwszy następujący argument (którym jest nasz $formatNode
), wywołała jego
metodę print()
(która zwróci jego reprezentację kodu PHP) i wstawiła wynik na pozycję symbolu
zastępczego.
Implementacja getIterator()
dla podwęzłów
Nasz DatetimeNode
ma teraz węzeł potomny: wyrażenie $format
. Musimy udostępnić ten
węzeł potomny przejściom kompilacji, dostarczając go w metodzie getIterator()
. Pamiętaj, aby dostarczyć
referencję (&
), aby umożliwić przejściom potencjalne zastąpienie węzła.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Dlaczego jest to kluczowe? Wyobraź sobie przejście Sandbox, które musi sprawdzić, czy argument $format
nie
zawiera zabronionego wywołania funkcji (np. {datetime dangerousFunction()}
). Jeśli getIterator()
nie
dostarczy $this->format
, przejście Sandbox nigdy nie zobaczyłoby wywołania dangerousFunction()
wewnątrz argumentu naszego tagu, co stworzyłoby potencjalną lukę bezpieczeństwa. Dostarczając go, umożliwiamy Sandboxowi
(i innym przejściom) sprawdzanie i potencjalne modyfikowanie węzła wyrażenia $format
.
Użycie ulepszonego tagu
Tag teraz poprawnie obsługuje opcjonalny argument:
Domyślny format: {datetime}
Własny format: {datetime 'd.m.Y'}
Użycie zmiennej: {datetime $userDateFormatPreference}
{* To spowodowałoby błąd po sparsowaniu 'd.m.Y', ponieważ ", foo" jest nieoczekiwane *}
{* {datetime 'd.m.Y', foo} *}
Następnie przyjrzymy się tworzeniu tagów parzystych, które przetwarzają zawartość między nimi.
Obsługa tagów parzystych
Dotychczas nasz tag {datetime}
był samozamykający (koncepcyjnie). Nie miał żadnej zawartości między
tagiem początkowym a końcowym. Wiele przydatnych tagów jednak pracuje z blokiem zawartości szablonu. Nazywa się je tagami
parzystymi. Przykłady obejmują {if}...{/if}
, {block}...{/block}
lub własny tag, który teraz
stworzymy: {debug}...{/debug}
.
Ten tag pozwoli nam zawrzeć w naszych szablonach informacje debugowania, które powinny być widoczne tylko podczas rozwoju.
Cel: Stworzyć tag parzysty {debug}
, którego zawartość jest renderowana tylko wtedy, gdy aktywna jest
specyficzna flaga “trybu deweloperskiego”.
Wprowadzenie providerów
Czasami Twoje tagi potrzebują dostępu do danych lub usług, które nie są przekazywane bezpośrednio jako parametry szablonu. Na przykład określenie, czy aplikacja jest w trybie deweloperskim, dostęp do obiektu użytkownika lub uzyskanie wartości konfiguracyjnych. Latte dostarcza mechanizm zwany providerami (Providers) do tego celu.
Providerzy są rejestrowani w Twoim rozszerzeniu
za pomocą metody getProviders()
. Ta metoda zwraca tablicę asocjacyjną, gdzie klucze są nazwami, pod którymi
providerzy będą dostępni w kodzie wykonawczym szablonu, a wartości są rzeczywistymi danymi lub obiektami.
Wewnątrz kodu PHP generowanego przez metodę print()
Twojego tagu, możesz uzyskać dostęp do tych providerów
za pośrednictwem specjalnej właściwości obiektu $this->global
. Ponieważ ta właściwość jest
współdzielona przez wszystkie rozszerzenia, dobrą praktyką jest prefikowanie nazw Twoich providerów, aby zapobiec
potencjalnym kolizjom nazw z kluczowymi providerami Latte lub providerami z innych rozszerzeń firm trzecich. Powszechną
konwencją jest używanie krótkiego, unikalnego prefiksu związanego z Twoim producentem lub nazwą rozszerzenia. Dla naszego
przykładu użyjemy prefiksu app
, a flaga trybu deweloperskiego będzie dostępna jako
$this->global->appDevMode
.
Słowo kluczowe yield
do parsowania zawartości
Jak mówimy parserowi Latte, aby przetworzył zawartość między {debug}
a {/debug}
? Tutaj
wchodzi w grę słowo kluczowe yield
.
Kiedy yield
jest używane w funkcji create()
, funkcja staje się generatorem PHP. Jego wykonywanie zostaje wstrzymane, a
kontrola wraca do głównego TemplateParser
. TemplateParser
następnie kontynuuje parsowanie zawartości
szablonu aż napotka odpowiadający tag zamykający ({/debug}
w naszym przypadku).
Gdy zostanie znaleziony tag zamykający, TemplateParser
wznawia wykonywanie naszej funkcji create()
bezpośrednio po instrukcji yield
. Wartość zwrócona przez instrukcję yield
to tablica
zawierająca dwa elementy:
AreaNode
reprezentujący sparsowaną zawartość między tagiem początkowym a końcowym.- Obiekt
Tag
reprezentujący tag zamykający (np.{/debug}
).
Stwórzmy klasę DebugNode
i jej metodę create
wykorzystującą yield
.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Publiczna właściwość do przechowywania sparsowanej wewnętrznej zawartości
public AreaNode $content;
/**
* Funkcja parsowania dla tagu parzystego {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // zauważ typ zwracany
{
$node = $tag->node = new self;
// Wstrzymaj parsowanie, uzyskaj wewnętrzną zawartość i tag końcowy, gdy zostanie znaleziony {/debug}
[$node->content, $endTag] = yield;
return $node;
}
// ... print() i getIterator() zostaną zaimplementowane dalej ...
}
Uwaga: $endTag
jest null
, jeśli tag jest używany jako n:atrybut, tj.
<div n:debug>...</div>
.
Implementacja print()
dla warunkowego renderowania
Metoda print()
teraz musi generować kod PHP, który w czasie wykonania sprawdzi providera appDevMode
i wykona kod dla wewnętrznej zawartości tylko wtedy, gdy flaga jest true.
public function print(PrintContext $context): string
{
// Wygeneruje instrukcję PHP 'if', która w czasie wykonania sprawdzi providera
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Jeśli jest w trybie deweloperskim, wypisze wewnętrzną zawartość
%node
}
XX,
$this->position, // Dla komentarza %line
$this->content, // Węzeł zawierający AST wewnętrznej zawartości
);
}
To jest proste. Używamy PrintContext::format()
do stworzenia standardowej instrukcji PHP if
.
Wewnątrz if
umieszczamy symbol zastępczy %node
dla $this->content
. Latte rekurencyjnie
wywoła $this->content->print($context)
do wygenerowania kodu PHP dla wewnętrznej części tagu, ale tylko
jeśli $this->global->appDevMode
zostanie ocenione w czasie wykonania jako true.
Implementacja getIterator()
dla zawartości
Podobnie jak w przypadku węzła argumentu w poprzednim przykładzie, nasz DebugNode
ma teraz węzeł potomny:
AreaNode $content
. Musimy go udostępnić, dostarczając go w getIterator()
:
public function &getIterator(): \Generator
{
// Dostarcza referencję do węzła zawartości
yield $this->content;
}
To umożliwia przejściom kompilacji zejście do zawartości naszego tagu {debug}
, co jest ważne, nawet jeśli
zawartość jest renderowana warunkowo. Na przykład Sandbox musi analizować zawartość niezależnie od tego, czy
appDevMode
jest true czy false.
Rejestracja i użycie
Zarejestruj tag i providera w swoim rozszerzeniu:
class MyLatteExtension extends Extension
{
// Zakładamy, że $isDevelopmentMode jest określone gdzieś (np. z konfiguracji)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Rejestracja nowego tagu
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Rejestracja providera
];
}
}
// Przy rejestracji rozszerzenia:
$isDev = true; // Określ to na podstawie środowiska Twojej aplikacji
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
I jego użycie w szablonie:
<p>Zwykła zawartość widoczna zawsze.</p>
{debug}
<div class="debug-panel">
ID aktualnego użytkownika: {$user->id}
Czas żądania: {=time()}
</div>
{/debug}
<p>Kolejna zwykła zawartość.</p>
Integracja n:atrybutów
Latte oferuje wygodny skrócony zapis dla wielu tagów parzystych: n:atrybuty. Jeśli masz tag parzysty jak {tag}...{/tag}
i chcesz, aby jego efekt zastosował się bezpośrednio do pojedynczego elementu HTML, często możesz zapisać go bardziej
zwięźle jako atrybut n:tag
na tym elemencie.
Dla większości standardowych tagów parzystych, które definiujesz (jak nasz {debug}
), Latte automatycznie
włączy odpowiadającą wersję atrybutu n:
. Podczas rejestracji nie musisz robić nic dodatkowego:
{* Standardowe użycie tagu parzystego *}
{debug}<div>Informacje do debugowania</div>{/debug}
{* Równoważne użycie z n:atrybutem *}
<div n:debug>Informacje do debugowania</div>
Obie wersje wyrenderują <div>
tylko jeśli $this->global->appDevMode
jest true. Prefiksy
inner-
i tag-
również działają zgodnie z oczekiwaniami.
Czasami logika Twojego tagu może potrzebować zachowywać się nieco inaczej w zależności od tego, czy jest używany jako
standardowy tag parzysty, czy jako n:atrybut, lub czy użyto prefiksu jak n:inner-tag
lub n:tag-tag
.
Obiekt Latte\Compiler\Tag
, przekazany do Twojej funkcji parsowania create()
, dostarcza te
informacje:
$tag->isNAttribute(): bool
: Zwracatrue
, jeśli tag jest parsowany jako n:atrybut$tag->prefix: ?string
: Zwraca prefiks użyty z n:atrybutem, co może byćnull
(nie jest n:atrybutem),Tag::PrefixNone
,Tag::PrefixInner
lubTag::PrefixTag
Teraz, gdy rozumiemy proste tagi, parsowanie argumentów, tagi parzyste, providerów i n:atrybuty, zajmijmy się bardziej
złożonym scenariuszem obejmującym tagi zagnieżdżone w innych tagach, wykorzystując nasz tag {debug}
jako punkt
wyjścia.
Tagi pośrednie
Niektóre tagi parzyste pozwalają lub nawet wymagają, aby inne tagi pojawiły się wewnątrz nich przed końcowym
tagiem zamykającym. Nazywa się je tagami pośrednimi. Klasyczne przykłady obejmują
{if}...{elseif}...{else}...{/if}
lub {switch}...{case}...{default}...{/switch}
.
Rozszerzmy nasz tag {debug}
o obsługę opcjonalnej klauzuli {else}
, która będzie renderowana, gdy
aplikacja nie jest w trybie deweloperskim.
Cel: Zmodyfikować {debug}
tak, aby obsługiwał opcjonalny tag pośredni {else}
. Końcowa
składnia powinna być {debug} ... {else} ... {/debug}
.
Parsowanie tagów pośrednich za pomocą yield
Już wiemy, że yield
wstrzymuje funkcję parsowania create()
i zwraca sparsowaną zawartość wraz
z tagiem końcowym. yield
jednak oferuje więcej kontroli: możesz mu dostarczyć tablicę nazw tagów
pośrednich. Kiedy parser napotka którykolwiek z tych określonych tagów na tym samym poziomie zagnieżdżenia (tj.
jako bezpośrednie dzieci tagu nadrzędnego, nie wewnątrz innych bloków lub tagów wewnątrz niego), również zatrzyma
parsowanie.
Kiedy parsowanie zatrzymuje się z powodu tagu pośredniego, zatrzymuje parsowanie zawartości, wznawia generator
create()
i przekazuje z powrotem częściowo sparsowaną zawartość oraz tag pośredni sam w sobie (zamiast
końcowego tagu zamykającego). Nasza funkcja create()
może następnie przetworzyć ten tag pośredni (np.
sparsować jego argumenty, jeśli jakieś miał) i ponownie użyć yield
do parsowania kolejnej części
zawartości aż do końcowego tagu zamykającego lub innego oczekiwanego tagu pośredniego.
Zmodyfikujmy DebugNode::create()
tak, aby oczekiwał {else}
:
<?php
namespace App\Latte;
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
{
// Zawartość dla części {debug}
public AreaNode $thenContent;
// Opcjonalna zawartość dla części {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield i oczekuj albo {/debug} albo {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Sprawdź, czy tag, przy którym się zatrzymaliśmy, był {else}
if ($nextTag?->name === 'else') {
// Yield ponownie, aby sparsować zawartość między {else} a {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() i getIterator() zostaną zaktualizowane dalej ...
}
Teraz yield ['else']
mówi Latte, aby zatrzymało parsowanie nie tylko dla {/debug}
, ale także dla
{else}
. Jeśli {else}
zostanie znaleziony, $nextTag
będzie zawierał obiekt
Tag
dla {else}
. Następnie ponownie używamy yield
bez argumentów, co oznacza, że teraz
oczekujemy tylko końcowego tagu {/debug}
, i zapisujemy wynik w $node->elseContent
. Jeśli
{else}
nie został znaleziony, $nextTag
byłby Tag
dla {/debug}
(lub
null
, jeśli używany jako n:atrybut), a $node->elseContent
pozostałby null
.
Implementacja print()
z {else}
Metoda print()
musi odzwierciedlać nową strukturę. Powinna generować instrukcję PHP if/else
opartą na providerze devMode
.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Kod dla gałęzi 'then' (zawartość {debug})
} else {
%node // Kod dla gałęzi 'else' (zawartość {else})
}
XX,
$this->position, // Numer wiersza dla warunku 'if'
$this->thenContent, // Pierwszy symbol zastępczy %node
$this->elseContent ?? new NopNode, // Drugi symbol zastępczy %node
);
}
To jest standardowa struktura PHP if/else
. Używamy %node
dwukrotnie; format()
zastępuje dostarczone węzły po kolei. Używamy ?? new NopNode
do uniknięcia błędów, jeśli
$this->elseContent
jest null
– NopNode
po prostu nic nie wydrukuje.
Implementacja getIterator()
dla obu zawartości
Teraz mamy potencjalnie dwa węzły potomne zawartości ($thenContent
i $elseContent
). Musimy
dostarczyć oba, jeśli istnieją:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Użycie ulepszonego tagu
Tag może być teraz używany z opcjonalną klauzulą {else}
:
{debug}
<p>Wyświetlanie informacji debugowania, ponieważ devMode jest WŁĄCZONY.</p>
{else}
<p>Informacje debugowania są ukryte, ponieważ devMode jest WYŁĄCZONY.</p>
{/debug}
Obsługa stanu i zagnieżdżania
Nasze poprzednie przykłady ({datetime}
, {debug}
) były stosunkowo bezstanowe w ramach swoich metod
print()
. Albo bezpośrednio wypisywały zawartość, albo przeprowadzały prostą kontrolę warunkową opartą na
globalnym providerze. Wiele tagów jednak musi zarządzać jakąś formą stanu podczas renderowania lub obejmuje
ewaluację wyrażeń użytkownika, które powinny być uruchamiane tylko raz ze względu na wydajność lub poprawność. Dalej
musimy rozważyć, co się stanie, gdy nasze własne tagi są zagnieżdżone.
Zilustrujmy te koncepcje, tworząc tag {repeat $count}...{/repeat}
. Ten tag będzie powtarzał swoją wewnętrzną
zawartość $count
-krotnie.
Cel: Zaimplementować {repeat $count}
, który powtarza swoją zawartość określoną liczbę razy.
Potrzeba tymczasowych i unikalnych zmiennych
Wyobraź sobie, że użytkownik napisze:
{repeat rand(1, 5)} Zawartość {/repeat}
Gdybyśmy naiwnie wygenerowali pętlę PHP for
w ten sposób w naszej metodzie print()
:
// Uproszczony, NIEPRAWIDŁOWY wygenerowany kod
for ($i = 0; $i < rand(1, 5); $i++) {
// wypisanie zawartości
}
To byłoby źle! Wyrażenie rand(1, 5)
byłoby ponownie ewaluowane przy każdej iteracji pętli, co
prowadziłoby do nieprzewidywalnej liczby powtórzeń. Musimy ewaluować wyrażenie $count
raz przed
rozpoczęciem pętli i zapisać jego wynik.
Wygenerujemy kod PHP, który najpierw ewaluuje wyrażenie liczby i zapisuje je w tymczasowej zmiennej wykonawczej. Aby
zapobiec kolizjom ze zmiennymi zdefiniowanymi przez użytkownika szablonu oraz wewnętrznymi zmiennymi Latte (jak
$ʟ_...
), użyjemy konwencji prefiksu $__
(podwójne podkreślenie) dla naszych tymczasowych
zmiennych.
Wygenerowany kod wyglądałby wtedy tak:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// wypisanie zawartości
}
Teraz rozważmy zagnieżdżanie:
{repeat $countA} {* Zewnętrzna pętla *}
{repeat $countB} {* Wewnętrzna pętla *}
...
{/repeat}
{/repeat}
Gdyby zarówno zewnętrzny, jak i wewnętrzny tag {repeat}
generował kod używający tych samych nazw
zmiennych tymczasowych (np. $__count
i $__i
), wewnętrzna pętla nadpisałaby zmienne zewnętrznej
pętli, co naruszyłoby logikę.
Musimy zapewnić, że zmienne tymczasowe generowane dla każdej instancji tagu {repeat}
są unikalne.
Osiągniemy to za pomocą PrintContext::generateId()
. Ta metoda zwraca unikalną liczbę całkowitą podczas fazy
kompilacji. Możemy dołączyć ten ID do nazw naszych zmiennych tymczasowych.
Więc zamiast $__count
będziemy generować $__count_1
dla pierwszego tagu repeat,
$__count_2
dla drugiego itd. Podobnie dla licznika pętli użyjemy $__i_1
, $__i_2
itd.
Implementacja RepeatNode
Stwórzmy klasę węzła.
<?php
namespace App\Latte;
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 parsowania dla {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // upewnia się, że $count jest podany
$node = $tag->node = new self;
// Parsuje wyrażenie liczby
$node->count = $tag->parser->parseExpression();
// Uzyskanie wewnętrznej zawartości
[$node->content] = yield;
return $node;
}
/**
* Generuje pętlę PHP 'for' z unikalnymi nazwami zmiennych.
*/
public function print(PrintContext $context): string
{
// Generowanie unikalnych nazw zmiennych
$id = $context->generateId();
$countVar = '$__count_' . $id; // np. $__count_1, $__count_2, itd.
$iteratorVar = '$__i_' . $id; // np. $__i_1, $__i_2, itd.
return $context->format(
<<<'XX'
// Ewaluacja wyrażenia liczby *raz* i zapisanie
%raw = (int) (%node);
// Pętla z użyciem zapisanej liczby i unikalnej zmiennej iteracyjnej
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Renderowanie wewnętrznej zawartości
}
XX,
$countVar, // %0 - Zmienna do zapisania liczby
$this->count, // %1 - Węzeł wyrażenia dla liczby
$iteratorVar, // %2 - Nazwa zmiennej iteracyjnej pętli
$this->position, // %3 - Komentarz z numerem wiersza dla samej pętli
$this->content // %4 - Węzeł wewnętrznej zawartości
);
}
/**
* Dostarcza węzły potomne (wyrażenie liczby i zawartość).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
Metoda create()
parsuje wymagane wyrażenie $count
za pomocą parseExpression()
.
Najpierw wywoływane jest $tag->expectArguments()
. Zapewnia to, że użytkownik podał coś po
{repeat}
. Chociaż $tag->parser->parseExpression()
zawiodłoby, gdyby nic nie zostało podane,
komunikat o błędzie mógłby dotyczyć nieoczekiwanej składni. Użycie expectArguments()
dostarcza znacznie
jaśniejszy błąd, konkretnie stwierdzający, że brakuje argumentów dla tagu {repeat}
.
Metoda print()
generuje kod PHP odpowiedzialny za wykonywanie logiki powtarzania w czasie wykonania. Zaczyna od
generowania unikalnych nazw dla tymczasowych zmiennych PHP, których będzie potrzebować.
Metoda $context->format()
jest wywoływana z nowym symbolem zastępczym %raw
, który wstawia
surowy ciąg dostarczony jako odpowiadający argument. Tutaj wstawia unikalną nazwę zmiennej zapisaną w
$countVar
(np. $__count_1
). A co z %0.raw
i %2.raw
? To demonstruje
pozycyjne symbole zastępcze. Zamiast zwykłego %raw
, który bierze następny dostępny surowy
argument, %2.raw
jawnie bierze argument o indeksie 2 (którym jest $iteratorVar
) i wstawia jego
surową wartość ciągu. Pozwala nam to ponownie użyć ciągu $iteratorVar
bez wielokrotnego przekazywania go na
liście argumentów dla format()
.
To starannie skonstruowane wywołanie format()
generuje wydajną i bezpieczną pętlę PHP, która poprawnie
obsługuje wyrażenie liczby i unika kolizji nazw zmiennych, nawet gdy tagi {repeat}
są zagnieżdżone.
Rejestracja i użycie
Zarejestruj tag w swoim rozszerzeniu:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Rejestracja tagu repeat
];
}
}
Użyj go w szablonie, w tym zagnieżdżania:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Wewnętrzna pętla</td>
{/repeat}
</tr>
{/repeat}
Ten przykład demonstruje, jak obsługiwać stan (liczniki pętli) i potencjalne problemy z zagnieżdżaniem za pomocą
zmiennych tymczasowych z prefiksem $__
i unikalnych z ID od PrintContext::generateId()
.
Czyste n:atrybuty
Podczas gdy wiele n:atrybutów
jak n:if
lub n:foreach
służy jako wygodne skróty dla
ich odpowiedników w tagach parzystych ({if}...{/if}
, {foreach}...{/foreach}
), Latte pozwala również
definiować tagi, które istnieją tylko w formie n:atrybutu. Są one często używane do modyfikowania atrybutów lub
zachowania elementu HTML, do którego są dołączone.
Standardowe przykłady wbudowane w Latte obejmują n:class
, który pomaga dynamicznie budować atrybut
class
, oraz n:attr
, który może ustawić wiele
dowolnych atrybutów.
Stwórzmy własny czysty n:atrybut: n:confirm
, który doda dialog potwierdzenia JavaScript przed wykonaniem akcji
(jak podążenie za linkiem lub wysłanie formularza).
Cel: Zaimplementować n:confirm="'Jesteś pewien?'"
, który doda obsługę zdarzenia onclick
do zapobiegania domyślnej akcji, jeśli użytkownik anuluje dialog potwierdzenia.
Implementacja ConfirmNode
Potrzebujemy klasy Node i funkcji parsowania.
<?php
namespace App\Latte;
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 dla kontekstów JavaScript i atrybutu HTML.
return $context->format(
<<<'XX'
echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
Metoda print()
generuje kod PHP, który ostatecznie podczas renderowania szablonu wypisze atrybut HTML
onclick="..."
. Obsługa zagnieżdżonych kontekstów (JavaScript wewnątrz atrybutu HTML) wymaga starannego
escapowania. Filtr LR\Filters::escapeJs(%node)
jest wywoływany w czasie wykonania i escapuje wiadomość poprawnie
do użycia wewnątrz JavaScriptu (wyjście byłoby jak "Sure?"
). Następnie filtr
LR\Filters::escapeHtmlAttr(...)
escapuje znaki, które są specjalne w atrybutach HTML, więc zmieniłoby to wyjście
na return confirm("Sure?")
. To dwustopniowe escapowanie w czasie wykonania zapewnia, że
wiadomość jest bezpieczna dla JavaScriptu, a wynikowy kod JavaScript jest bezpieczny do wstawienia do atrybutu 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(...), // Rejestracja n:confirm
];
}
}
Teraz możesz użyć n:confirm
na linkach, przyciskach lub 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>
Kiedy użytkownik kliknie na link, przeglądarka wykona kod onclick
, wyświetli dialog potwierdzenia i przejdzie
do delete.php
tylko wtedy, gdy użytkownik kliknie “OK”.
Ten przykład demonstruje, jak można stworzyć czysty n:atrybut do modyfikowania zachowania lub atrybutów swojego
hostującego elementu HTML, generując odpowiedni kod PHP w jego metodzie print()
. Pamiętaj o podwójnym
escapowaniu, które jest często wymagane: raz dla kontekstu docelowego (JavaScript w tym przypadku) i ponownie dla kontekstu
atrybutu HTML.
Tematy zaawansowane
Podczas gdy poprzednie sekcje obejmują podstawowe koncepcje, oto kilka bardziej zaawansowanych tematów, na które możesz natknąć się podczas tworzenia własnych tagów Latte.
Tryby wyjścia tagów
Obiekt Tag
przekazany do Twojej funkcji create()
ma właściwość outputMode
. Ta
właściwość wpływa na to, jak Latte traktuje otaczające białe znaki i wcięcia, szczególnie gdy tag jest używany na
własnej linii. Możesz zmodyfikować tę właściwość w swojej funkcji create()
.
Tag::OutputKeepIndentation
(Domyślny dla większości tagów jak{=...}
): Latte stara się zachować wcięcie przed tagiem. Nowe linie po tagu są generalnie zachowywane. Jest to odpowiednie dla tagów, które wypisują zawartość w linii.Tag::OutputRemoveIndentation
(Domyślny dla tagów blokowych jak{if}
,{foreach}
): Latte usuwa początkowe wcięcie i potencjalnie jedną następującą nową linię. Pomaga to utrzymać wygenerowany kod PHP czystszym i zapobiega dodatkowym pustym liniom w wyjściu HTML spowodowanym przez sam tag. Użyj tego dla tagów, które reprezentują struktury sterujące lub bloki, które same nie powinny dodawać białych znaków.Tag::OutputNone
(Używany przez tagi jak{var}
,{default}
): Podobny doRemoveIndentation
, ale sygnalizuje silniej, że sam tag nie produkuje bezpośredniego wyjścia, potencjalnie wpływając na przetwarzanie białych znaków wokół niego jeszcze bardziej agresywnie. Odpowiedni dla tagów deklaracyjnych lub ustawiających.
Wybierz tryb, który najlepiej pasuje do celu Twojego tagu. Dla większości tagów strukturalnych lub sterujących zazwyczaj
odpowiedni jest OutputRemoveIndentation
.
Dostęp do tagów nadrzędnych/najbliższych
Czasami zachowanie tagu musi zależeć od kontekstu, w którym jest używany, konkretnie w którym tagu(tagach) nadrzędnym
się znajduje. Obiekt Tag
przekazany do Twojej funkcji create()
dostarcza metodę
closestTag(array $classes, ?callable $condition = null): ?Tag
dokładnie do tego celu.
Ta metoda przeszukuje w górę hierarchię aktualnie otwartych tagów (w tym elementów HTML reprezentowanych wewnętrznie
podczas parsowania) i zwraca obiekt Tag
najbliższego przodka, który odpowiada określonym kryteriom. Jeśli nie
zostanie znaleziony żaden pasujący przodek, zwraca null
.
Tablica $classes
określa, jakiego rodzaju tagów przodków szukasz. Sprawdza, czy powiązany węzeł tagu przodka
($ancestorTag->node
) jest instancją tej klasy.
function create(Tag $tag)
{
// Szukanie najbliższego tagu przodka, którego węzeł jest instancją ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Możemy uzyskać dostęp do samej instancji ForeachNode:
$foreachNode = $foreachTag->node;
}
}
Zauważ $foreachTag->node
: Działa to tylko dlatego, że jest konwencją w rozwoju tagów Latte natychmiastowe
przypisanie utworzonego węzła do $tag->node
w ramach metody create()
, jak zawsze robiliśmy.
Czasami samo porównanie typu węzła nie wystarcza. Możesz potrzebować sprawdzić specyficzną właściwość potencjalnego
tagu przodka lub jego węzła. Opcjonalny drugi argument dla closestTag()
to funkcja typu callable, która przyjmuje
potencjalny obiekt Tag
przodka i powinna zwracać, czy jest to prawidłowe 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, które są świadome kontekstu i wymuszają poprawne użycie w ramach
struktury Twojego szablonu, co prowadzi do bardziej solidnych i zrozumiałych szablonów.
Symbole zastępcze PrintContext::format()
Często używaliśmy PrintContext::format()
do generowania kodu PHP w metodach print()
naszych
węzłów. Przyjmuje ona ciąg maski i następujące argumenty, które zastępują symbole zastępcze w masce. Oto podsumowanie
dostępnych symboli zastępczych:
%node
: Argument musi być instancjąNode
. Wywołuje metodęprint()
węzła i wstawia wynikowy ciąg kodu PHP.%dump
: Argument jest dowolną wartością PHP. Eksportuje wartość do prawidłowego kodu PHP. Odpowiedni dla 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 wyjściowego kodu PHP bez żadnego escapowania ani modyfikacji. Używaj z ostrożnością, głównie do wstawiania wstępnie wygenerowanych fragmentów kodu PHP lub nazw zmiennych.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: Argument musi byćExpression\ArrayNode
. Wypisuje elementy tablicy sformatowane jako argumenty do wywołania funkcji lub metody (oddzielone przecinkami, obsługuje nazwane argumenty, jeśli są obecne).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: Argument musi być obiektemPosition
(zazwyczaj$this->position
). Wstawia komentarz PHP/* line X */
wskazujący numer wiersza źródła.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Generuje kod PHP, który w czasie wykonania escapuje wewnętrzne wyrażenie za pomocą bieżących, świadomych kontekstu reguł escapowania.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: Argument musi byćModifierNode
. Generuje kod PHP, który stosuje filtry określone wModifierNode
do wewnętrznej zawartości, w tym świadome kontekstu escapowanie, jeśli nie jest wyłączone za pomocą|noescape
.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Podobny do%modify
, ale przeznaczony do modyfikowania bloków przechwyconej zawartości (często HTML).
Możesz jawnie odwoływać się do argumentów według ich indeksu (od zera): %0.node
, %1.dump
,
%2.raw
, itd. Pozwala to ponownie użyć argumentu kilka razy 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
Podczas gdy parseExpression()
, parseArguments()
, itp., obejmują wiele przypadków, czasami
potrzebujesz bardziej złożonej logiki parsowania używającej niższego poziomu TokenStream
dostępnego przez
$tag->parser->stream
.
Cel: Stworzyć tag {embedYoutube $videoID, width: 640, height: 480}
. Chcemy sparsować wymagane ID wideo
(ciąg lub zmienną) poprzedzone opcjonalnymi parami klucz-wartość dla wymiarów.
<?php
namespace App\Latte;
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;
// Parsowanie wymaganego ID wideo
$node->videoId = $tag->parser->parseExpression();
// Parsowanie opcjonalnych par klucz-wartość
$stream = $tag->parser->stream; // Uzyskanie strumienia tokenów
while ($stream->tryConsume(',')) { // Wymaga oddzielenia przecinkiem
// Oczekiwanie identyfikatora 'width' lub 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Oczekiwanie separatora dwukropka
$value = $tag->parser->parseExpression(); // Parsowanie wyrażenia wartości
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Nieznany argument '$key'. Oczekiwano 'width' lub 'height'.", $keyToken->position);
}
}
return $node;
}
}
Ten poziom kontroli pozwala definiować bardzo specyficzne i złożone składnie dla Twoich własnych tagów poprzez bezpośrednią interakcję ze strumieniem tokenów.
Użycie AuxiliaryNode
Latte dostarcza ogólne “pomocnicze” węzły dla specjalnych sytuacji podczas generowania kodu lub w ramach przejść
kompilacji. Są to AuxiliaryNode
i Php\Expression\AuxiliaryNode
.
Uważaj AuxiliaryNode
za elastyczny węzeł kontenerowy, który deleguje swoje podstawowe funkcjonalności –
generowanie kodu i udostępnianie węzłów potomnych – argumentom dostarczonym w jego konstruktorze:
- Delegacja
print()
: Pierwszy argument konstruktora to closure PHP. Kiedy Latte wywołuje metodęprint()
naAuxiliaryNode
, uruchamia tę dostarczoną closure. Closure przyjmujePrintContext
i wszelkie węzły przekazane w drugim argumencie konstruktora, co pozwala definiować całkowicie własną logikę generowania kodu PHP w czasie wykonania. - Delegacja
getIterator()
: Drugi argument konstruktora to tablica obiektówNode
. Kiedy Latte potrzebuje przejść przez dzieciAuxiliaryNode
(np. podczas przejść kompilacji), jego metodagetIterator()
po prostu dostarcza węzły wymienione w tej tablicy.
Przykład:
$node = new AuxiliaryNode(
// 1. Ta closure staje się ciałem print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Te węzły są dostarczane przez metodę getIterator() i przekazywane do closure powyżej
[$argumentNode1, $argumentNode2]
);
Latte dostarcza dwa odrębne typy w zależności od tego, gdzie potrzebujesz wstawić wygenerowany kod:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Użyj tego, gdy potrzebujesz wygenerować fragment kodu PHP, który reprezentuje wyrażenieLatte\Compiler\Nodes\AuxiliaryNode
: Użyj tego do bardziej ogólnych celów, gdy potrzebujesz wstawić blok kodu PHP reprezentujący jedną lub więcej instrukcji
Ważnym powodem użycia AuxiliaryNode
zamiast standardowych węzłów (jak StaticMethodCallNode
) w
ramach Twojej metody print()
lub przejścia kompilacji jest kontrola widoczności dla kolejnych przejść
kompilacji, szczególnie tych związanych z bezpieczeństwem, jak Sandbox.
Rozważ scenariusz: Twoje przejście kompilacji musi opakować wyrażenie dostarczone przez użytkownika
($userExpr
) wywołaniem specyficznej, zaufanej funkcji pomocniczej myInternalSanitize($userExpr)
. Jeśli
utworzysz standardowy węzeł new FunctionCallNode('myInternalSanitize', [$userExpr])
, będzie on w pełni widoczny
dla przejścia AST. Jeśli przejście Sandbox działa później i myInternalSanitize
nie jest na jego
liście dozwolonych, Sandbox może zablokować lub zmodyfikować to wywołanie, potencjalnie naruszając wewnętrzną
logikę Twojego tagu, nawet jeśli Ty, autor tagu, wiesz, że to konkretne wywołanie jest bezpieczne i niezbędne.
Możesz więc generować wywołanie bezpośrednio w ramach closure AuxiliaryNode
.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... wewnątrz print() lub przejścia kompilacji ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Bezpośrednie generowanie kodu PHP
$userExpr,
),
// WAŻNE: Nadal przekaż oryginalny węzeł wyrażenia użytkownika tutaj!
[$userExpr],
);
W tym przypadku przejście Sandbox widzi AuxiliaryNode
, ale nie analizuje kodu PHP generowanego przez jego
closure. Nie może bezpośrednio zablokować wywołania myInternalSanitize
generowanego wewnątrz
closure.
Podczas gdy sam wygenerowany kod PHP jest ukryty przed przejściami, wejścia do tego kodu (węzły reprezentujące dane
lub wyrażenia użytkownika) muszą być nadal możliwe do przejścia. Dlatego drugi argument konstruktora
AuxiliaryNode
jest kluczowy. Musisz przekazać tablicę zawierającą wszystkie oryginalne węzły (jak
$userExpr
w przykładzie powyżej), których używa Twoja closure. getIterator()
AuxiliaryNode
dostarczy te węzły, umożliwiając przejściom kompilacji, takim jak Sandbox, analizowanie ich
pod kątem potencjalnych problemów.
Dobre praktyki
- Jasny cel: Upewnij się, że Twój tag ma jasny i niezbędny cel. Nie twórz tagów do zadań, które można łatwo rozwiązać za pomocą filtrów lub funkcji.
- Poprawnie zaimplementuj
getIterator()
: Zawsze implementujgetIterator()
i dostarczaj referencje (&
) do wszystkich węzłów potomnych (argumentów, zawartości), które zostały sparsowane z szablonu. Jest to niezbędne dla przejść kompilacji, bezpieczeństwa (Sandbox) i potencjalnych przyszłych optymalizacji. - Publiczne właściwości dla węzłów: Właściwości zawierające węzły potomne czyń publicznymi, aby przejścia kompilacji mogły je w razie potrzeby modyfikować.
- Używaj
PrintContext::format()
: Wykorzystuj metodęformat()
do generowania kodu PHP. Obsługuje cudzysłowy, poprawnie escapuje symbole zastępcze i automatycznie dodaje komentarze z numerem wiersza. - Zmienne tymczasowe (
$__
): Podczas generowania kodu PHP wykonawczego, który potrzebuje zmiennych tymczasowych (np. do przechowywania 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żony lub potrzebuje stanu specyficznego dla
instancji w czasie wykonania, użyj
$context->generateId()
w ramach swojej metodyprint()
, aby utworzyć unikalne sufiksy dla Twoich zmiennych tymczasowych$__
. - Providerzy dla danych zewnętrznych: Używaj providerów (rejestrowanych przez
Extension::getProviders()
) do dostępu do danych lub usług wykonawczych ($this->global->…) zamiast hardkodowania wartości lub polegania na stanie globalnym. Używaj prefiksów producenta dla nazw providerów. - Rozważ n:atrybuty: Jeśli Twój tag parzysty logicznie operuje na jednym elemencie HTML, Latte prawdopodobnie zapewnia
automatyczne wsparcie
n:atrybutu
. Miej to na uwadze dla wygody użytkownika. Jeśli tworzysz tag modyfikujący atrybut, rozważ, czy czystyn:atrybut
jest najodpowiedniejszą formą. - Testowanie: Pisz testy dla swoich tagów, obejmujące zarówno parsowanie różnych wejść składniowych, jak i poprawność wyjścia generowanego kodu PHP.
Przestrzegając tych wytycznych, możesz tworzyć potężne, solidne i łatwe w utrzymaniu własne tagi, które bezproblemowo integrują się z silnikiem szablonów Latte.
Studiowanie klas węzłów, które są częścią Latte, jest najlepszym sposobem na nauczenie się wszystkich szczegółów procesu parsowania.