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:
- definiranje funkcije za razčlenitev oznake (odgovorna za razčlenitev oznake v vozlišče)
- ustvarjanje razreda vozlišča (odgovoren za generiranje kode PHP in pregledovanje AST)
- registracija oznake z uporabo funkcije Extension::getTags()
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 citatovparseArguments(): ArrayNode
vsebina polja (npr.10, true, foo => bar
)parseModifier(): ModifierNode
za modifikator (npr.|upper|truncate:10
)parseType(): expressionNode
za namig tipa (npr.int|string
aliFoo\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);