Ustvarjanje razširitve

Razširitev je razred za večkratno uporabo, ki lahko določa oznake, filtre, funkcije, ponudnike itd. po meri.

Razširitve ustvarimo, kadar želimo svoje prilagoditve Latte ponovno uporabiti v različnih projektih ali jih deliti z drugimi. Koristno je tudi, da za vsak spletni projekt ustvarite razširitev, ki bo vsebovala vse posebne oznake in filtre, ki jih želite uporabiti v predlogah projekta.

Razred razširitve

Razširitev je razred, ki deduje od Latte\Extension. Registrira se v Latte z uporabo addExtension() (ali prek konfiguracijske datoteke):

$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);

Če registrirate več razširitev in opredeljujejo enako poimenovane oznake, filtre ali funkcije, zmaga zadnja dodana razširitev. To tudi pomeni, da lahko vaše razširitve razveljavijo izvirne oznake/filtre/funkcije.

Kadar koli spremenite razred in samodejno osveževanje ni izklopljeno, bo Latte samodejno ponovno sestavil vaše predloge.

Razred lahko implementira katero koli od naslednjih metod:

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;
}

Za predstavo, kako je videti razširitev, si oglejte vgrajeno razširitev “CoreExtension:https://github.com/…xtension.php”.

beforeCompile(Latte\Engine $engine)void

Pokliče se, preden se sestavi predloga. Metoda se lahko uporablja na primer za inicializacije, povezane s sestavljanjem.

getTags(): array

Pokliče se, ko je predloga sestavljena. Vrne asociativno polje imena značk ⇒ klicni, ki so funkcije za razčlenjevanje značk.

public function getTags(): array
{
	return [
		'foo' => [FooNode::class, 'create'],
		'bar' => [BarNode::class, 'create'],
		'n:baz' => [NBazNode::class, 'create'],
		// ...
	];
}

Oznaka n:baz predstavlja čisti n:atribut, tj. je oznaka, ki se lahko zapiše samo kot atribut.

V primeru oznak foo in bar bo Latte samodejno prepoznal, ali gre za pare, in če je tako, jih je mogoče samodejno zapisati z uporabo n:atributov, vključno z različicami s predponama n:inner-foo in n:tag-foo.

Vrstni red izvajanja takih n:atributov je določen z njihovim vrstnim redom v polju, ki ga vrne getTags(). Tako se n:foo vedno izvede pred n:bar, tudi če so atributi v oznaki HTML navedeni v obratnem vrstnem redu kot <div n:bar="..." n:foo="...">.

Če morate določiti vrstni red n:atributov v več razširitvah, uporabite pomožno metodo order(), kjer parameter before xor after določa, katere oznake se razvrstijo pred ali za oznako.

public function getTags(): array
{
	return [
		'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
		'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
	];
}

getPasses(): array

Ta metoda se kliče, ko je predloga sestavljena. Vrne asociativno polje name pass ⇒ callable, ki so funkcije, ki predstavljajo tako imenovane prevoze sestavljavca, ki prečkajo in spreminjajo AST.

Spet lahko uporabimo pomožno metodo order(). Vrednost parametrov before ali after je lahko * s pomenom pred/po vseh.

public function getPasses(): array
{
	return [
		'optimize' => [Passes::class, 'optimizePass'],
		'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
		// ...
	];
}

beforeRender(Latte\Engine $engine)void

Pokliče se pred vsakim izrisom predloge. Metoda se lahko uporablja na primer za inicializacijo spremenljivk, ki se uporabljajo med upodabljanjem.

getFilters(): array

Pokliče se, preden se predloga izriše. Vrne filtre kot asociativno polje imena filtrov ⇒ klicni.

public function getFilters(): array
{
	return [
		'batch' => [$this, 'batchFilter'],
		'trim' => [$this, 'trimFilter'],
		// ...
	];
}

getFunctions(): array

Pokliče se pred izrisom predloge. Vrne funkcije kot asociativno polje naslov funkcije ⇒ klicno.

public function getFunctions(): array
{
	return [
		'clamp' => [$this, 'clampFunction'],
		'divisibleBy' => [$this, 'divisibleByFunction'],
		// ...
	];
}

getProviders(): array

Pokliče se, preden se predloga prikaže. Vrne polje ponudnikov, ki so običajno predmeti, ki uporabljajo oznake med izvajanjem. Dostop do njih je mogoč prek $this->global->....

