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:

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 necotate
  • parseArguments(): 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 sau Foo\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);
versiune: 3.0