Ustvarjanje lastnih značk
Ta stran ponuja obsežen vodnik za ustvarjanje lastnih značk v Latte. Obravnavali bomo vse od preprostih značk do bolj zapletenih scenarijev z gnezdeno vsebino in specifičnimi potrebami razčlenjevanja, pri čemer bomo gradili na vašem razumevanju, kako Latte prevaja predloge.
Lastne značke zagotavljajo najvišjo raven nadzora nad sintakso predloge in logiko izrisovanja, vendar so tudi najbolj zapletena točka razširitve. Preden se odločite ustvariti lastno značko, vedno razmislite, ali ne obstaja enostavnejša rešitev ali če ustrezna značka že ne obstaja v standardnem naboru. Lastne značke uporabljajte le, če enostavnejše alternative ne zadostujejo za vaše potrebe.
Razumevanje postopka prevajanja
Za učinkovito ustvarjanje lastnih značk je koristno pojasniti, kako Latte obdeluje predloge. Razumevanje tega postopka pojasnjuje, zakaj so značke strukturirane ravno tako in kako se prilegajo širšemu kontekstu.
Prevajanje predloge v Latte, poenostavljeno, vključuje te ključne korake:
- Leksična analiza: Lekser bere izvorno kodo predloge (datoteko
.latte
) in jo razdeli na zaporedje majhnih, ločenih delov, imenovanih žetoni (npr.{
,foreach
,$variable
,}
, besedilo HTML itd.). - Razčlenjevanje: Razčlenjevalnik vzame ta tok žetonov in iz njega zgradi smiselno drevesno strukturo, ki predstavlja logiko in vsebino predloge. To drevo se imenuje abstraktno sintaktično drevo (AST).
- Prevajalski prehodi: Pred generiranjem PHP kode Latte zažene prevajalske prehode. To so funkcije, ki prehajajo celoten AST in ga lahko spreminjajo ali zbirajo informacije. Ta korak je ključen za funkcije, kot sta varnost (Sandbox) ali optimizacija.
- Generiranje kode: Na koncu prevajalnik preide (potencialno spremenjen) AST in generira ustrezno kodo PHP razreda. Ta PHP koda je tisto, kar dejansko izriše predlogo ob zagonu.
- Predpomnjenje (Caching): Generirana PHP koda se shrani na disk, kar naredi nadaljnja izrisovanja zelo hitra, saj so koraki 1–4 preskočeni.
V resnici je prevajanje nekoliko bolj zapleteno. Latte ima dva lekserja in razčlenjevalnika: enega za HTML predlogo in drugega za PHP-podobno kodo znotraj značk. Prav tako se razčlenjevanje ne zgodi šele po tokenizaciji, ampak lekser in razčlenjevalnik tečeta vzporedno v dveh “nitih” in se usklajujeta. Verjemite mi, programiranje tega je bila raketna znanost :-)
Celoten postopek, od nalaganja vsebine predloge, preko razčlenjevanja, do generiranja končne datoteke, je mogoče zaporedno izvesti 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);
Anatomija značke
Ustvarjanje popolnoma delujoče lastne značke v Latte vključuje več medsebojno povezanih delov. Preden se lotimo implementacije, razumimo osnovne koncepte in terminologijo z uporabo analogije s HTML in Document Object Model (DOM).
Značke proti Vozliščem (Analogija s HTML)
V HTML pišemo značke kot <p>
ali <div>...</div>
. Te značke so sintaksa
v izvorni kodi. Ko brskalnik razčleni ta HTML, ustvari pomnilniško predstavitev, imenovano Document Object Model (DOM).
V DOM so HTML značke predstavljene z vozlišči (natančneje vozlišči Element
v terminologiji JavaScript
DOM). S temi vozlišči programsko delamo (npr. z JavaScript document.getElementById(...)
se vrne vozlišče
Element). Značka je samo besedilna predstavitev v izvorni datoteki; vozlišče je objektna predstavitev v logičnem
drevesu.
Latte deluje podobno:
- V datoteki
.latte
predloge pišete Latte značke, kot sta{foreach ...}
in{/foreach}
. To je sintaksa, s katero delate kot avtor predloge. - Ko Latte razčleni predlogo, zgradi Abstract Syntax Tree (AST). To drevo je sestavljeno iz vozlišč. Vsaka Latte značka, HTML element, kos besedila ali izraz v predlogi postane eno ali več vozlišč v tem drevesu.
- Osnovni razred za vsa vozlišča v AST je
Latte\Compiler\Node
. Tako kot ima DOM različne vrste vozlišč (Element, Text, Comment), ima AST Latte različne vrste vozlišč. Srečali se boste zLatte\Compiler\Nodes\TextNode
za statično besedilo,Latte\Compiler\Nodes\Html\ElementNode
za HTML elemente,Latte\Compiler\Nodes\Php\ExpressionNode
za izraze znotraj značk in ključno za lastne značke, vozlišči, ki dedujejo izLatte\Compiler\Nodes\StatementNode
.
Zakaj StatementNode
?
HTML elementi (Html\ElementNode
) primarno predstavljajo strukturo in vsebino. PHP izrazi
(Php\ExpressionNode
) predstavljajo vrednosti ali izračune. Kaj pa Latte značke kot {if}
,
{foreach}
ali naša lastna {datetime}
? Te značke izvajajo dejanja, nadzorujejo tok programa
ali generirajo izpis na podlagi logike. So funkcionalne enote, ki naredijo Latte močan engine za predloge, ne le
označevalni jezik.
V programiranju se takšne enote, ki izvajajo dejanja, pogosto imenujejo “statements” (stavki). Zato vozlišča, ki
predstavljajo te funkcionalne Latte značke, tipično dedujejo iz Latte\Compiler\Nodes\StatementNode
. To jih loči od
čisto strukturnih vozlišč (kot so HTML elementi) ali vozlišč, ki predstavljajo vrednosti (kot so izrazi).
Ključne komponente
Poglejmo glavne komponente, potrebne za ustvarjanje lastne značke:
Funkcija za razčlenjevanje značke
- Ta PHP klicna funkcija (callable) razčleni sintakso Latte značke (
{...}
) v izvorni predlogi. - Prejme informacije o znački (kot so njeno ime, položaj in ali gre za n:atribut) preko objekta Latte\Compiler\Tag.
- Njeno primarno orodje za razčlenjevanje parametrov in izrazov znotraj ločil značke je objekt Latte\Compiler\TagParser, dostopen preko
$tag->parser
(to je drug razčlenjevalnik kot tisti, ki razčleni celotno predlogo). - Za parne značke uporablja
yield
za signaliziranje Latteju, naj razčleni notranjo vsebino med začetno in končno značko. - Končni cilj funkcije za razčlenjevanje je ustvariti in vrniti instanco razreda vozlišča, ki je dodana v AST.
- Navada je (čeprav ni zahtevano) implementirati funkcijo za razčlenjevanje kot statično metodo (pogosto imenovano
create
) neposredno v ustreznem razredu vozlišča. To ohranja logiko razčlenjevanja in predstavitev vozlišča lepo v enem paketu, omogoča dostop do zasebnih/zaščitenih elementov razreda, če je potrebno, in izboljšuje organizacijo.
Razred vozlišča
- Predstavlja logično funkcijo vaše značke v Abstract Syntax Tree (AST).
- Vsebuje razčlenjene informacije (kot so parametri ali vsebina) kot javne lastnosti. Te lastnosti pogosto vsebujejo druge
instance
Node
(npr.ExpressionNode
za razčlenjene parametre,AreaNode
za razčlenjeno vsebino). - Metoda
print(PrintContext $context): string
generira PHP kodo (stavek ali serijo stavkov), ki izvaja dejanje značke med izrisovanjem predloge. - Metoda
getIterator(): \Generator
omogoča dostop do podrejenih vozlišč (parametrov, vsebine) za prehod prevajalskih prehodov. Mora zagotavljati reference (&
), da omogoči prehodom potencialno spreminjanje ali zamenjavo podvozlišč. - Ko je celotna predloga razčlenjena v AST, Latte zažene vrsto prevajalskih prehodov. Ti prehodi prehajajo celoten AST z uporabo
metode
getIterator()
, ki jo zagotavlja vsako vozlišče. Lahko pregledujejo vozlišča, zbirajo informacije in celo spreminjajo drevo (npr. s spreminjanjem javnih lastnosti vozlišč ali popolno zamenjavo vozlišč). Ta zasnova, ki zahteva celovitgetIterator()
, je temeljna. Omogoča močnim funkcijam, kot je Sandbox, da analizirajo in potencialno spremenijo obnašanje katerega koli dela predloge, vključno z vašimi lastnimi značkami, kar zagotavlja varnost in doslednost.
Registracija preko razširitve
- Latte morate obvestiti o vaši novi znački in katera funkcija za razčlenjevanje naj se zanjo uporabi. To se zgodi znotraj razširitve Latte.
- Znotraj vašega razreda razširitve implementirate metodo
getTags(): array
. Ta metoda vrne asociativno polje, kjer so ključi imena značk (npr.'mytag'
,'n:myattribute'
), vrednosti pa so PHP klicne funkcije (callable), ki predstavljajo njihove ustrezne funkcije za razčlenjevanje (npr.MyNamespace\DatetimeNode::create(...)
).
Povzetek: Funkcija za razčlenjevanje značke pretvori izvorno kodo predloge vaše značke v vozlišče
AST. Razred vozlišča nato zna pretvoriti samega sebe v izvedljivo PHP kodo za prevedeno predlogo
in omogoča dostop do svojih podvozlišč za prevajalske prehode preko getIterator()
. Registracija preko
razširitve poveže ime značke s funkcijo za razčlenjevanje in o njej obvesti Latte.
Zdaj bomo raziskali, kako implementirati te komponente korak za korakom.
Ustvarjanje preproste značke
Lotimo se ustvarjanja vaše prve lastne Latte značke. Začeli bomo z zelo preprostim primerom: značko z imenom
{datetime}
, ki izpiše trenutni datum in čas. Sprva ta značka ne bo sprejemala nobenih parametrov, vendar jo
bomo izboljšali kasneje v razdelku “Razčlenjevanje parametrov značke”. Prav tako
nima nobene notranje vsebine.
Ta primer vas bo vodil skozi osnovne korake: definiranje razreda vozlišča, implementacija njegovih metod print()
in getIterator()
, ustvarjanje funkcije za razčlenjevanje in končno registracija značke.
Cilj: Implementirati {datetime}
za izpis trenutnega datuma in časa z uporabo PHP funkcije
date()
.
Ustvarjanje razreda vozlišča
Najprej potrebujemo razred, ki bo predstavljal našo značko v Abstract Syntax Tree (AST). Kot smo že omenili, dedujemo iz
Latte\Compiler\Nodes\StatementNode
.
Ustvarite datoteko (npr. DatetimeNode.php
) in definirajte razred:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Funkcija za razčlenjevanje značke, klicana, ko je najden {datetime}.
*/
public static function create(Tag $tag): self
{
// Naša preprosta značka trenutno ne sprejema nobenih parametrov, zato nam ni treba ničesar razčlenjevati
$node = $tag->node = new self;
return $node;
}
/**
* Generira PHP kodo, ki se bo izvedla pri izrisovanju predloge.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Omogoča dostop do podrejenih vozlišč za prevajalske prehode Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Ko Latte naleti na {datetime}
v predlogi, pokliče funkcijo za razčlenjevanje create()
. Njena
naloga je vrniti instanco DatetimeNode
.
Metoda print()
generira PHP kodo, ki se bo izvedla pri izrisovanju predloge. Kličemo metodo
$context->format()
, ki sestavi končni niz PHP kode za prevedeno predlogo. Prvi parameter,
'echo date('Y-m-d H:i:s') %line;'
, je maska, v katero se dopolnijo naslednji parametri. Nadomestni znak
%line
pove metodi format()
, naj uporabi drugi parameter, ki je $this->position
, in
vstavi komentar kot /* line 15 */
, ki povezuje generirano PHP kodo nazaj na izvirno vrstico predloge, kar je ključno
za razhroščevanje.
Lastnost $this->position
je podedovana iz osnovnega razreda Node
in jo samodejno nastavi
razčlenjevalnik Latte. Vsebuje objekt Latte\Compiler\Position, ki označuje, kje je bila
značka najdena v izvorni datoteki .latte
.
Metoda getIterator()
je temeljna za prevajalske prehode. Mora zagotoviti vsa podrejena vozlišča, vendar naš
preprost DatetimeNode
trenutno nima nobenih parametrov ali vsebine, torej nobenih podrejenih vozlišč. Kljub temu
mora metoda še vedno obstajati in biti generator, tj. ključna beseda yield
mora biti nekako prisotna v telesu
metode.
Registracija preko razširitve
Končno obvestimo Latte o novi znački. Ustvarite razred razširitve (npr.
MyLatteExtension.php
) in registrirajte značko v njeni metodi getTags()
.
<?php
namespace App\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Vrne seznam značk, ki jih ponuja ta razširitev.
* @return array<string, callable> Mapa: 'ime-značke' => funkcija-za-razčlenjevanje
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Kasneje tukaj registrirajte več značk
];
}
}
Nato registrirajte to razširitev v Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Ustvarite predlogo:
<p>Stran generirana: {datetime}</p>
Pričakovan izpis: <p>Stran generirana: 2023-10-27 11:00:00</p>
Povzetek te faze
Uspešno smo ustvarili osnovno lastno značko {datetime}
. Definirali smo njeno predstavitev v AST
(DatetimeNode
), obdelali njeno razčlenjevanje (create()
), določili, kako naj generira PHP kodo
(print()
), zagotovili, da so njeni otroci dostopni za prehod (getIterator()
), in jo registrirali
v Latte.
V naslednjem razdelku bomo to značko izboljšali tako, da bo sprejemala parametre, in pokazali, kako razčlenjevati izraze ter upravljati podrejena vozlišča.
Razčlenjevanje parametrov značke
Naša preprosta značka {datetime}
deluje, vendar ni zelo prilagodljiva. Izboljšajmo jo, da bo sprejemala
neobvezen parameter: formatni niz za funkcijo date()
. Zahtevana sintaksa bo {datetime $format}
.
Cilj: Prilagoditi {datetime}
tako, da sprejema neobvezen PHP izraz kot parameter, ki bo uporabljen kot
formatni niz za date()
.
Predstavitev TagParser
Preden prilagodimo kodo, je pomembno razumeti orodje, ki ga bomo uporabljali: Latte\Compiler\TagParser. Ko glavni razčlenjevalnik
Latte (TemplateParser
) naleti na Latte značko, kot je {datetime ...}
ali n:atribut, prenese
razčlenjevanje vsebine znotraj značke (del med {
in }
ali vrednost atributa) na specializiran
TagParser
.
Ta TagParser
deluje izključno s parametri značke. Njegova naloga je obdelati žetone, ki predstavljajo
te parametre. Ključno je, da mora obdelati celotno vsebino, ki mu je na voljo. Če se vaša funkcija za razčlenjevanje
konča, vendar TagParser
ni dosegel konca parametrov (preverjeno preko $tag->parser->isEnd()
), bo
Latte vrgel izjemo, saj to kaže, da so znotraj značke ostali nepričakovani žetoni. Nasprotno, če značka zahteva
parametre, morate na začetku vaše funkcije za razčlenjevanje poklicati $tag->expectArguments()
. Ta metoda
preveri, ali so parametri prisotni, in vrže koristno izjemo, če je bila značka uporabljena brez kakršnih koli parametrov.
TagParser
ponuja uporabne metode za razčlenjevanje različnih vrst parametrov:
parseExpression(): ExpressionNode
: Razčleni PHP-podoben izraz (spremenljivke, literale, operatorje, klice funkcij/metod itd.). Obravnava sintaktični sladkor Latte, kot je na primer obravnavanje preprostih alfanumeričnih nizov kot nizov v narekovajih (npr.foo
se razčleni, kot da bi bilo'foo'
).parseUnquotedStringOrExpression(): ExpressionNode
: Razčleni bodisi standardni izraz bodisi nenarekovajni niz. Nenarekovajni nizi so zaporedja, ki jih Latte dovoljuje brez narekovajev, pogosto uporabljena za stvari, kot so poti do datotek (npr.{include ../file.latte}
). Če razčleni nenarekovajni niz, vrneStringNode
.parseArguments(): ArrayNode
: Razčleni parametre, ločene z vejicami, potencialno s ključi, kot10, name: 'John', true
.parseModifier(): ModifierNode
: Razčleni filtre kot|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Razčleni PHP namige tipov kotint
,?string
,array|Foo
.
Za bolj zapletene ali nižje ravni potreb po razčlenjevanju lahko neposredno komunicirate s tokom žetonov preko
$tag->parser->stream
. Ta objekt ponuja metode za preverjanje in obdelavo posameznih žetonov:
$tag->parser->stream->is(...): bool
: Preveri, ali trenutni žeton ustreza kateri od določenih vrst (npr.Token::Php_Variable
) ali literalnih vrednosti (npr.'as'
), ne da bi ga porabil. Uporabno za pogled naprej.$tag->parser->stream->consume(...): Token
: Porabi trenutni žeton in premakne položaj toka naprej. Če so kot parametri podane pričakovane vrste/vrednosti žetonov in trenutni žeton ne ustreza, vržeCompileException
. Uporabite to, ko pričakujete določen žeton.$tag->parser->stream->tryConsume(...): ?Token
: Poskusi porabiti trenutni žeton samo če ustreza eni od določenih vrst/vrednosti. Če ustreza, porabi žeton in ga vrne. Če ne ustreza, pusti položaj toka nespremenjen in vrnenull
. Uporabite to za neobvezne žetone ali ko izbirate med različnimi sintaktičnimi potmi.
Posodobitev funkcije za razčlenjevanje create()
S tem razumevanjem prilagodimo metodo create()
v DatetimeNode
tako, da razčleni neobvezen formatni
parameter z uporabo $tag->parser
.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
// Dodamo javno lastnost za shranjevanje razčlenjenega vozlišča formatnega izraza
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Preverimo, ali obstajajo kakšni žetoni
if (!$tag->parser->isEnd()) {
// Razčlenimo parameter kot PHP-podoben izraz z uporabo TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... metodi print() in getIterator() bosta posodobljeni kasneje ...
}
Dodali smo javno lastnost $format
. V create()
zdaj uporabljamo
$tag->parser->isEnd()
za preverjanje, ali obstajajo parametri. Če obstajajo,
$tag->parser->parseExpression()
obdela žetone za izraz. Ker mora TagParser
obdelati vse vhodne
žetone, bo Latte samodejno vrgel napako, če uporabnik napiše nekaj nepričakovanega za formatnim izrazom (npr.
{datetime 'Y-m-d', unexpected}
).
Posodobitev metode print()
Zdaj prilagodimo metodo print()
tako, da uporablja razčlenjen formatni izraz, shranjen v
$this->format
. Če format ni bil podan ($this->format
je null
), moramo uporabiti
privzeti formatni niz, na primer 'Y-m-d H:i:s'
.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node izpiše PHP kodno predstavitev $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
V spremenljivko $formatNode
shranimo vozlišče AST, ki predstavlja formatni niz za PHP funkcijo
date()
. Tukaj uporabljamo operator ničnega združevanja (??
). Če je uporabnik podal parameter
v predlogi (npr. {datetime 'd.m.Y'}
), potem lastnost $this->format
vsebuje ustrezno vozlišče
(v tem primeru StringNode
z vrednostjo 'd.m.Y'
), in to vozlišče se uporabi. Če uporabnik ni podal
parametra (napisal je samo {datetime}
), je lastnost $this->format
null
, in namesto tega
ustvarimo nov StringNode
s privzetim formatom 'Y-m-d H:i:s'
. To zagotavlja, da $formatNode
vedno vsebuje veljavno vozlišče AST za format.
V maski 'echo date(%node) %line;'
je uporabljen nov nadomestni znak %node
, ki pove metodi
format()
, naj vzame prvi naslednji parameter (kar je naš $formatNode
), pokliče njegovo metodo
print()
(ki vrne njegovo PHP kodno predstavitev) in vstavi rezultat na mesto nadomestnega znaka.
Implementacija getIterator()
za podvozlišča
Naš DatetimeNode
ima zdaj podrejeno vozlišče: izraz $format
. Moramo to podrejeno vozlišče
omogočiti dostop prevajalskim prehodom tako, da ga zagotovimo v metodi getIterator()
. Ne pozabite zagotoviti
reference (&
), da omogočite prehodom potencialno zamenjavo vozlišča.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Zakaj je to ključno? Predstavljajte si prehod Sandbox, ki mora preveriti, ali parameter $format
ne vsebuje
prepovedanega klica funkcije (npr. {datetime dangerousFunction()}
). Če getIterator()
ne zagotovi
$this->format
, prehod Sandbox nikoli ne bi videl klica dangerousFunction()
znotraj parametra naše
značke, kar bi ustvarilo potencialno varnostno luknjo. Z zagotavljanjem mu omogočimo Sandboxu (in drugim prehodom), da
preverijo in potencialno spremenijo vozlišče izraza $format
.
Uporaba izboljšane značke
Značka zdaj pravilno obravnava neobvezen parameter:
Privzeti format: {datetime}
Lastni format: {datetime 'd.m.Y'}
Uporaba spremenljivke: {datetime $userDateFormatPreference}
{* To bi povzročilo napako po razčlenjevanju 'd.m.Y', ker je ", foo" nepričakovano *}
{* {datetime 'd.m.Y', foo} *}
Nato si bomo ogledali ustvarjanje parnih značk, ki obdelujejo vsebino med njimi.
Obravnavanje parnih značk
Do sedaj je bila naša značka {datetime}
samozapirajoča (konceptualno). Nima nobene vsebine med začetno
in končno značko. Vendar pa veliko uporabnih značk deluje z blokom vsebine predloge. Te se imenujejo parne značke.
Primeri vključujejo {if}...{/if}
, {block}...{/block}
ali lastno značko, ki jo bomo zdaj ustvarili:
{debug}...{/debug}
.
Ta značka nam bo omogočila vključitev informacij za razhroščevanje v naše predloge, ki naj bi bile vidne samo med razvojem.
Cilj: Ustvariti parno značko {debug}
, katere vsebina se izriše samo, če je aktivna specifična zastavica
“razvojnega načina”.
Predstavitev ponudnikov
Včasih vaše značke potrebujejo dostop do podatkov ali storitev, ki niso posredovane neposredno kot parametri predloge. Na primer, določanje, ali je aplikacija v razvojnem načinu, dostop do objekta uporabnika ali pridobivanje konfiguracijskih vrednosti. Latte zagotavlja mehanizem, imenovan ponudniki (Providers) za ta namen.
Ponudniki so registrirani v vaši razširitvi
z uporabo metode getProviders()
. Ta metoda vrne asociativno polje, kjer so ključi imena, pod katerimi bodo
ponudniki dostopni v izvajalni kodi predloge, vrednosti pa so dejanski podatki ali objekti.
Znotraj PHP kode, ki jo generira metoda print()
vaše značke, lahko do teh ponudnikov dostopate preko posebne
lastnosti objekta $this->global
. Ker je ta lastnost deljena med vsemi razširitvami, je dobra praksa predpona
imen vaših ponudnikov za preprečevanje potencialnih kolizij imen s ključnimi ponudniki Latte ali ponudniki iz drugih
razširitev tretjih oseb. Običajna konvencija je uporaba kratke, edinstvene predpone, povezane z vašim proizvajalcem ali imenom
razširitve. Za naš primer bomo uporabili predpono app
in zastavica razvojnega načina bo dostopna kot
$this->global->appDevMode
.
Ključna beseda yield
za razčlenjevanje vsebine
Kako povemo razčlenjevalniku Latte, naj obdela vsebino med {debug}
in {/debug}
? Tukaj pride
v poštev ključna beseda yield
.
Ko se yield
uporabi v funkciji create()
, funkcija postane PHP generator. Njegovo izvajanje se zaustavi in nadzor
se vrne glavnemu TemplateParser
. TemplateParser
nato nadaljuje z razčlenjevanjem vsebine predloge,
dokler ne naleti na ustrezno zapiralno značko ({/debug}
v našem primeru).
Ko je najdena zapiralna značka, TemplateParser
nadaljuje izvajanje naše funkcije create()
takoj za
stavkom yield
. Vrednost, ki jo vrne stavek yield
, je polje, ki vsebuje dva elementa:
AreaNode
, ki predstavlja razčlenjeno vsebino med začetno in končno značko.- Objekt
Tag
, ki predstavlja zapiralno značko (npr.{/debug}
).
Ustvarimo razred DebugNode
in njegovo metodo create
, ki uporablja yield
.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Javna lastnost za shranjevanje razčlenjene notranje vsebine
public AreaNode $content;
/**
* Funkcija za razčlenjevanje parne značke {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // opazite vrnjeni tip
{
$node = $tag->node = new self;
// Zaustavi razčlenjevanje, pridobi notranjo vsebino in končno značko, ko je najden {/debug}
[$node->content, $endTag] = yield;
return $node;
}
// ... print() in getIterator() bosta implementirani kasneje ...
}
Opomba: $endTag
je null
, če je značka uporabljena kot n:atribut, tj.
<div n:debug>...</div>
.
Implementacija print()
za pogojno izrisovanje
Metoda print()
mora zdaj generirati PHP kodo, ki ob izvajanju preveri ponudnika appDevMode
in izvede
kodo za notranjo vsebino le, če je zastavica true.
public function print(PrintContext $context): string
{
// Generira PHP stavek 'if', ki ob izvajanju preveri ponudnika
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Če je v razvojnem načinu, izpiše notranjo vsebino
%node
}
XX,
$this->position, // Za %line komentar
$this->content, // Vozlišče, ki vsebuje AST notranje vsebine
);
}
To je preprosto. Uporabljamo PrintContext::format()
za ustvarjanje standardnega PHP stavka if
.
Znotraj if
postavimo nadomestni znak %node
za $this->content
. Latte rekurzivno pokliče
$this->content->print($context)
za generiranje PHP kode za notranji del značke, vendar le, če
$this->global->appDevMode
ob izvajanju vrne true.
Implementacija getIterator()
za vsebino
Tako kot pri vozlišču parametra v prejšnjem primeru, ima naš DebugNode
zdaj podrejeno vozlišče:
AreaNode $content
. Moramo ga omogočiti dostop tako, da ga zagotovimo v getIterator()
:
public function &getIterator(): \Generator
{
// Zagotavlja referenco na vozlišče vsebine
yield $this->content;
}
To omogoča prevajalskim prehodom, da se spustijo v vsebino naše značke {debug}
, kar je pomembno, tudi če je
vsebina pogojno izrisana. Na primer, Sandbox mora analizirati vsebino ne glede na to, ali je appDevMode
true
ali false.
Registracija in uporaba
Registrirajte značko in ponudnika v vaši razširitvi:
class MyLatteExtension extends Extension
{
// Predpostavljamo, da je $isDevelopmentMode določen nekje (npr. iz konfiguracije)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Registracija nove značke
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Registracija ponudnika
];
}
}
// Pri registraciji razširitve:
$isDev = true; // Določite to na podlagi okolja vaše aplikacije
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
In njegova uporaba v predlogi:
<p>Običajna vsebina, vedno vidna.</p>
{debug}
<div class="debug-panel">
ID trenutnega uporabnika: {$user->id}
Čas zahteve: {=time()}
</div>
{/debug}
<p>Druga običajna vsebina.</p>
Integracija n:atributov
Latte ponuja priročen skrajšan zapis za mnoge parne značke: n:atributi. Če imate parno značko, kot je
{tag}...{/tag}
, in želite, da se njen učinek uporabi neposredno na enem samem HTML elementu, jo lahko pogosto
zapišete bolj jedrnato kot atribut n:tag
na tem elementu.
Za večino standardnih parnih značk, ki jih definirate (kot je naša {debug}
), bo Latte samodejno omogočil
ustrezno različico n:
atributa. Med registracijo vam ni treba storiti ničesar dodatnega:
{* Standardna uporaba parne značke *}
{debug}<div>Informacije za razhroščevanje</div>{/debug}
{* Enakovredna uporaba z n:atributom *}
<div n:debug>Informacije za razhroščevanje</div>
Obe različici bosta izrisali <div>
samo, če je $this->global->appDevMode
true. Predpone
inner-
in tag-
prav tako delujejo po pričakovanjih.
Včasih se mora logika vaše značke obnašati nekoliko drugače, odvisno od tega, ali je uporabljena kot standardna parna
značka ali kot n:atribut, ali če je uporabljena predpona kot n:inner-tag
ali n:tag-tag
. Objekt
Latte\Compiler\Tag
, posredovan vaši funkciji za razčlenjevanje create()
, zagotavlja te
informacije:
$tag->isNAttribute(): bool
: Vrnetrue
, če je značka razčlenjena kot n:atribut.$tag->prefix: ?string
: Vrne predpono, uporabljeno z n:atributom, kar je lahkonull
(ni n:atribut),Tag::PrefixNone
,Tag::PrefixInner
aliTag::PrefixTag
.
Zdaj, ko razumemo preproste značke, razčlenjevanje parametrov, parne značke, ponudnike in n:atribute, se lotimo bolj
zapletenega scenarija, ki vključuje značke, gnezdenih v drugih značkah, z uporabo naše značke {debug}
kot
izhodišča.
Vmesne značke
Nekatere parne značke omogočajo ali celo zahtevajo, da se druge značke pojavijo znotraj njih pred končno zapiralno
značko. Te se imenujejo vmesne značke. Klasični primeri vključujejo {if}...{elseif}...{else}...{/if}
ali
{switch}...{case}...{default}...{/switch}
.
Razširimo našo značko {debug}
tako, da podpira neobvezno klavzulo {else}
, ki bo izrisana, ko
aplikacija ni v razvojnem načinu.
Cilj: Prilagoditi {debug}
tako, da podpira neobvezno vmesno značko {else}
. Končna sintaksa
naj bi bila {debug} ... {else} ... {/debug}
.
Razčlenjevanje vmesnih značk z yield
Že vemo, da yield
zaustavi funkcijo za razčlenjevanje create()
in vrne razčlenjeno vsebino skupaj
s končno značko. Vendar yield
ponuja več nadzora: lahko mu posredujete polje imen vmesnih značk. Ko
razčlenjevalnik naleti na katero koli od teh določenih značk na isti ravni gnezdenja (tj. kot neposredne otroke
starševske značke, ne znotraj drugih blokov ali značk znotraj nje), prav tako ustavi razčlenjevanje.
Ko se razčlenjevanje ustavi zaradi vmesne značke, ustavi razčlenjevanje vsebine, nadaljuje generator create()
in preda nazaj delno razčlenjeno vsebino in vmesno značko samo (namesto končne zapiralne značke). Naša funkcija
create()
lahko nato obdela to vmesno značko (npr. razčleni njene parametre, če jih je imela) in ponovno uporabi
yield
za razčlenjevanje naslednjega dela vsebine do končne zapiralne značke ali druge pričakovane
vmesne značke.
Prilagodimo DebugNode::create()
tako, da pričakuje {else}
:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Vsebina za del {debug}
public AreaNode $thenContent;
// Neobvezna vsebina za del {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield in pričakovati bodisi {/debug} ali {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Preveriti, ali je bila značka, pri kateri smo se ustavili, {else}
if ($nextTag?->name === 'else') {
// Yield ponovno za razčlenjevanje vsebine med {else} in {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() in getIterator() bosta posodobljeni kasneje ...
}
Zdaj yield ['else']
pove Latteju, naj ustavi razčlenjevanje ne samo za {/debug}
, ampak tudi za
{else}
. Če je {else}
najden, bo $nextTag
vseboval objekt Tag
za
{else}
. Nato ponovno uporabimo yield
brez parametrov, kar pomeni, da zdaj pričakujemo samo končno
značko {/debug}
, in shranimo rezultat v $node->elseContent
. Če {else}
ni bil najden,
bi bil $nextTag
Tag
za {/debug}
(ali null
, če je uporabljen kot n:atribut) in
$node->elseContent
bi ostal null
.
Implementacija print()
z {else}
Metoda print()
mora odražati novo strukturo. Morala bi generirati PHP stavek if/else
, ki temelji na
ponudniku devMode
.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Koda za vejo 'then' (vsebina {debug})
} else {
%node // Koda za vejo 'else' (vsebina {else})
}
XX,
$this->position, // Številka vrstice za pogoj 'if'
$this->thenContent, // Prvi nadomestni znak %node
$this->elseContent ?? new NopNode, // Drugi nadomestni znak %node
);
}
To je standardna PHP struktura if/else
. Uporabljamo %node
dvakrat; format()
zaporedno
zamenja podana vozlišča. Uporabljamo ?? new NopNode
za izogibanje napakam, če je
$this->elseContent
null
– NopNode
preprosto ne izpiše ničesar.
Implementacija getIterator()
za obe vsebini
Zdaj imamo potencialno dve podrejeni vozlišči vsebine ($thenContent
in $elseContent
). Zagotoviti
moramo obe, če obstajata:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Uporaba izboljšane značke
Značka se zdaj lahko uporablja z neobvezno klavzulo {else}
:
{debug}
<p>Prikazovanje informacij za razhroščevanje, ker je devMode VKLOPLJEN.</p>
{else}
<p>Informacije za razhroščevanje so skrite, ker je devMode IZKLOPLJEN.</p>
{/debug}
Obravnavanje stanja in gnezdenja
Naši prejšnji primeri ({datetime}
, {debug}
) so bili relativno brez stanja znotraj svojih metod
print()
. Bodisi so neposredno izpisovali vsebino bodisi izvajali preprosto pogojno preverjanje na podlagi globalnega
ponudnika. Vendar pa morajo mnoge značke upravljati neko obliko stanja med izrisovanjem ali vključujejo vrednotenje
uporabniških izrazov, ki naj bi se zaradi učinkovitosti ali pravilnosti zagnali samo enkrat. Poleg tega moramo razmisliti, kaj
se zgodi, ko so naše lastne značke gnezdenje.
Ilustrirajmo te koncepte z ustvarjanjem značke {repeat $count}...{/repeat}
. Ta značka bo ponovila svojo
notranjo vsebino $count
-krat.
Cilj: Implementirati {repeat $count}
, ki ponovi svojo vsebino določeno število krat.
Potreba po začasnih & edinstvenih spremenljivkah
Predstavljajte si, da uporabnik napiše:
{repeat rand(1, 5)} Vsebina {/repeat}
Če bi naivno generirali PHP for
zanko na ta način v naši metodi print()
:
// Poenostavljena, NAPAČNA generirana koda
for ($i = 0; $i < rand(1, 5); $i++) {
// izpis vsebine
}
To bi bilo narobe! Izraz rand(1, 5)
bi bil ponovno ovrednoten pri vsaki iteraciji zanke, kar bi vodilo do
nepredvidljivega števila ponovitev. Izraz $count
moramo ovrednotiti enkrat pred začetkom zanke in shraniti
njegov rezultat.
Generirali bomo PHP kodo, ki najprej ovrednoti izraz števila in ga shrani v začasno izvajalno spremenljivko. Da bi
preprečili kolizije s spremenljivkami, ki jih definira uporabnik predloge, in notranjimi spremenljivkami Latte (kot je
$ʟ_...
), bomo uporabili konvencijo predpone $__
(dvojno podčrtaj) za naše začasne
spremenljivke.
Generirana koda bi potem izgledala takole:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// izpis vsebine
}
Zdaj razmislimo o gnezdenju:
{repeat $countA} {* Zunanja zanka *}
{repeat $countB} {* Notranja zanka *}
...
{/repeat}
{/repeat}
Če bi tako zunanja kot notranja značka {repeat}
generirali kodo, ki uporablja ista imena začasnih
spremenljivk (npr. $__count
in $__i
), bi notranja zanka prepisala spremenljivke zunanje zanke, kar bi
porušilo logiko.
Zagotoviti moramo, da so začasne spremenljivke, generirane za vsako instanco značke {repeat}
, edinstvene.
To dosežemo z uporabo PrintContext::generateId()
. Ta metoda vrne edinstveno celo število med fazo prevajanja. To
ID lahko pripnemo k imenom naših začasnih spremenljivk.
Torej namesto $__count
bomo generirali $__count_1
za prvo značko repeat, $__count_2
za
drugo itd. Podobno bomo za števec zanke uporabili $__i_1
, $__i_2
itd.
Implementacija RepeatNode
Ustvarimo razred vozlišča.
<?php
namespace App\Latte;
use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class RepeatNode extends StatementNode
{
public ExpressionNode $count;
public AreaNode $content;
/**
* Funkcija za razčlenjevanje za {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // zagotovi, da je $count podan
$node = $tag->node = new self;
// Razčleni izraz števila
$node->count = $tag->parser->parseExpression();
// Pridobivanje notranje vsebine
[$node->content] = yield;
return $node;
}
/**
* Generira PHP 'for' zanko z edinstvenimi imeni spremenljivk.
*/
public function print(PrintContext $context): string
{
// Generiranje edinstvenih imen spremenljivk
$id = $context->generateId();
$countVar = '$__count_' . $id; // npr. $__count_1, $__count_2, itd.
$iteratorVar = '$__i_' . $id; // npr. $__i_1, $__i_2, itd.
return $context->format(
<<<'XX'
// Ovrednotenje izraza števila *enkrat* in shranjevanje
%raw = (int) (%node);
// Zanka z uporabo shranjenega števila in edinstvene iteracijske spremenljivke
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Izrisovanje notranje vsebine
}
XX,
$countVar, // %0 - Spremenljivka za shranjevanje števila
$this->count, // %1 - Vozlišče izraza za število
$iteratorVar, // %2 - Ime iteracijske spremenljivke zanke
$this->position, // %3 - Komentar s številko vrstice za samo zanko
$this->content // %4 - Vozlišče notranje vsebine
);
}
/**
* Zagotavlja podrejena vozlišča (izraz števila in vsebina).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
Metoda create()
razčleni zahtevani izraz $count
z uporabo parseExpression()
. Najprej
se pokliče $tag->expectArguments()
. To zagotavlja, da je uporabnik podal nekaj za {repeat}
.
Medtem ko bi $tag->parser->parseExpression()
spodletel, če nič ne bi bilo podano, bi sporočilo o napaki
lahko bilo o nepričakovani sintaksi. Uporaba expectArguments()
zagotavlja veliko jasnejšo napako, ki posebej
navaja, da manjkajo parametri za značko {repeat}
.
Metoda print()
generira PHP kodo, odgovorno za izvajanje logike ponavljanja ob izvajanju. Začne z generiranjem
edinstvenih imen za začasne PHP spremenljivke, ki jih bo potrebovala.
Metoda $context->format()
je klicana z novim nadomestnim znakom %raw
, ki vstavi surov
niz, podan kot ustrezen parameter. Tukaj vstavi edinstveno ime spremenljivke, shranjeno v $countVar
(npr.
$__count_1
). Kaj pa %0.raw
in %2.raw
? To prikazuje pozicijske nadomestne znake.
Namesto samo %raw
, ki vzame naslednji razpoložljiv surovi parameter, %2.raw
eksplicitno vzame
parameter na indeksu 2 (kar je $iteratorVar
) in vstavi njegovo surovo nizovno vrednost. To nam omogoča ponovno
uporabo niza $iteratorVar
, ne da bi ga večkrat posredovali v seznamu parametrov za format()
.
Ta skrbno sestavljen klic format()
generira učinkovito in varno PHP zanko, ki pravilno obravnava izraz števila
in se izogiba kolizijam imen spremenljivk, tudi ko so značke {repeat}
gnezdenje.
Registracija in uporaba
Registrirajte značko v vaši razširitvi:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Registracija značke repeat
];
}
}
Uporabite jo v predlogi, vključno z gnezdenjem:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Notranja zanka</td>
{/repeat}
</tr>
{/repeat}
Ta primer prikazuje, kako obravnavati stanje (števce zank) in potencialne težave z gnezdenjem z uporabo začasnih
spremenljivk s predpono $__
in edinstvenih z ID-jem od PrintContext::generateId()
.
Čisti n:atributi
Medtem ko mnogi n:atributi
, kot sta n:if
ali n:foreach
, služijo kot priročne
okrajšave za njihove ustrezne parne značke ({if}...{/if}
, {foreach}...{/foreach}
), Latte omogoča tudi
definiranje značk, ki obstajajo samo v obliki n:atributa. Te se pogosto uporabljajo za spreminjanje atributov ali
obnašanja HTML elementa, na katerega so pripeti.
Standardni primeri, vgrajeni v Latte, vključujejo n:class
, ki pomaga dinamično sestaviti atribut
class
, in n:attr
, ki lahko nastavi več
poljubnih atributov.
Ustvarimo si lasten čisti n:atribut: n:confirm
, ki doda JavaScript potrditveno pogovorno okno pred izvedbo
dejanja (kot je sledenje povezavi ali pošiljanje obrazca).
Cilj: Implementirati n:confirm="'Ste prepričani?'"
, ki doda obravnavnik onclick
za
preprečitev privzetega dejanja, če uporabnik prekliče potrditveno pogovorno okno.
Implementacija ConfirmNode
Potrebujemo razred Node in funkcijo za razčlenjevanje.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
class ConfirmNode extends StatementNode
{
public ExpressionNode $message;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->message = $tag->parser->parseExpression();
return $node;
}
/**
* Generira kodo atributa 'onclick' s pravilnim ubežanjem znakov.
*/
public function print(PrintContext $context): string
{
// Zagotavlja pravilno ubežanje znakov za kontekste JavaScript in HTML atributa.
return $context->format(
<<<'XX'
echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
Metoda print()
generira PHP kodo, ki na koncu med izrisovanjem predloge izpiše HTML atribut
onclick="..."
. Obravnavanje gnezdenih kontekstov (JavaScript znotraj HTML atributa) zahteva skrbno ubežanje znakov.
Filter LR\Filters::escapeJs(%node)
se pokliče ob izvajanju in pravilno ubeži sporočilo za uporabo znotraj
JavaScripta (izpis bi bil kot "Sure?"
). Nato filter LR\Filters::escapeHtmlAttr(...)
ubeži znake, ki so
posebni v HTML atributih, tako da bi to spremenilo izpis v return confirm("Sure?")
. To
dvostopenjsko ubežanje znakov ob izvajanju zagotavlja, da je sporočilo varno za JavaScript in da je nastala JavaScript koda
varna za vstavljanje v HTML atribut onclick
.
Registracija in uporaba
Registrirajte n:atribut v vaši razširitvi. Ne pozabite na predpono n:
v ključu:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // Registracija n:confirm
];
}
}
Zdaj lahko uporabite n:confirm
na povezavah, gumbih ali elementih obrazca:
<a href="delete.php?id=123" n:confirm='"Ali res želite izbrisati element {$id}?"'>Izbriši</a>
Generirana HTML koda:
<a href="delete.php?id=123" onclick="return confirm("Ali res želite izbrisati element 123?")">Izbriši</a>
Ko uporabnik klikne na povezavo, brskalnik izvede kodo onclick
, prikaže potrditveno pogovorno okno in preide na
delete.php
samo, če uporabnik klikne “OK”.
Ta primer prikazuje, kako je mogoče ustvariti čisti n:atribut za spreminjanje obnašanja ali atributov svojega gostiteljskega
HTML elementa z generiranjem ustrezne PHP kode v njegovi metodi print()
. Ne pozabite na dvojno ubežanje znakov, ki
je pogosto zahtevano: enkrat za ciljni kontekst (JavaScript v tem primeru) in ponovno za kontekst HTML atributa.
Napredne teme
Medtem ko prejšnji razdelki pokrivajo osnovne koncepte, je tukaj nekaj naprednejših tem, na katere lahko naletite pri ustvarjanju lastnih Latte značk.
Načini izpisa značk
Objekt Tag
, posredovan vaši funkciji create()
, ima lastnost outputMode
. Ta lastnost
vpliva na to, kako Latte obravnava okoliške presledke in zamike, zlasti ko je značka uporabljena v svoji vrstici. To lastnost
lahko spremenite v vaši funkciji create()
.
Tag::OutputKeepIndentation
(Privzeto za večino značk kot{=...}
): Latte poskuša ohraniti zamik pred značko. Nove vrstice po znački so na splošno ohranjene. To je primerno za značke, ki izpisujejo vsebino v vrstici.Tag::OutputRemoveIndentation
(Privzeto za blokovne značke kot{if}
,{foreach}
): Latte odstrani začetni zamik in potencialno eno naslednjo novo vrstico. To pomaga ohranjati generirano PHP kodo čistejšo in preprečuje dodatne prazne vrstice v HTML izpisu, ki jih povzroči sama značka. Uporabite to za značke, ki predstavljajo kontrolne strukture ali bloke, ki sami ne bi smeli dodajati presledkov.Tag::OutputNone
(Uporabljajo ga značke kot{var}
,{default}
): Podobno kotRemoveIndentation
, vendar močneje signalizira, da značka sama ne proizvaja neposrednega izpisa, kar potencialno še bolj agresivno vpliva na obdelavo presledkov okoli nje. Primerno za deklarativne ali nastavitvene značke.
Izberite način, ki najbolje ustreza namenu vaše značke. Za večino strukturnih ali kontrolnih značk je običajno primeren
OutputRemoveIndentation
.
Dostop do starševskih/najbližjih značk
Včasih mora obnašanje značke biti odvisno od konteksta, v katerem se uporablja, natančneje, v kateri starševski
znački(ah) se nahaja. Objekt Tag
, posredovan vaši funkciji create()
, zagotavlja metodo
closestTag(array $classes, ?callable $condition = null): ?Tag
natančno za ta namen.
Ta metoda išče navzgor po hierarhiji trenutno odprtih značk (vključno s HTML elementi, ki so interno predstavljeni med
razčlenjevanjem) in vrne objekt Tag
najbližjega prednika, ki ustreza specifičnim kriterijem. Če ni najden noben
ustrezen prednik, vrne null
.
Polje $classes
določa, kakšno vrsto predniških značk iščete. Preverja, ali je povezano vozlišče predniške
značke ($ancestorTag->node
) instanca tega razreda.
function create(Tag $tag)
{
// Iskanje najbližje predniške značke, katere vozlišče je instanca ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Lahko dostopamo do instance ForeachNode same:
$foreachNode = $foreachTag->node;
}
}
Opazite $foreachTag->node
: To deluje samo zato, ker je konvencija pri razvoju Latte značk takoj dodeliti
ustvarjeno vozlišče k $tag->node
znotraj metode create()
, kot smo vedno počeli.
Včasih samo primerjava tipa vozlišča ni dovolj. Morda boste morali preveriti specifično lastnost potencialne predniške
značke ali njenega vozlišča. Neobvezni drugi parameter za closestTag()
je klicna funkcija (callable), ki prejme
potencialni predniški objekt Tag
in mora vrniti, ali je veljavno ujemanje.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Pogoj: blok mora biti dinamičen
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
Uporaba closestTag()
omogoča ustvarjanje značk, ki so kontekstno zavedne in uveljavljajo pravilno uporabo
znotraj strukture vaše predloge, kar vodi do bolj robustnih in razumljivih predlog.
Nadomestni znaki PrintContext::format()
Pogosto smo uporabljali PrintContext::format()
za generiranje PHP kode v metodah print()
naših
vozlišč. Sprejme niz maske in naslednje parametre, ki nadomestijo nadomestne znake v maski. Tukaj je povzetek razpoložljivih
nadomestnih znakov:
%node
: Parameter mora biti instancaNode
. Pokliče metodoprint()
vozlišča in vstavi nastali niz PHP kode.%dump
: Parameter je katera koli PHP vrednost. Izvozi vrednost v veljavno PHP kodo. Primerno za skalarje, polja, null.$context->format('echo %dump;', 'Hello')
→echo 'Hello';
$context->format('$arr = %dump;', [1, 2])
→$arr = [1, 2];
%raw
: Vstavi parameter neposredno v izhodno PHP kodo brez kakršnega koli ubežanja znakov ali prilagoditev. Uporabljajte previdno, predvsem za vstavljanje predgeneriranih fragmentov PHP kode ali imen spremenljivk.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: Parameter mora bitiExpression\ArrayNode
. Izpiše elemente polja, formatirane kot parametri za klic funkcije ali metode (ločeni z vejicami, obravnava poimenovane parametre, če so prisotni).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: Parameter mora biti objektPosition
(običajno$this->position
). Vstavi PHP komentar/* line X */
, ki označuje številko vrstice vira.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Generira PHP kodo, ki ob izvajanju ubeži notranji izraz z uporabo trenutnih kontekstno zavednih pravil ubežanja znakov.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: Parameter mora bitiModifierNode
. Generira PHP kodo, ki uporabi filtre, določene vModifierNode
, na notranji vsebini, vključno s kontekstno zavednim ubežanjem znakov, če ni onemogočeno z|noescape
.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Podobno kot%modify
, vendar namenjeno za spreminjanje blokov zajete vsebine (pogosto HTML).
Lahko eksplicitno sklicujete na parametre po njihovem indeksu (od nič): %0.node
, %1.dump
,
%2.raw
, itd. To omogoča ponovno uporabo parametra večkrat v maski, ne da bi ga ponovno posredovali v
format()
. Glejte primer značke {repeat}
, kjer sta bila uporabljena %0.raw
in
%2.raw
.
Primer kompleksnega razčlenjevanja parametrov
Medtem ko parseExpression()
, parseArguments()
, itd., pokrivajo mnoge primere, včasih potrebujete
bolj zapleteno logiko razčlenjevanja z uporabo nižje ravni TokenStream
, ki je na voljo preko
$tag->parser->stream
.
Cilj: Ustvariti značko {embedYoutube $videoID, width: 640, height: 480}
. Želimo razčleniti zahtevani ID
videa (niz ali spremenljivko), ki mu sledijo neobvezni pari ključ-vrednost za dimenzije.
<?php
namespace App\Latte;
class YoutubeNode extends StatementNode
{
public ExpressionNode $videoId;
public ?ExpressionNode $width = null;
public ?ExpressionNode $height = null;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
// Razčlenjevanje zahtevanega ID-ja videa
$node->videoId = $tag->parser->parseExpression();
// Razčlenjevanje neobveznih parov ključ-vrednost
$stream = $tag->parser->stream; // Pridobivanje toka žetonov
while ($stream->tryConsume(',')) { // Zahteva ločitev z vejico
// Pričakovanje identifikatorja 'width' ali 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Pričakovanje ločila dvopičja
$value = $tag->parser->parseExpression(); // Razčlenjevanje izraza vrednosti
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Neznan parameter '$key'. Pričakovano 'width' ali 'height'.", $keyToken->position);
}
}
return $node;
}
}
Ta raven nadzora vam omogoča definiranje zelo specifičnih in kompleksnih sintaks za vaše lastne značke z neposredno interakcijo s tokom žetonov.
Uporaba AuxiliaryNode
Latte ponuja splošna “pomožna” vozlišča za posebne situacije med generiranjem kode ali znotraj prevajalskih prehodov.
To sta AuxiliaryNode
in Php\Expression\AuxiliaryNode
.
Predstavljajte si AuxiliaryNode
kot prilagodljivo vsebniško vozlišče, ki svoje osnovne funkcionalnosti –
generiranje kode in izpostavljanje podrejenih vozlišč – prenese na parametre, podane v njegovem konstruktorju:
- Delegacija
print()
: Prvi parameter konstruktorja je PHP closure. Ko Latte pokliče metodoprint()
naAuxiliaryNode
, izvede to podano closure. Closure prejmePrintContext
in katera koli vozlišča, posredovana v drugem parametru konstruktorja, kar vam omogoča definiranje popolnoma lastne logike generiranja PHP kode ob izvajanju. - Delegacija
getIterator()
: Drugi parameter konstruktorja je polje objektovNode
. Ko mora Latte preiti otrokeAuxiliaryNode
(npr. med prevajalskimi prehodi), njegova metodagetIterator()
preprosto zagotovi vozlišča, navedena v tem polju.
Primer:
$node = new AuxiliaryNode(
// 1. Ta closure postane telo print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Ta vozlišča so zagotovljena z metodo getIterator() in posredovana zgornji closure
[$argumentNode1, $argumentNode2]
);
Latte ponuja dva različna tipa, ki temeljita na tem, kje morate vstaviti generirano kodo:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Uporabite to, ko morate generirati kos PHP kode, ki predstavlja izraz.Latte\Compiler\Nodes\AuxiliaryNode
: Uporabite to za bolj splošne namene, ko morate vstaviti blok PHP kode, ki predstavlja enega ali več stavkov.
Pomemben razlog za uporabo AuxiliaryNode
namesto standardnih vozlišč (kot je StaticMethodCallNode
)
znotraj vaše metode print()
ali prevajalskega prehoda je nadzor vidnosti za naslednje prevajalske prehode,
zlasti tiste, povezane z varnostjo, kot je Sandbox.
Razmislite o scenariju: Vaš prevajalski prehod mora oviti izraz, ki ga je podal uporabnik ($userExpr
), s klicem
specifične, zaupanja vredne pomožne funkcije myInternalSanitize($userExpr)
. Če ustvarite standardno vozlišče
new FunctionCallNode('myInternalSanitize', [$userExpr])
, bo popolnoma vidno za prehod AST. Če prehod Sandbox teče
kasneje in myInternalSanitize
ni na njegovem seznamu dovoljenih, lahko Sandbox ta klic blokira ali
spremeni, kar potencialno poruši notranjo logiko vaše značke, čeprav vi, avtor značke, veste, da je ta specifičen
klic varen in potreben. Zato lahko klic generirate neposredno znotraj closure AuxiliaryNode
.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... znotraj print() ali prevajalskega prehoda ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Neposredno generiranje PHP kode
$userExpr,
),
// POMEMBNO: Še vedno tukaj posredujte izvirno vozlišče uporabniškega izraza!
[$userExpr],
);
V tem primeru prehod Sandbox vidi AuxiliaryNode
, vendar ne analizira PHP kode, ki jo generira njegova
closure. Ne more neposredno blokirati klica myInternalSanitize
, generiranega znotraj closure.
Medtem ko je generirana PHP koda sama skrita pred prehodi, morajo biti vhodi v to kodo (vozlišča, ki predstavljajo
uporabniške podatke ali izraze) še vedno prehodni. Zato je drugi parameter konstruktorja AuxiliaryNode
ključen. Morate posredovati polje, ki vsebuje vsa izvirna vozlišča (kot je $userExpr
v zgornjem primeru),
ki jih vaša closure uporablja. getIterator()
AuxiliaryNode
bo zagotovil ta vozlišča, kar
omogoča prevajalskim prehodom, kot je Sandbox, da jih analizirajo za potencialne težave.
Najboljše prakse
- Jasen namen: Zagotovite, da ima vaša značka jasen in nujen namen. Ne ustvarjajte značk za naloge, ki jih je mogoče enostavno rešiti z uporabo filtrov ali funkcij.
- Pravilno implementirajte
getIterator()
: Vedno implementirajtegetIterator()
in zagotovite reference (&
) na vsa podrejena vozlišča (parametre, vsebino), ki so bila razčlenjena iz predloge. To je nujno za prevajalske prehode, varnost (Sandbox) in potencialne prihodnje optimizacije. - Javne lastnosti za vozlišča: Lastnosti, ki vsebujejo podrejena vozlišča, naredite javne, da jih lahko prevajalski prehodi po potrebi spreminjajo.
- Uporabljajte
PrintContext::format()
: Izkoristite metodoformat()
za generiranje PHP kode. Obravnava narekovaje, pravilno ubeži nadomestne znake in samodejno dodaja komentarje s številko vrstice. - Začasne spremenljivke (
$__
): Pri generiranju izvajalne PHP kode, ki potrebuje začasne spremenljivke (npr. za shranjevanje vmesnih vsot, števce zank), uporabljajte konvencijo predpone$__
za izogibanje kolizijam z uporabniškimi spremenljivkami in notranjimi spremenljivkami Latte$ʟ_
. - Gnezdenje in edinstveni ID-ji: Če je vaša značka lahko gnezdena ali potrebuje stanje, specifično za instanco ob
izvajanju, uporabite
$context->generateId()
znotraj vaše metodeprint()
za ustvarjanje edinstvenih pripon za vaše začasne spremenljivke$__
. - Ponudniki za zunanje podatke: Uporabljajte ponudnike (registrirane preko
Extension::getProviders()
) za dostop do izvajalnih podatkov ali storitev ($this->global->…) namesto trdega kodiranja vrednosti ali zanašanja na globalno stanje. Uporabljajte predpone proizvajalca za imena ponudnikov. - Razmislite o n:atributih: Če vaša parna značka logično deluje na enem samem HTML elementu, Latte verjetno
zagotavlja samodejno podporo
n:atributu
. Imejte to v mislih za udobje uporabnika. Če ustvarjate značko, ki spreminja atribut, razmislite, ali je čistin:atribut
najprimernejša oblika. - Testiranje: Pišite teste za vaše značke, ki pokrivajo tako razčlenjevanje različnih sintaktičnih vnosov kot pravilnost izpisa generirane PHP kode.
Z upoštevanjem teh smernic lahko ustvarite močne, robustne in vzdržljive lastne značke, ki se brezhibno integrirajo z mehanizmom predlog Latte.
Študij razredov vozlišč, ki so del Latte, je najboljši način za učenje vseh podrobnosti o postopku razčlenjevanja.