public function getProviders(): array
{
	return [
		'myFoo' => $this->foo,
		'myBar' => $this->bar,
		// ...
	];
}

getCacheKey(Latte\Engine $engine)mixed

Pokliče se pred izrisom predloge. Vrnjena vrednost postane del ključa, katerega hash je vsebovan v imenu sestavljene datoteke predloge. Tako bo Latte za različne vrnjene vrednosti ustvaril različne datoteke predpomnilnika.

Kako deluje Latte?

Da bi razumeli, kako opredeliti oznake po meri ali prehode za sestavljanje, je treba razumeti, kako Latte deluje pod pokrovom.

Sestavljanje predlog v Latte poenostavljeno deluje takole:

  • Najprej lekser označi izvorno kodo predloge na majhne dele (žetone) za lažjo obdelavo.
  • nato parser pretvori tok žetonov v smiselno drevo vozlišč (Abstract Syntax Tree, AST)
  • na koncu prevajalnik iz AST ustvari** razred PHP, ki upodobi predlogo in jo shrani v predpomnilnik.

Pravzaprav je sestavljanje nekoliko bolj zapleteno. Latte ima dva** leksikatorja in razčlenjevalnika: enega za predlogo HTML in drugega za kodo PHP znotraj oznak. Prav tako se razčlenjevanje ne izvaja po tokenizaciji, temveč leksikator in razčlenjevalnik delujeta vzporedno v dveh “nitih” in se usklajujeta. To je raketna znanost :-)

Poleg tega imajo vse oznake svoje rutine za razčlenjevanje. Ko razčlenjevalnik naleti na oznako, pokliče njeno funkcijo za razčlenjevanje (vrne funkcijo Extension::getTags()). Njihova naloga je razčleniti argumente oznake in v primeru parnih oznak tudi notranjo vsebino. Vrne vozlišče, ki postane del AST. Za podrobnosti glejte Funkcija za razčlenjevanje oznak.

Ko razčlenjevalnik konča svoje delo, imamo popoln AST, ki predstavlja predlogo. Korensko vozlišče je Latte\Compiler\Nodes\TemplateNode. Posamezna vozlišča znotraj drevesa nato predstavljajo ne le oznake, temveč tudi elemente HTML, njihove atribute, vse izraze, uporabljene znotraj oznak, itd.

Nato pridejo na vrsto tako imenovani Compiler passes, ki so funkcije (vrne jih Extension::getPasses()), ki spreminjajo AST.

Celoten postopek, od nalaganja vsebine predloge prek razčlenjevanja do generiranja končne datoteke, je mogoče zaporedoma izvajati s to kodo, s katero lahko eksperimentirate in izpisujete vmesne rezultate:

$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);

Primer AST

Za boljšo predstavo o AST dodamo primer. To je izvorna predloga:

{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}

To je njena predstavitev v obliki 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')
            )
        )
   )
)

Oznake po meri

Za opredelitev nove oznake so potrebni trije koraki:

Funkcija za razčlenjevanje oznak

Za razčlenjevanje oznak skrbi funkcija za razčlenjevanje (tista, ki jo vrne Extension::getTags()). Njena naloga je razčleniti in preveriti vse argumente znotraj oznake (za to uporablja TagParser). Poleg tega, če je oznaka par, bo od TemplateParserja zahtevala razčlenitev in vrnitev notranje vsebine. Funkcija ustvari in vrne vozlišče, ki je običajno otrok Latte\Compiler\Nodes\StatementNode, to pa postane del AST.

Za vsako vozlišče ustvarimo razred, kar bomo storili zdaj, in vanj kot statično tovarno elegantno umestimo funkcijo za razčlenjevanje. Kot primer poskusimo ustvariti znano oznako {foreach}:

use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// funkcija razčlenjevanja, ki za zdaj samo ustvari vozlišče
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// koda bo dodana pozneje
	}

	public function &getIterator(): \Generator
	{
		// koda bo dodana pozneje
	}
}

Funkciji za razčlenjevanje create() se posreduje objekt Latte\Compiler\Tag, ki nosi osnovne informacije o znački (ali gre za klasično značko ali n:atribut, v kateri vrstici je itd.), v glavnem pa dostopa do Latte\Compiler\TagParser v $tag->parser.

