Tworzenie rozszerzenia
Rozszerzenie to klasa wielokrotnego użytku, która może definiować niestandardowe tagi, filtry, funkcje, dostawców itp.
Tworzymy rozszerzenia, gdy chcemy ponownie użyć naszych dostosowań Latte w różnych projektach lub podzielić się nimi z innymi. Warto również utworzyć rozszerzenie dla każdego projektu internetowego, które będzie zawierało wszystkie konkretne tagi i filtry, które chcesz użyć w szablonach projektu.
Klasa rozszerzona
Extension jest klasą dziedziczącą po Latte\Extension.
Jest ona rejestrowana do Latte za pomocą addExtension()
(lub pliku konfiguracyjnego):
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
Jeśli zarejestrujesz wiele rozszerzeń i zdefiniują one identycznie nazwane tagi, filtry lub funkcje, wygrywa ostatnio dodane rozszerzenie. Oznacza to również, że twoje rozszerzenia mogą nadpisać natywne tagi / filtry / funkcje.
Za każdym razem, gdy dokonasz zmiany w klasie i autoodświeżanie nie jest wyłączone, Latte automatycznie przekompiluje twoje szablony.
Klasa może implementować dowolną z poniższych metod:
abstract class Extension
{
/**
* Inicializace před kompilací šablony.
*/
public function beforeCompile(Engine $engine): void;
/**
* Vrací seznam parserů pro značky Latte.
* @return array<string, callable>
*/
public function getTags(): array;
/**
* Vrací seznam průchodů kompilátoru.
* @return array<string, callable>
*/
public function getPasses(): array;
/**
* Vrací seznam |filtrů.
* @return array<string, callable>
*/
public function getFilters(): array;
/**
* Vrací seznam funkcí použitých v šablonách.
* @return array<string, callable>
*/
public function getFunctions(): array;
/**
* Vrací seznam providerů.
* @return array<mixed>
*/
public function getProviders(): array;
/**
* Vrací hodnotu pro rozlišení více verzí šablony.
*/
public function getCacheKey(Engine $engine): mixed;
/**
* Inicializace před vykreslením šablony.
*/
public function beforeRender(Template $template): void;
}
Aby uzyskać pomysł, jak wygląda rozszerzenie, zobacz wbudowany CoreExtension.
beforeCompile(Latte\Engine $engine): void
Jest on wywoływany przed kompilacją szablonu. Metoda ta może być używana na przykład do inicjalizacji związanych z kompilacją.
getTags(): array
Wywoływany podczas kompilacji szablonu. Zwraca tablicę asocjacyjną nazwa tagu⇒ callable, które są funkcjami parsowania tagów.
public function getTags(): array
{
return [
'foo' => [FooNode::class, 'create'],
'bar' => [BarNode::class, 'create'],
'n:baz' => [NBazNode::class, 'create'],
// ...
];
}
Znacznik n:baz
reprezentuje czysty n:atrybut, czyli jest to znacznik, który może być zapisany tylko jako
atrybut.
W przypadku znaczników foo
i bar
, Latte automatycznie rozpozna, czy są one sparowane, a jeśli tak,
to automatycznie zapisze je z użyciem n:attributes, w tym warianty z przedrostkami n:inner-foo
i
n:tag-foo
.
O kolejności wykonania takich n:atrybutów decyduje ich kolejność w polu zwracanym przez getTags()
. Tak więc,
n:foo
jest zawsze wykonywany przed n:bar
, nawet jeśli atrybuty w znaczniku HTML są wymienione w
odwrotnej kolejności jako <div n:bar="..." n:foo="...">
.
Jeśli musisz określić kolejność n:atrybutów w wielu rozszerzeniach, użyj metody pomocniczej order()
, gdzie
parametr before
lub after
określa przed lub po jakich znacznikach znacznik jest uporządkowany.
public function getTags(): array
{
return [
'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
];
}
getPasses(): array
Wywoływany podczas kompilacji szablonu. Zwraca tablicę asocjacyjną name pass ⇒ callable, która jest funkcją reprezentującą tzw. przejścia kompilatora, które przemierzają i modyfikują AST.
Również w tym przypadku można zastosować metodę pomocniczą order()
. Wartość parametrów
before
lub after
może być '*'
ze znaczeniem before/after all.
public function getPasses(): array
{
return [
'optimize' => [Passes::class, 'optimizePass'],
'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
// ...
];
}
beforeRender(Latte\Engine $engine): void
Jest on wywoływany przed każdym renderowaniem szablonu. Metoda może być wykorzystana np. do inicjalizacji zmiennych używanych podczas renderowania.
getFilters(): array
Jest on wywoływany przed wyrenderowaniem szablonu. Zwraca filtry jako tablicę asocjacyjną nazwa filtra ⇒ callable.
public function getFilters(): array
{
return [
'batch' => [$this, 'batchFilter'],
'trim' => [$this, 'trimFilter'],
// ...
];
}
getFunctions(): array
Wywoływany przed wyrenderowaniem szablonu. Zwraca funkcję jako tablicę asocjacyjną nazwa funkcji ⇒ callable.
public function getFunctions(): array
{
return [
'clamp' => [$this, 'clampFunction'],
'divisibleBy' => [$this, 'divisibleByFunction'],
// ...
];
}
getProviders(): array
Wywoływany przed wyrenderowaniem szablonu. Zwraca tablicę dostawców, które są zwykle obiektami, które używają tagów w
czasie rzeczywistym. Dostęp do nich uzyskuje się poprzez stronę $this->global->...
.
public function getProviders(): array
{
return [
'myFoo' => $this->foo,
'myBar' => $this->bar,
// ...
];
}
getCacheKey(Latte\Engine $engine): mixed
Jest on wywoływany przed wyrenderowaniem szablonu. Zwracana wartość staje się częścią klucza, którego hash zawarty jest w nazwie skompilowanego pliku szablonu. Tak więc dla różnych wartości zwrotnych Latte wygeneruje różne pliki pamięci podręcznej.
Jak działa Latte?
Aby zrozumieć, jak zdefiniować niestandardowe tagi lub przejścia kompilatora, konieczne jest zrozumienie, jak Latte działa pod maską.
Kompilacja szablonów w Latte jest uproszczona w następujący sposób:
- Najpierw lexer tokenizuje kod źródłowy szablonu na małe części (tokeny) dla łatwiejszego przetwarzania.
- Następnie parser przekształca strumień tokenów w sensowne drzewo węzłów (abstrakcyjne drzewo składniowe, AST).
- Na koniec kompilator generuje klasę PHP z AST, która renderuje szablon i buforuje go.
W rzeczywistości kompilacja jest nieco bardziej skomplikowana. Latte posiada dwa lexery i parsery: jeden dla szablonu HTML, a drugi dla kodu podobnego do PHP wewnątrz znaczników. Również parsowanie nie jest uruchamiane po tokenizacji, ale lexer i parser działają równolegle w dwóch „wątkach“ i koordynują. To rocket science :-)
Ponadto wszystkie tagi mają swoje własne procedury parsowania. Gdy parser napotka znacznik, wywołuje swoją funkcję parsującą (zwraca ona Extension::getTags()). Ich zadaniem jest parsowanie argumentów znaczników oraz, w przypadku znaczników sparowanych, wewnętrznej treści. Zwraca węzeł, który staje się częścią AST. Zobacz sekcję Funkcje parsowania znaczników, aby uzyskać szczegółowe informacje.
Kiedy parser zakończy swoją pracę, mamy kompletny AST reprezentujący szablon. Węzeł główny to
Latte\Compiler\Nodes\TemplateNode
. Poszczególne węzły wewnątrz drzewa reprezentują więc nie tylko znaczniki,
ale także elementy HTML, ich atrybuty, wszelkie wyrażenia użyte wewnątrz znaczników itd.
Następnie przychodzą tzw. Compiler Passes, czyli funkcje (zwracane przez Extension::getPasses()), które modyfikują AST.
Cały proces, od ładowania zawartości szablonu, przez parsowanie, po generowanie ostatecznego pliku, może być sekwencjonowany za pomocą tego kodu, z którym możesz eksperymentować i zrzucać pośrednie kroki:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Przykład AST
Aby lepiej zapoznać się z formą AST, dodajemy próbkę. To jest szablon źródłowy:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
I to jest jego reprezentacja w postaci AST:
Latte\Compiler\Nodes\TemplateNode( Latte\Compiler\Nodes\FragmentNode( - Latte\Essential\Nodes\ForeachNode( expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') ) value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') content: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode(' ') - Latte\Compiler\Nodes\Html\ElementNode('li')( content: Latte\Essential\Nodes\PrintNode( expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') name: Latte\Compiler\Nodes\Php\IdentifierNode('name') ) modifier: Latte\Compiler\Nodes\Php\ModifierNode( filters: - Latte\Compiler\Nodes\Php\FilterNode('upper') ) ) ) ) else: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode('no items found') ) ) ) )
Tagi własne
Do zdefiniowania nowego znacznika wymagane są trzy kroki:
- Definiowanie funkcji parsowania tagów (odpowiedzialnej za parsowanie tagu do węzła).
- stworzenie klasy node (odpowiedzialnej za generowanie kodu PHP i przemierzanie AST)
- rejestrowanie tagu za pomocą Extension::getTags()
Funkcja parsowania znaczników
Parsowanie tagów jest obsługiwane przez funkcję parsującą (tę zwróconą przez Extension::getTags()). Jej zadaniem jest parsowanie i sprawdzenie, czy wewnątrz tagu nie ma żadnych
argumentów (wykorzystuje do tego TagParser). Ponadto, jeśli tag jest parą, poprosi TemplateParser o parsowanie i zwrócenie
wewnętrznej zawartości. Funkcja tworzy i zwraca węzeł, który zwykle jest dzieckiem
Latte\Compiler\Nodes\StatementNode
, a ten staje się częścią AST.
Tworzymy klasę dla każdego węzła, co teraz zrobimy, i zgrabnie umieszczamy w niej funkcję parsowania jako statyczną
fabrykę. Jako przykład spróbujmy stworzyć znany nam już znacznik {foreach}
:
use Latte\Compiler\Nodes\StatementNode;
class ForeachNode extends StatementNode
{
// funkcja parsowania, która na razie tworzy tylko węzeł
public static function create(Latte\Compiler\Tag $tag): self
{
$node = $tag->node = new self;
return $node;
}
public function print(Latte\Compiler\PrintContext $context): string
{
// kod, który zostanie dodany później
}
public function &getIterator(): \Generator
{
// kod, który zostanie dodany później
}
}
Funkcji parsującej create()
przekazywany jest obiekt Latte\Compiler\Tag, który przenosi podstawowe informacje
o znaczniku (czy jest to klasyczny znacznik, czy n:atrybut, w jakiej linii się znajduje itp.), a przede wszystkim udostępnia Latte\Compiler\TagParser w
$tag->parser
.
Jeśli znacznik musi mieć argumenty, sprawdzamy ich istnienie wywołując $tag->expectArguments()
. Do ich
parsowania dostępne są metody obiektu $tag->parser
:
parseExpression(): ExpressionNode
dla wyrażenia podobnego do PHP (np.10 + 3
)parseUnquotedStringOrExpression(): ExpressionNode
dla wyrażenia lub unquoted-string.parseArguments(): ArrayNode
dla zawartości tablicy (np.10, true, foo => bar
)parseModifier(): ModifierNode
dla modyfikatora (np.|upper|truncate:10
)parseType(): ExpressionNode
dla typehint (np.int|string
lubFoo\Bar[]
)
a następnie niskopoziomowy Latte\Compiler\TokenStream działający bezpośrednio na tokenach:
$tag->parser->stream->consume(...): Token
$tag->parser->stream->tryConsume(...): ?Token
Latte rozszerza składnię PHP w drobny sposób, na przykład dodając modyfikatory, skrócone operatory trójskładnikowe lub
pozwalając na pisanie prostych ciągów alfanumerycznych bez cudzysłowów. Dlatego właśnie używamy określenia
PHP-like zamiast PHP. W ten sposób metoda parseExpression()
parsuje np. foo
jako
'foo'
. Ponadto unquoted-string jest specjalnym przypadkiem ciągu znaków, który również nie musi być
cytowany, ale jednocześnie nie musi być alfanumeryczny. Na przykład ścieżka do pliku w znaczniku
{include ../file.latte}
. Do jego parsowania używana jest metoda parseUnquotedStringOrExpression()
.
Studiowanie klas węzłów, które są częścią Latte, jest najlepszym sposobem na poznanie wszystkich szczegółów procesu parsowania.
Wróćmy do znacznika {foreach}
. W nim oczekujemy argumentów o postaci
výraz + 'as' + druhý výraz
i parsujemy je w następujący sposób:
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;
class ForeachNode extends StatementNode
{
public ExpressionNode $expression;
public ExpressionNode $value;
public static function create(Latte\Compiler\Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->expression = $tag->parser->parseExpression();
$tag->parser->stream->consume('as');
$node->value = $parser->parseExpression();
return $node;
}
}
Wyrażenia, które wpisaliśmy do zmiennych $expression
i $value
, reprezentują węzły
podrzędne.
Definiuj zmienne z węzłami podrzędnymi jako publiczne, aby można je było modyfikować w kolejnych krokach przetwarzania. Jednocześnie muszą być one udostępnione do przeglądania.
Dla sparowanych tagów, takich jak nasze, metoda musi nadal pozwalać TemplateParser parsować wnętrze tagu. Zajmuje się tym
yield
, który zwraca parę [zawartość wewnętrzna, tag końcowy]. Zawartość wewnętrzną przechowujemy w zmiennej
$node->content
.
public AreaNode $content;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $endTag] = yield;
return $node;
}
Słowo kluczowe yield
powoduje przerwanie metody create()
, kierując ją z powrotem do
TemplateParser, który kontynuuje parsowanie treści aż do trafienia na znacznik end. Następnie przekazuje kontrolę z powrotem
do create()
, która kontynuuje od miejsca, w którym się skończyła. Użycie metody yield
automatycznie zwraca Generator
.
Możesz również przekazać tablicę nazw tagów do yield
, aby zatrzymać przetwarzanie, jeśli wystąpią one
przed tagiem końcowym. Dzięki temu będziemy mogli zaimplementować konstrukcję {foreach}...{else}...{/foreach}
.
Jeśli pojawia się {else}
, to treść po nim parsujemy na $node->elseContent
:
public AreaNode $content;
public ?AreaNode $elseContent = null;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $nextTag] = yield ['else'];
if ($nextTag?->name === 'else') {
[$node->elseContent] = yield;
}
return $node;
}
Zwrócenie węzła kończy parsowanie znacznika.
Generowanie kodu PHP
Każdy węzeł musi implementować metodę print()
. Zwraca ona kod PHP renderujący podany fragment szablonu (kod
runtime). Jako parametr przekazywany jest obiekt Latte\Compiler\PrintContext, który posiada przydatną
metodę format()
upraszczającą kompilację kodu wynikowego.
Metoda format(string $mask, ...$args)
akceptuje w masce następujące placeholdery:
%node
wymienia węzeł%dump
eksportuje wartość do PHP%raw
wstawia tekst bezpośrednio bez żadnych przekształceń%args
wypisuje ArrayNode jako argumenty do wywołania funkcji%line
wypisuje komentarz z numerem linii%escape(...)
ucieka z treści%modify(...)
stosuje modyfikator%modifyContent(...)
stosuje modyfikator dla bloków
Nasza funkcja print()
może wyglądać tak (dla uproszczenia zaniedbujemy gałąź else
):
public function print(Latte\Compiler\PrintContext $context): string
{
return $context->format(
<<<'XX'
foreach (%node as %node) %line {
%node
}
XX,
$this->expression,
$this->value,
$this->position,
$this->content,
);
}
Zmienna $this->position
jest już zdefiniowana przez klasę Latte\Compiler\Node i ustawiona przez parser. Zawiera obiekt
Latte\Compiler\Position z pozycją znacznika w kodzie
źródłowym w postaci numeru wiersza i kolumny.
Kod runtime może używać zmiennych pomocniczych. Aby uniknąć kolizji ze zmiennymi używanymi przez sam szablon, zwyczajowo
poprzedzamy je znakiem $ʟ__
.
Może również używać dowolnych wartości w czasie runtime, które przekazuje do szablonu w postaci tzw. providerów za
pomocą metody Extension::getProviders(). Dostęp do nich uzyskuje się za pomocą
$this->global->...
.
Przeglądanie AST
W celu dogłębnego przeglądania drzewa AST konieczne jest zaimplementowanie metody getIterator()
:
public function &getIterator(): \Generator
{
yield $this->expression;
yield $this->value;
yield $this->content;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Zauważ, że getIterator()
zwraca referencje. Dzięki temu odwiedzający węzły mogą zastępować
poszczególne węzły innymi.
Jeśli węzeł ma subnody, konieczne jest zaimplementowanie tej metody i udostępnienie wszystkich subnodów. W przeciwnym razie może powstać dziura w zabezpieczeniach. Na przykład tryb piaskownicy nie byłby w stanie sprawdzić węzłów podrzędnych i zapewnić, że nie są na nich wywoływane nieautoryzowane konstrukcje.
Ponieważ słowo kluczowe yield
musi być obecne w ciele metody, nawet jeśli nie ma węzłów dzieci, napisz to w
następujący sposób:
public function &getIterator(): \Generator
{
if (false) {
yield;
}
}
AuxiliaryNode
Jeśli tworzysz nowy tag dla Latte, zaleca się utworzenie dla niego dedykowanej klasy węzła, która będzie reprezentować
go w drzewie AST (patrz klasa ForeachNode
w powyższym przykładzie). W niektórych przypadkach przydatna może
okazać się trywialna klasa węzła pomocniczego AuxiliaryNode, która pozwala
przekazać ciało metody print()
i listę węzłów udostępnionych przez metodę getIterator()
jako
parametry konstruktora:
// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode
$node = new AuxiliaryNode(
// body of the print() method:
fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
// nodes accessed via getIterator() and also passed into the print() method:
[$argNode],
);
Kompilator przechodzi
Przejścia kompilatora to funkcje, które modyfikują AST lub zbierają w nich informacje. Są one zwracane przez metodę Extension::getPasses().
Przeszukiwanie węzłów
Najczęstszym sposobem pracy z AST jest użycie Latte\Compiler\NodeTraverser:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
$ast = (new NodeTraverser)->traverse(
$ast,
enter: fn(Node $node) => ...,
leave: fn(Node $node) => ...,
);
Funkcja enter (tj. node visitor) jest wywoływana przy pierwszym napotkaniu węzła, zanim zostaną przetworzone jego podwęzły. Funkcja leave jest wywoływana po odwiedzeniu wszystkich węzłów podrzędnych. Powszechną praktyką jest to, że funkcja enter służy do zebrania pewnych informacji, a następnie funkcja leave dokonuje korekt na podstawie tych informacji. Do czasu wywołania funkcji leave, cały kod wewnątrz węzła zostanie odwiedzony i zebrane zostaną niezbędne informacje.
Jak zmodyfikować AST? Najprostszym sposobem jest po prostu zmodyfikowanie właściwości węzłów. Drugim sposobem jest
całkowite zastąpienie węzła poprzez zwrócenie nowego węzła. Przykład: poniższy kod zmieni wszystkie liczby całkowite w
AST na łańcuchy (np. 42 zostanie zmienione na '42'
).
use Latte\Compiler\Nodes\Php;
$ast = (new NodeTraverser)->traverse(
$ast,
leave: function (Node $node) {
if ($node instanceof Php\Scalar\IntegerNode) {
return new Php\Scalar\StringNode((string) $node->value);
}
},
);
Moduł AST może z łatwością zawierać tysiące węzłów, a przemierzanie ich wszystkich może być powolne. W niektórych przypadkach można uniknąć całkowitego traversal.
Jeśli przeszukasz drzewo dla wszystkich węzłów Html\ElementNode
, to wiesz, że gdy zobaczysz węzeł
Php\ExpressionNode
, nie ma sensu sprawdzać również wszystkich jego węzłów dziecięcych, ponieważ HTML nie
może być wewnątrz wyrażeń. W tym przypadku możesz powiedzieć traverserowi, aby nie wykonywał rekurencji do
węzła klasy:
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Php\ExpressionNode) {
return NodeTraverser::DontTraverseChildren;
}
// ...
},
);
Jeśli szukasz tylko jednego konkretnego węzła, możliwe jest również całkowite przerwanie traversal po znalezieniu go.
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Nodes\ParametersNode) {
return NodeTraverser::StopTraversal;
}
// ...
},
);
Pomocnicy dla węzłów
Klasa Latte\Compiler\NodeHelpers zapewnia pewne metody, które mogą znaleźć węzły AST, które albo spełniają określony warunek, itp. Kilka przykładów:
use Latte\Compiler\NodeHelpers;
// znajduje wszystkie węzły elementów HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);
// znajduje pierwszy węzeł tekstowy
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);
// konwertuje węzeł PHP na wartość rzeczywistą
$value = NodeHelpers::toValue($node);
// konwertuje statyczny węzeł tekstowy na ciąg znaków
$text = NodeHelpers::toText($node);