Crearea unei extensii
O extensie este o clasă reutilizabilă care poate defini etichete, filtre, funcții, furnizori etc. personalizate.
Creăm extensii atunci când dorim să reutilizăm personalizările noastre Latte în diferite proiecte sau să le împărtășim cu alții. De asemenea, este util să creați o extensie pentru fiecare proiect web care va conține toate etichetele și filtrele specifice pe care doriți să le utilizați în șabloanele de proiect.
Clasa de extensie
Extension este o clasă care moștenește din Latte\Extension. Este înregistrată în Latte utilizând
addExtension()
(sau prin intermediul fișierului
de configurare):
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
Dacă înregistrați mai multe extensii și acestea definesc etichete, filtre sau funcții cu nume identice, ultima extensie adăugată câștigă. Acest lucru implică, de asemenea, faptul că extensiile dvs. pot suprascrie etichetele/filtrele/funcțiile native.
Ori de câte ori efectuați o modificare într-o clasă și actualizarea automată nu este dezactivată, Latte va recompila automat șabloanele dumneavoastră.
O clasă poate implementa oricare dintre următoarele metode:
abstract class Extension
{
/**
* Initializes before template is compiler.
*/
public function beforeCompile(Engine $engine): void;
/**
* Returns a list of parsers for Latte tags.
* @return array<string, callable>
*/
public function getTags(): array;
/**
* Returns a list of compiler passes.
* @return array<string, callable>
*/
public function getPasses(): array;
/**
* Returns a list of |filters.
* @return array<string, callable>
*/
public function getFilters(): array;
/**
* Returns a list of functions used in templates.
* @return array<string, callable>
*/
public function getFunctions(): array;
/**
* Returns a list of providers.
* @return array<mixed>
*/
public function getProviders(): array;
/**
* Returns a value to distinguish multiple versions of the template.
*/
public function getCacheKey(Engine $engine): mixed;
/**
* Initializes before template is rendered.
*/
public function beforeRender(Template $template): void;
}
Pentru a vă face o idee despre cum arată extensia, aruncați o privire la extensia încorporată CoreExtension.
beforeCompile(Latte\Engine $engine): void
Apelată înainte de compilarea șablonului. Metoda poate fi utilizată, de exemplu, pentru inițializări legate de compilare.
getTags(): array
Apelată atunci când șablonul este compilat. Returnează o matrice asociativă numele etichetei⇒ callable, care sunt funcții de analiză a etichetelor.
public function getTags(): array
{
return [
'foo' => [FooNode::class, 'create'],
'bar' => [BarNode::class, 'create'],
'n:baz' => [NBazNode::class, 'create'],
// ...
];
}
Eticheta n:baz
reprezintă un atribut n:pur, adică este o etichetă care poate fi scrisă numai ca atribut.
În cazul etichetelor foo
și bar
, Latte va recunoaște automat dacă acestea sunt perechi și, în
caz afirmativ, pot fi scrise automat folosind n:attributes, inclusiv variantele cu prefixele n:inner-foo
și
n:tag-foo
.
Ordinea de execuție a acestor n:attributes este determinată de ordinea lor în matricea returnată de getTags()
.
Astfel, n:foo
este întotdeauna executat înainte de n:bar
, chiar dacă atributele sunt enumerate în
ordine inversă în tag-ul HTML ca <div n:bar="..." n:foo="...">
.
Dacă aveți nevoie să determinați ordinea celor n:atribute în mai multe extensii, utilizați metoda de ajutor
order()
, unde parametrul before
xor after
determină ce etichete sunt ordonate înainte sau
după etichetă.
public function getTags(): array
{
return [
'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
];
}
getPasses(): array
Este apelată atunci când șablonul este compilat. Returnează o matrice asociativă name pass ⇒ callable, care sunt funcții reprezentând așa-numitele pase de compilare care traversează și modifică AST.
Din nou, poate fi utilizată metoda de ajutor order()
. Valoarea parametrilor before
sau
after
poate fi *
cu semnificația before/after all.
public function getPasses(): array
{
return [
'optimize' => [Passes::class, 'optimizePass'],
'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
// ...
];
}
beforeRender(Latte\Engine $engine): void
Este apelată înainte de fiecare redare a șablonului. Metoda poate fi utilizată, de exemplu, pentru a inițializa variabilele utilizate în timpul redării.
getFilters(): array
Este apelată înainte ca șablonul să fie redat. Returnează filtrele sub forma unei matrice asociative filter name ⇒ callable.
public function getFilters(): array
{
return [
'batch' => [$this, 'batchFilter'],
'trim' => [$this, 'trimFilter'],
// ...
];
}
getFunctions(): array
Se apelează înainte ca șablonul să fie redat. Returnează funcțiile sub forma unui tablou asociativ function name ⇒ callable.
public function getFunctions(): array
{
return [
'clamp' => [$this, 'clampFunction'],
'divisibleBy' => [$this, 'divisibleByFunction'],
// ...
];
}
getProviders(): array
Se apelează înainte ca șablonul să fie redat. Returnează o matrice de furnizori, care sunt, de obicei, obiecte care
utilizează etichete în timpul execuției. Acestea sunt accesate prin intermediul $this->global->...
.
public function getProviders(): array
{
return [
'myFoo' => $this->foo,
'myBar' => $this->bar,
// ...
];
}
getCacheKey(Latte\Engine $engine): mixed
Este apelată înainte ca șablonul să fie redat. Valoarea returnată devine parte a cheii a cărei hash este conținută în numele fișierului șablon compilat. Astfel, pentru valori de returnare diferite, Latte va genera fișiere cache diferite.
Cum funcționează Latte?
Pentru a înțelege cum se definesc etichete personalizate sau pase de compilare, este esențial să înțelegem cum funcționează Latte sub capotă.
Compilarea șabloanelor în Latte funcționează în mod simplist astfel:
- În primul rând, lexer tokenizează codul sursă al șablonului în bucăți mici (tokens) pentru o procesare mai ușoară
- Apoi, parserul convertește fluxul de token-uri într-un arbore de noduri semnificativ (Arborele de sintaxă abstract, AST).
- În cele din urmă, compilatorul generează o clasă PHP din AST care redă șablonul și îl pune în cache.
De fapt, compilarea este un pic mai complicată. Latte are două lexere și analizoare: una pentru șablonul HTML și una pentru codul de tip PHP din interiorul etichetelor. De asemenea, parsarea nu se execută după tokenizare, ci lexerul și parserul rulează în paralel în două „fire“ și se coordonează. Este o știință a rachetelor :-)
În plus, toate etichetele au propriile lor rutine de parsare. Atunci când parserul întâlnește o etichetă, apelează funcția de parsare a acesteia (returnează Extension::getTags()). Sarcina acestora este de a analiza argumentele tag-ului și, în cazul tag-urilor împerecheate, conținutul interior. Aceasta returnează un nod care devine parte din AST. Pentru detalii, consultați funcția de analiză a etichetelor.
Când parserul își termină munca, avem un AST complet care reprezintă șablonul. Nodul rădăcină este
Latte\Compiler\Nodes\TemplateNode
. Nodurile individuale din interiorul arborelui reprezintă nu numai etichetele, ci
și elementele HTML, atributele acestora, orice expresii utilizate în interiorul etichetelor etc.
După aceasta, intră în joc așa-numitele Compiler passes, care sunt funcții (returnate de Extension::getPasses()) care modifică AST.
Întregul proces, de la încărcarea conținutului șablonului, trecând prin parsare, până la generarea fișierului rezultat, poate fi secvențiat cu acest cod, pe care îl puteți experimenta și arunca rezultatele intermediare:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Exemplu de AST
Pentru a avea o idee mai bună despre AST, adăugăm un exemplu. Acesta este șablonul sursă:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Și aceasta este reprezentarea sa sub formă de 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') ) ) ) )
Etichete personalizate
Pentru a defini o nouă etichetă, sunt necesare trei etape:
- definirea funcției de analiză a etichetei (responsabilă de analiza etichetei într-un nod)
- crearea unei clase de nod (responsabilă de generarea codului PHP și de traversarea AST)
- înregistrarea etichetei folosind Extension::getTags()
Funcția de analiză a etichetelor
Parsarea etichetelor este gestionată de funcția de parsare a acestora (cea returnată de Extension::getTags()). Sarcina sa este de a analiza și verifica orice argumente din interiorul etichetei
(pentru aceasta utilizează TagParser). În plus, dacă eticheta este o pereche, aceasta va cere TemplateParser să analizeze și
să returneze conținutul interior. Funcția creează și returnează un nod, care este, de obicei, un copil al
Latte\Compiler\Nodes\StatementNode
, iar acesta devine parte din AST.
Creăm o clasă pentru fiecare nod, lucru pe care îl vom face acum, și plasăm în mod elegant funcția de parsare în ea ca
o fabrică statică. Ca exemplu, să încercăm să creăm cunoscuta etichetă {foreach}
:
use Latte\Compiler\Nodes\StatementNode;
class ForeachNode extends StatementNode
{
// o funcție de parsare care creează doar un nod pentru moment
public static function create(Latte\Compiler\Tag $tag): self
{
$node = $tag->node = new self;
return $node;
}
public function print(Latte\Compiler\PrintContext $context): string
{
// codul va fi adăugat mai târziu
}
public function &getIterator(): \Generator
{
// codul va fi adăugat mai târziu
}
}
Funcției de parsare create()
i se transmite un obiect Latte\Compiler\Tag, care conține informații de bază despre
etichetă (dacă este o etichetă clasică sau un n:attribute, pe ce linie se află etc.) și accesează în principal Latte\Compiler\TagParser în
$tag->parser
.
În cazul în care tag-ul trebuie să aibă argumente, se verifică existența acestora prin apelarea
$tag->expectArguments()
. Metodele obiectului $tag->parser
sunt disponibile pentru analizarea
acestora:
parseExpression(): ExpressionNode
pentru o expresie de tip PHP (de exemplu,10 + 3
).parseUnquotedStringOrExpression(): ExpressionNode
pentru o expresie sau un șir de caractere necotateparseArguments(): ArrayNode
conținutul matricei (de exemplu,10, true, foo => bar
)parseModifier(): ModifierNode
pentru un modificator (de exemplu,|upper|truncate:10
)parseType(): expressionNode
pentru un indicator de tip (de exemplu,int|string
sauFoo\Bar[]
)
și un nivel scăzut Latte\Compiler\TokenStream care operează direct cu token-uri:
$tag->parser->stream->consume(...): Token
$tag->parser->stream->tryConsume(...): ?Token
Latte extinde sintaxa PHP în moduri mici, de exemplu prin adăugarea de modificatori, operatori ternari scurtați sau
permițând scrierea șirurilor alfanumerice simple fără ghilimele. Acesta este motivul pentru care folosim termenul
PHP-like în loc de PHP. Astfel, metoda parseExpression()
analizează foo
ca 'foo'
,
de exemplu. În plus, unquoted-string este un caz special al unui șir de caractere care, de asemenea, nu trebuie să fie
citat, dar în același timp nu trebuie să fie alfanumeric. De exemplu, este vorba de calea de acces la un fișier în eticheta
{include ../file.latte}
. Metoda parseUnquotedStringOrExpression()
este utilizată pentru a-l
analiza.
Studierea claselor de noduri care fac parte din Latte este cel mai bun mod de a învăța toate detaliile de detaliu ale procesului de analiză.
Să ne întoarcem la eticheta {foreach}
. În ea, așteptăm argumente de forma
expression + 'as' + second expression
, pe care le analizăm după cum urmează:
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;
}
}
Expresiile pe care le-am scris în variabilele $expression
și $value
reprezintă subnoduri.
Definiți variabilele cu subnoduri ca fiind publice, astfel încât acestea să poată fi modificate în etapele ulterioare de procesare, dacă este necesar. De asemenea, este necesar să le puneți la dispoziție pentru traversare.
Pentru etichetele împerecheate, cum este cazul nostru, metoda trebuie să permită, de asemenea, TemplateParser să analizeze
conținutul interior al etichetei. Acest lucru este gestionat de yield
, care returnează o pereche [inner content,
end tag]. Stocăm conținutul interior în variabila $node->content
.
public AreaNode $content;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $endTag] = yield;
return $node;
}
Cuvântul cheie yield
determină încheierea metodei create()
, returnând controlul înapoi la
TemplateParser, care continuă să analizeze conținutul până când ajunge la tag-ul final. Apoi transmite controlul înapoi la
create()
, care continuă de unde a rămas. Utilizarea metodei yield
, returnează automat
Generator
.
Puteți, de asemenea, să transmiteți la yield
o matrice de nume de etichete pentru care doriți să opriți
analizarea dacă apar înainte de eticheta de sfârșit. Acest lucru ne ajută să implementăm metoda
{foreach}...{else}...{/foreach}
construcția. Dacă apare {else}
, analizăm conținutul de după el în
$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;
}
Nodul de întoarcere finalizează analiza etichetelor.
Generarea codului PHP
Fiecare nod trebuie să implementeze metoda print()
. Returnează codul PHP care redă partea dată a șablonului
(cod de execuție). I se transmite ca parametru un obiect Latte\Compiler\PrintContext, care are o metodă
utilă format()
care simplifică asamblarea codului rezultat.
Metoda format(string $mask, ...$args)
acceptă în mască următoarele spații libere:
%node
imprimă Node%dump
exportă valoarea în PHP%raw
inserează textul direct, fără nicio transformare%args
tipărește ArrayNode ca argumente la apelul funcției%line
tipărește un comentariu cu un număr de linie%escape(...)
evadează conținutul%modify(...)
aplică un modificator%modifyContent(...)
aplică un modificator la blocuri
Funcția noastră print()
ar putea arăta astfel (pentru simplificare, neglijăm ramura 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,
);
}
Variabila $this->position
este deja definită de clasa Latte\Compiler\Node și este setată de parser. Ea conține un
obiect Latte\Compiler\Position cu poziția etichetei
în codul sursă sub forma unui număr de rând și coloană.
Codul de execuție poate utiliza variabile auxiliare. Pentru a evita coliziunea cu variabilele utilizate de șablonul
propriu-zis, este o convenție să le prefixeze cu caracterele $ʟ__
.
De asemenea, se pot utiliza valori arbitrare în timpul execuției, care sunt transmise șablonului sub formă de furnizori cu
ajutorul metodei Extension::getProviders(). Acesta le accesează folosind
$this->global->...
.
Traversarea AST
Pentru a parcurge arborele AST în profunzime, este necesar să se implementeze metoda getIterator()
. Aceasta va
permite accesul la subnoduri:
public function &getIterator(): \Generator
{
yield $this->expression;
yield $this->value;
yield $this->content;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Rețineți că getIterator()
returnează o referință. Aceasta este ceea ce permite vizitatorilor de noduri să
înlocuiască nodurile individuale cu alte noduri.
În cazul în care un nod are subnoduri, este necesar să se implementeze această metodă și să se facă disponibile toate subnodurile. În caz contrar, s-ar putea crea o gaură de securitate. De exemplu, modul „sandbox“ nu ar putea controla subnodurile și nu s-ar putea asigura că în acestea nu sunt apelate construcții nepermise.
Deoarece cuvântul cheie yield
trebuie să fie prezent în corpul metodei chiar dacă nu are noduri copil,
scrieți-o după cum urmează:
public function &getIterator(): \Generator
{
if (false) {
yield;
}
}
AuxiliaryNode
Dacă creați o nouă etichetă pentru Latte, este recomandabil să creați o clasă de nod dedicată pentru aceasta, care
o va reprezenta în arborele AST (a se vedea clasa ForeachNode
din exemplul de mai sus). În unele cazuri, s-ar
putea să vi se pară utilă clasa de noduri auxiliare triviale AuxiliaryNode, care vă permite
să treceți ca parametri de constructor corpul metodei print()
și lista de noduri accesibile prin metoda
getIterator()
:
// 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],
);
Compilatorul trece
Compiler Passes sunt funcții care modifică AST-urile sau colectează informații din acestea. Acestea sunt returnate de metoda Extension::getPasses().
Traversarea nodurilor
Cel mai comun mod de a lucra cu AST este prin utilizarea unui server Latte\Compiler\NodeTraverser:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
$ast = (new NodeTraverser)->traverse(
$ast,
enter: fn(Node $node) => ...,
leave: fn(Node $node) => ...,
);
Funcția enter (adică vizitator) este apelată atunci când un nod este întâlnit pentru prima dată, înainte ca subnodurile sale să fie procesate. Funcția leave este apelată după ce toate subnodurile au fost vizitate. Un model comun este acela că enter este utilizat pentru a colecta anumite informații, iar apoi leave efectuează modificări pe baza acestora. În momentul în care leave este apelată, tot codul din interiorul nodului a fost deja vizitat și informațiile necesare au fost colectate.
Cum se modifică AST? Cea mai simplă metodă este de a modifica pur și simplu proprietățile nodurilor. A doua modalitate
este de a înlocui în întregime nodul prin returnarea unui nou nod. Exemplu: următorul cod va schimba toate numerele întregi
din AST în șiruri de caractere (de exemplu, 42 va fi schimbat în '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);
}
},
);
Un AST poate conține cu ușurință mii de noduri, iar parcurgerea tuturor acestora poate fi lentă. În unele cazuri, este posibil să se evite o parcurgere completă.
Dacă sunteți în căutarea tuturor Html\ElementNode
într-un arbore, știți că, odată ce ați văzut
Php\ExpressionNode
, nu are rost să verificați și toate nodurile sale inferioare, deoarece HTML nu poate fi inclus
în expresii. În acest caz, puteți instrui traversarea pentru a nu mai intra în nodul de clasă:
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Php\ExpressionNode) {
return NodeTraverser::DontTraverseChildren;
}
// ...
},
);
Dacă nu căutați decât un singur nod specific, este de asemenea posibil să întrerupeți complet parcurgerea după ce l-ați găsit.
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Nodes\ParametersNode) {
return NodeTraverser::StopTraversal;
}
// ...
},
);
Ajutoare pentru noduri
Clasa Latte\Compiler\NodeHelpers oferă câteva metode care pot găsi noduri AST care satisfac un anumit callback etc. Sunt prezentate câteva exemple:
use Latte\Compiler\NodeHelpers;
// găsește toate nodurile elementelor HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);
// găsește primul nod de text
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);
// convertește nodul de valoare PHP în valoare reală
$value = NodeHelpers::toValue($node);
// convertește nodul textual static în șir de caractere
$text = NodeHelpers::toText($node);