Če mora imeti oznaka argumente, preveri njihov obstoj tako, da pokliče $tag->expectArguments(). Za njihovo analizo so na voljo metode predmeta $tag->parser:

  • parseExpression(): ExpressionNode za izraz, podoben PHP (npr. 10 + 3)
  • parseUnquotedStringOrExpression(): ExpressionNode za izraz ali niz brez citatov
  • parseArguments(): ArrayNode vsebina polja (npr. 10, true, foo => bar)
  • parseModifier(): ModifierNode za modifikator (npr. |upper|truncate:10)
  • parseType(): expressionNode za namig tipa (npr. int|string ali Foo\Bar[])

in nizkonivojski Latte\Compiler\TokenStream, ki deluje neposredno z žetoni:

  • $tag->parser->stream->consume(...): Token
  • $tag->parser->stream->tryConsume(...): ?Token

Latte razširi sintakso PHP na majhne načine, na primer z dodajanjem modifikatorjev, skrajšanih ternarnih operatorjev ali omogočanjem zapisa preprostih alfanumeričnih nizov brez narekovajev. Zato uporabljamo izraz PHP-like namesto PHP. Tako na primer metoda parseExpression() razčleni foo kot 'foo'. Poleg tega je neocitni niz poseben primer niza, ki ga prav tako ni treba navajati z narekovaji, hkrati pa ni treba, da je alfanumerični. To je na primer pot do datoteke v oznaki {include ../file.latte}. Za njegovo razčlenjevanje se uporablja metoda parseUnquotedStringOrExpression().

Preučevanje razredov vozlišč, ki so del Latte, je najboljši način za spoznavanje vseh podrobnosti postopka razčlenjevanja.

Vrnimo se k oznaki {foreach}. V njej pričakujemo argumente v obliki expression + 'as' + second expression, ki jih razčlenimo na naslednji način:

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;
	}
}

Izrazi, ki smo jih zapisali v spremenljivki $expression in $value, predstavljajo podmene.

Spremenljivke s podvozli opredelite kot javne, tako da jih lahko po potrebi spremenite v nadaljnjih korakih obdelave. Prav tako jih je treba dati na voljo za prečkanje.

Za parne oznake, kot je naša, mora metoda omogočiti, da TemplateParser razčleni tudi notranjo vsebino oznake. Za to poskrbi yield, ki vrne par [notranja vsebina, končna oznaka]. Notranjo vsebino shranimo v spremenljivko $node->content.

public AreaNode $content;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $endTag] = yield;
	return $node;
}

Ključna beseda yield povzroči, da se metoda create() zaključi in vrne nadzor nazaj v TemplateParser, ki nadaljuje z razčlenjevanjem vsebine, dokler ne naleti na končno oznako. Nato preda krmiljenje nazaj metodi create(), ki nadaljuje z delom, ki ga je končala. Uporaba metode yield, samodejno vrne Generator.

Na naslov yield lahko posredujete tudi polje imen oznak, za katere želite ustaviti razčlenjevanje, če se pojavijo pred končno oznako. To nam pomaga pri izvajanju metode {foreach}...{else}...{/foreach} konstrukcijo. Če se pojavi {else}, vsebino za njo razčlenimo v $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;
}

Vračanje vozlišča zaključi razčlenjevanje oznak.

Ustvarjanje kode PHP

Vsako vozlišče mora izvajati metodo print(). Vrne kodo PHP, ki upodablja dani del predloge (izvajalna koda). Kot parameter se ji posreduje objekt Latte\Compiler\PrintContext, ki ima uporabno metodo format(), ki poenostavi sestavljanje dobljene kode.

Metoda format(string $mask, ...$args) v maski sprejema naslednje nadomestne znake:

  • %node izpiše vozlišče
  • %dump izvozi vrednost v PHP
  • %raw vstavi besedilo neposredno brez preoblikovanja
  • %args izpiše ArrayNode kot argumente za klic funkcije
  • %line izpiše komentar s številko vrstice
  • %escape(...) izriše vsebino
  • %modify(...) uporabi modifikator
  • %modifyContent(...) uporabi modifikator za bloke

Naša funkcija print() je lahko videti takole (zaradi preprostosti zanemarjamo vejo 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,
	);
}

Spremenljivka $this->position je že opredeljena v razredu Latte\Compiler\Node in jo določi razčlenjevalnik. Vsebuje objekt Latte\Compiler\Position s položajem oznake v izvorni kodi v obliki številke vrstice in stolpca.

Izvedbena koda lahko uporablja pomožne spremenljivke. Da bi se izognili trku s spremenljivkami, ki jih uporablja sama predloga, je običajno, da jim dodamo predpono z znaki $ʟ__.

V času izvajanja lahko uporablja tudi poljubne vrednosti, ki se predlogi posredujejo v obliki ponudnikov z metodo Extension::getProviders(). Do njih dostopa z uporabo $this->global->....

Prehajanje po AST

Za poglobljeno potovanje po drevesu AST je treba implementirati metodo getIterator(). To bo omogočilo dostop do podvozij:

public function &getIterator(): \Generator
{
	yield $this->expression;
	yield $this->value;
	yield $this->content;
	if ($this->elseContent) {
		yield $this->elseContent;
	}
}

Upoštevajte, da getIterator() vrne referenco. To obiskovalcem vozlišč omogoča zamenjavo posameznih vozlišč z drugimi vozlišči.

Če ima vozlišče podvozlišča, je treba implementirati to metodo in dati na voljo vsa podvozlišča. V nasprotnem primeru lahko nastane varnostna luknja. Na primer, način peskovnika ne bi mogel nadzorovati podvozij in zagotoviti, da se v njih ne kličejo nedovoljene konstrukcije.

Ker mora biti ključna beseda yield prisotna v telesu metode, tudi če ta nima podrejenih vozlišč, jo zapišite na naslednji način:

public function &getIterator(): \Generator
{
	if (false) {
		yield;
	}
}

AuxiliaryNode

Če ustvarjate novo oznako za Latte, je zanjo priporočljivo ustvariti poseben razred vozlišča, ki jo bo predstavljal v drevesu AST (glej razred ForeachNode v zgornjem primeru). V nekaterih primerih se vam bo morda zdel uporaben trivialni pomožni razred vozlišč AuxiliaryNode, ki vam omogoča, da kot parametre konstruktorja posredujete telo metode print() in seznam vozlišč, do katerih je dostopna 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],
);

Prevajalnik prenese

Compiler Passes so funkcije, ki spreminjajo AST ali zbirajo informacije v njih. Vrne jih metoda Extension::getPasses().

Premikanje vozlišč

Najpogostejši način za delo z AST je uporaba Latte\Compiler\NodeTraverser:

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: fn(Node $node) => ...,
	leave: fn(Node $node) => ...,
);

Funkcija enter (tj. obiskovalec) se pokliče ob prvem srečanju z vozliščem, preden se obdelajo njegova podvozja. Funkcija leave se pokliče, ko so obiskana vsa podvozja. Pogost vzorec je, da se funkcija enter uporablja za zbiranje nekaterih informacij, nato pa funkcija leave na podlagi teh informacij izvede spremembe. Ko se pokliče funkcija leave, bo vsa koda v vozlišču že obiskana in zbrane bodo potrebne informacije.

Kako spremeniti AST? Najlažje je preprosto spremeniti lastnosti vozlišč. Drugi način je, da vozlišče v celoti zamenjamo tako, da vrnemo novo vozlišče. Primer: naslednja koda bo vsa cela števila v AST spremenila v nize (npr. 42 bo spremenjeno v '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);
        }
	},
);

AST lahko zlahka vsebuje na tisoče vozlišč, premikanje po vseh vozliščih pa je lahko počasno. V nekaterih primerih se je mogoče izogniti celotnemu obhodu.

Če iščete vse Html\ElementNode v drevesu, veste, da ko enkrat vidite Php\ExpressionNode, nima smisla preverjati tudi vseh njegovih podrejenih vozlišč, saj HTML ne more biti znotraj v izrazih. V tem primeru lahko potovalniku naročite, naj ne seže v vozlišče razreda:

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Php\ExpressionNode) {
			return NodeTraverser::DontTraverseChildren;
        }
        // ...
	},
);

Če iščete samo eno določeno vozlišče, lahko po tem, ko ga najdete, tudi v celoti prekinete potovanje.

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Nodes\ParametersNode) {
			return NodeTraverser::StopTraversal;
        }
        // ...
	},
);

Pomočniki vozlišč

Razred Latte\Compiler\NodeHelpers ponuja nekaj metod, s katerimi lahko poiščemo vozlišča AST, ki izpolnjujejo določene povratne klice itd. Prikazanih je nekaj primerov:

use Latte\Compiler\NodeHelpers;

// najde vsa vozlišča elementov HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// najde prvo vozlišče besedila
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// pretvori vozlišče vrednosti PHP v realno vrednost
$value = NodeHelpers::toValue($node);

// pretvori statično besedilno vozlišče v niz
$text = NodeHelpers::toText($node);
različica: 3.0