Kompilacijski prehodi
Kompilacijski prehodi zagotavljajo zmogljiv mehanizem za analizo in spreminjanje predlog Latte po njihovem razčlenjevanju v abstraktno sintaktično drevo (AST) in pred generiranjem končne PHP kode. To omogoča napredno manipulacijo s predlogami, optimizacije, varnostne preglede (kot je Sandbox) in zbiranje informacij o predlogah. Ta vodnik vas bo vodil skozi ustvarjanje lastnih kompilacijskih prehodov.
Kaj je kompilacijski prehod?
Za razumevanje vloge kompilacijskih prehodov si oglejte Proces kompilacije Latte. Kot lahko vidite, kompilacijski prehodi delujejo v ključni fazi, kar omogoča globok poseg med začetnim razčlenjevanjem in končnim izpisom kode.
V jedru je kompilacijski prehod preprosto PHP klicni objekt (kot funkcija, statična metoda ali metoda instance), ki sprejme
en argument: korenski vozel AST predloge, ki je vedno instanca Latte\Compiler\Nodes\TemplateNode
.
Primarni cilj kompilacijskega prehoda je običajno eden ali oba od naslednjih:
- Analiza: Prehajati skozi AST in zbirati informacije o predlogi (npr. najti vse definirane bloke, preveriti uporabo specifičnih značk, zagotoviti izpolnjevanje določenih varnostnih omejitev).
- Sprememba: Spremeniti strukturo AST ali atribute vozlov (npr. samodejno dodati HTML atribute, optimizirati določene kombinacije značk, zamenjati zastarele značke z novimi, implementirati pravila peskovnika).
Registracija
Kompilacijski prehodi so registrirani s pomočjo metode razširitve
getPasses()
. Ta metoda vrača asociativno polje, kjer so ključi edinstvena imena prehodov (uporabljena interno in za
razvrščanje) in vrednosti so PHP klicni objekti, ki implementirajo logiko prehoda.
Prehodi, registrirani z osnovnimi razširitvami Latte in vašimi lastnimi razširitvami, tečejo zaporedno. Vrstni red je
lahko pomemben, še posebej, če en prehod temelji na rezultatih ali spremembah drugega. Latte ponuja pomožni mehanizem za nadzor
tega vrstnega reda, če je potrebno; glejte dokumentacijo za Extension::getPasses()
za podrobnosti.
Primer AST
Za boljšo predstavo o AST dodajamo primer. To je izvorna predloga:
In 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') ) ) ) )
Prehajanje AST s pomočjo NodeTraverser
Ročno pisanje rekurzivnih funkcij za prehajanje kompleksne strukture AST je utrujajoče in nagnjeno k napakam. Latte ponuja posebno orodje za ta namen: Latte\Compiler\NodeTraverser. Ta razred implementira načrtovalski vzorec Visitor, zahvaljujoč kateremu je prehajanje AST sistematično in enostavno obvladljivo.
Osnovna uporaba vključuje ustvarjanje instance NodeTraverser
in klic njene metode traverse()
,
posredovanje korenskega vozla AST in enega ali dveh „visitor“ klicnih objektov:
Lahko posredujete samo enter
visitor, samo leave
visitor, ali oba, odvisno od vaših potreb.
enter(Node $node)
: Ta funkcija se izvede za vsak vozel pred tem, ko obiskovalec obišče
kateregakoli od otrok tega vozla. Uporabna je za:
- Zbiranje informacij med prehajanjem drevesa navzdol.
- Odločanje pred obdelavo otrok (kot odločitev, da jih preskočimo, glejte Optimizacija prehajanja).
- Potencialne spremembe vozla pred obiskom otrok (manj pogosto).
leave(Node $node)
: Ta funkcija se izvede za vsak vozel po tem, ko so bili vsi njegovi otroci (in
njihova celotna poddrevesa) v celoti obiskani (tako vstop kot izstop). Je najpogostejše mesto za:
Oba visitorja enter
in leave
lahko neobvezno vračata vrednost za vplivanje na proces prehajanja.
Vračanje null
(ali nič) nadaljuje prehajanje normalno, vračanje instance Node
zamenja trenutni vozel,
in vračanje posebnih konstant kot NodeTraverser::RemoveNode
ali NodeTraverser::StopTraversal
spremeni
tok, kot je razloženo v naslednjih odsekih.
Kako prehajanje deluje
NodeTraverser
interno uporablja metodo getIterator()
, ki jo mora implementirati vsak razred
Node
(kot je bilo obravnavano v Ustvarjanje
lastnih značk). Iterira skozi otroke, pridobljene s pomočjo getIterator()
, rekurzivno kliče
traverse()
na njih in zagotavlja, da sta enter
in leave
visitorja klicana v pravilnem
globinsko-prvem vrstnem redu za vsak vozel v drevesu, dostopen preko iteratorjev. To ponovno poudarja, zakaj je pravilno
implementiran getIterator()
v vaših lastnih vozlih značk absolutno nujen za pravilno delovanje kompilacijskih
prehodov.
Napišimo preprost prehod, ki šteje, kolikokrat je v predlogi uporabljena značka {do}
(predstavljena z
Latte\Essential\Nodes\DoNode
).
V tem primeru smo potrebovali samo visitor enter
za preverjanje tipa vsakega obiskanega vozla.
Nato bomo preučili, kako ti visitorji dejansko spreminjajo AST.
Spreminjanje AST
Eden od glavnih namenov kompilacijskih prehodov je spreminjanje abstraktnega sintaktičnega drevesa. To omogoča zmogljive
transformacije, optimizacije ali uveljavljanje pravil neposredno na strukturi predloge pred generiranjem PHP kode.
NodeTraverser
ponuja več načinov, kako to doseči znotraj visitorjev enter
in leave
.
Pomembna opomba: Spreminjanje AST zahteva previdnost. Napačne spremembe – kot odstranitev osnovnih vozlov ali zamenjava vozla z nezdružljivim tipom – lahko vodijo do napak med generiranjem kode ali povzročijo nepričakovano vedenje med izvajanjem programa. Vedno temeljito testirajte svoje modifikacijske prehode.
Spreminjanje lastnosti vozlov
Najenostavnejši način za spreminjanje drevesa je neposredna sprememba javnih lastnosti vozlov, obiskanih med prehajanjem. Vsi vozli shranjujejo svoje razčlenjene argumente, vsebino ali atribute v javnih lastnostih.
Primer: Ustvarimo prehod, ki najde vse statične tekstovne vozle (TextNode
, ki predstavljajo običajen HTML
ali tekst zunaj Latte značk) in pretvori njihovo vsebino v velike črke neposredno v AST.
V tem primeru visitor enter
preverja, ali je trenutni $node
tipa TextNode
. Če je,
neposredno posodobimo njegovo javno lastnost $content
s pomočjo mb_strtoupper()
. To neposredno
spremeni vsebino statičnega teksta, shranjenega v AST pred generiranjem PHP kode. Ker spreminjamo objekt neposredno, nam
ni treba ničesar vračati iz visitorja.
Učinek: Če je predloga vsebovala <p>Hello</p>{= $var }<span>World</span>
, bo po tem
prehodu AST predstavljal nekaj takega: <p>HELLO</p>{= $var }<span>WORLD</span>
. To NE VPLIVA
na vsebino $var.
Zamenjava vozlov
Močnejša tehnika spreminjanja je popolna zamenjava vozla z drugim. To se naredi z vračanjem nove instance
Node
iz visitorja enter
ali leave
. NodeTraverser
nato zamenja prvotni
vozel z vrnjenim v strukturi starševskega vozla.
Primer: Ustvarimo prehod, ki najde vse uporabe konstante PHP_VERSION
(predstavljene z
ConstantFetchNode
) in jih zamenja neposredno z nizovnim literalom (StringNode
), ki vsebuje
dejansko različico PHP, zaznano med kompilacijo. To je oblika optimizacije v času kompilacije.
Tu visitor leave
identificira specifičen ConstantFetchNode
za PHP_VERSION
. Nato ustvari
popolnoma nov StringNode
, ki vsebuje vrednost konstante PHP_VERSION
v času kompilacije.
Z vračanjem tega $newNode
pove traverserju, naj zamenja prvotni ConstantFetchNode
v AST.
Učinek: Če je predloga vsebovala {= PHP_VERSION }
in kompilacija teče na PHP 8.2.1, bo AST po tem prehodu
učinkovito predstavljal {= '8.2.1' }
.
Izbira enter
vs. leave
za zamenjavo:
- Uporabite
leave
, če ustvarjanje novega vozla temelji na rezultatih obdelave otrok starega vozla, ali če želite preprosto zagotoviti, da so otroci obiskani pred zamenjavo (pogosta praksa). - Uporabite
enter
, če želite zamenjati vozel pred tem, ko so njegovi otroci sploh obiskani.
Odstranjevanje vozlov
Lahko popolnoma odstranite vozel iz AST z vračanjem posebne konstante NodeTraverser::RemoveNode
iz
visitorja.
Primer: Odstranimo vse komentarje predloge ({* ... *}
), ki so predstavljeni z CommentNode
v AST, generiranem s strani jedra Latte (čeprav so običajno obdelani prej, to služi kot primer).
Opozorilo: Uporabljajte RemoveNode
previdno. Odstranitev vozla, ki vsebuje osnovno vsebino ali vpliva na
strukturo (kot odstranitev vsebinskega vozla cikla), lahko vodi do poškodovanih predlog ali neveljavne generirane kode.
Najvarnejše je za vozle, ki so resnično neobvezni ali samostojni (kot komentarji ali razhroščevalne značke) ali za prazne
strukturne vozle (npr. prazen FragmentNode
je lahko v nekaterih kontekstih varno odstranjen s prehodom za
čiščenje).
Te tri metode – spreminjanje lastnosti, zamenjava vozlov in odstranjevanje vozlov – zagotavljajo osnovna orodja za manipulacijo z AST znotraj vaših kompilacijskih prehodov.
Optimizacija prehajanja
AST predlog je lahko precej velik, potencialno vsebujoč tisoče vozlov. Prehajanje vsakega posameznega vozla je lahko
nepotrebno in vpliva na zmogljivost kompilacije, če vaš prehod zanima samo specifične dele drevesa. NodeTraverser
ponuja načine za optimizacijo prehajanja:
Preskakovanje otrok
Če veste, da ko naletite na določen tip vozla, nobeden od njegovih potomcev ne more vsebovati vozlov, ki jih iščete, lahko
traverserju poveste, naj preskoči obisk njegovih otrok. To se naredi z vračanjem konstante
NodeTraverser::DontTraverseChildren
iz visitorja enter
. S tem izpustite cele veje pri prehodu,
kar potencialno prihrani znaten čas, še posebej v predlogah s kompleksnimi PHP izrazi znotraj značk.
Ustavitev prehajanja
Če vaš prehod potrebuje najti samo prvi pojav nečesa (specifičen tip vozla, izpolnitev pogoja), lahko popolnoma
ustavite celoten proces prehajanja, takoj ko to najdete. To dosežete z vračanjem konstante
NodeTraverser::StopTraversal
iz visitorja enter
ali leave
. Metoda traverse()
preneha obiskovati katerekoli nadaljnje vozle. To je zelo učinkovito, če potrebujete samo prvo ujemanje v potencialno zelo
velikem drevesu.
Uporaben pomočnik NodeHelpers
Medtem ko NodeTraverser
ponuja fino stopenjsko kontrolo, Latte ponuja tudi praktičen pomožni razred, Latte\Compiler\NodeHelpers, ki enkapsulira
NodeTraverser
za več pogostih nalog iskanja in analize, pogosto zahtevajoč manj pripravljalne kode.
find(Node $startNode, callable $filter): array
Ta statična metoda najde vse vozle v poddrevesu, ki se začne na $startNode
(vključno), ki izpolnjujejo
povratni klic $filter
. Vrača polje ujemajočih se vozlov.
Primer: Najti vse vozle spremenljivk (VariableNode
) v celotni predlogi.
findFirst(Node $startNode, callable $filter): ?Node
Podobno kot find
, vendar ustavi prehajanje takoj po najdbi prvega vozla, ki izpolnjuje povratni klic
$filter
. Vrača najden objekt Node
ali null
, če ni najden noben ujemajoč se vozel. To je
v bistvu praktičen ovoj okoli NodeTraverser::StopTraversal
.
Primer: Najti vozel {parameters}
(enako kot ročni primer prej, vendar krajše).
toValue(ExpressionNode $node, bool $constants = false): mixed
Ta statična metoda poskuša ovrednotiti ExpressionNode
v času kompilacije in vrniti njegovo ustrezno PHP
vrednost. Deluje zanesljivo samo za preproste literalne vozle (StringNode
, IntegerNode
,
FloatNode
, BooleanNode
, NullNode
) in instance ArrayNode
, ki vsebujejo samo
take ovrednotljive elemente.
Če je $constants
nastavljen na true
, bo poskušal rešiti tudi ConstantFetchNode
in
ClassConstantFetchNode
s preverjanjem defined()
in uporabo constant()
.
Če vozel vsebuje spremenljivke, klice funkcij ali druge dinamične elemente, ga ni mogoče ovrednotiti v času kompilacije in
metoda vrže InvalidArgumentException
.
Primer uporabe: Pridobivanje statične vrednosti argumenta značke med kompilacijo za odločanje v času kompilacije.
toText(?Node $node): ?string
Ta statična metoda je uporabna za ekstrakcijo preproste tekstovne vsebine iz preprostih vozlov. Deluje primarno z:
TextNode
: Vrača njegov$content
.FragmentNode
: Združi rezultattoText()
za vse njegove otroke. Če kateri otrok ni pretvorljiv v tekst (npr. vsebujePrintNode
), vrnenull
.NopNode
: Vrača prazen niz.- Drugi tipi vozlov: Vrača
null
.
Primer uporabe: Pridobivanje statične tekstovne vsebine vrednosti HTML atributa ali preprostega HTML elementa za analizo med kompilacijskim prehodom.
NodeHelpers
lahko poenostavi vaše kompilacijske prehode z zagotavljanjem pripravljenih rešitev za pogoste
naloge prehajanja in analize AST.
Praktični primeri
Uporabimo koncepte prehajanja in spreminjanja AST za reševanje nekaterih praktičnih problemov. Ti primeri prikazujejo pogoste vzorce, uporabljene v kompilacijskih prehodih.
Samodejno dodajanje loading="lazy"
k
<img>
Sodobni brskalniki podpirajo nativno leno nalaganje za slike s pomočjo atributa loading="lazy"
. Ustvarimo
prehod, ki samodejno doda ta atribut vsem značkam <img>
, ki še nimajo atributa loading
.
Pojasnilo:
- Visitor
enter
išče vozleHtml\ElementNode
z imenomimg
. - Iterira skozi obstoječe atribute (
$node->attributes->children
) in preverja, ali je atributloading
že prisoten. - Če ni najden, ustvari nov
Html\AttributeNode
, ki predstavljaloading="lazy"
.
Preverjanje klicev funkcij
Kompilacijski prehodi so osnova Latte Sandboxa. Čeprav je dejanski Sandbox sofisticiran, lahko demonstriramo osnovni princip preverjanja prepovedanih klicev funkcij.
Cilj: Preprečiti uporabo potencialno nevarne funkcije shell_exec
znotraj izrazov predloge.
Pojasnilo:
- Definiramo seznam prepovedanih imen funkcij.
- Visitor
enter
preverjaFunctionCallNode
. - Če je ime funkcije (
$node->name
) statičenNameNode
, preverjamo njegovo nizovno predstavitev v malih črkah proti našemu prepovedanemu seznamu. - Če je najdena prepovedana funkcija, vržemo
Latte\SecurityViolationException
, ki jasno označuje kršitev varnostnega pravila in ustavi kompilacijo.
Ti primeri prikazujejo, kako lahko kompilacijske prehode z uporabo NodeTraverser
izkoristimo za analizo,
samodejne spremembe in uveljavljanje varnostnih omejitev interakcij neposredno s strukturo AST predloge.
Najboljše prakse
Pri pisanju kompilacijskih prehodov imejte v mislih te smernice za ustvarjanje robustnih, vzdržljivih in učinkovitih razširitev:
- Vrstni red je pomemben: Zavedajte se vrstnega reda, v katerem tečejo prehodi. Če vaš prehod temelji na strukturi
AST, ustvarjeni z drugim prehodom (npr. osnovni prehodi Latte ali drug lasten prehod), ali če drugi prehodi lahko temeljijo na
vaših spremembah, uporabite mehanizem razvrščanja, ki ga ponuja
Extension::getPasses()
, za definiranje odvisnosti (before
/after
). Glejte dokumentacijo zaExtension::getPasses()
za podrobnosti. - Ena odgovornost: Prizadevajte si za prehode, ki opravljajo eno dobro definirano nalogo. Za kompleksne transformacije razmislite o razdelitvi logike na več prehodov – morda enega za analizo in drugega za spremembo na podlagi rezultatov analize. To izboljšuje preglednost in testabilnost.
- Zmogljivost: Ne pozabite, da kompilacijski prehodi dodajajo čas kompilacije predloge (čeprav se to običajno zgodi
samo enkrat, dokler se predloga ne spremeni). Izogibajte se računsko zahtevnim operacijam v vaših prehodih, če je le mogoče.
Izkoristite optimizacije prehajanja kot
NodeTraverser::DontTraverseChildren
inNodeTraverser::StopTraversal
, kadarkoli veste, da vam ni treba obiskati določenih delov AST. - Uporabljajte
NodeHelpers
: Za pogoste naloge, kot je iskanje specifičnih vozlov ali statično vrednotenje preprostih izrazov, preverite, aliLatte\Compiler\NodeHelpers
ponuja primerno metodo, preden pišete lastno logikoNodeTraverser
. To lahko prihrani čas in zmanjša količino pripravljalne kode. - Obravnavanje napak: Če vaš prehod zazna napako ali neveljavno stanje v AST predloge, vržite
Latte\CompileException
(aliLatte\SecurityViolationException
za varnostne težave) z jasnim sporočilom in ustreznim objektomPosition
(običajno$node->position
). To zagotavlja uporabno povratno informacijo razvijalcu predloge. - Idempotenca (če je mogoče): Idealno bi bilo, če bi zagon vašega prehoda večkrat na istem AST proizvedel enak rezultat kot njegov enkratni zagon. To ni vedno izvedljivo, vendar poenostavlja razhroščevanje in razmišljanje o interakcijah prehodov, če je to doseženo. Na primer, zagotovite, da vaš modifikacijski prehod preveri, ali je bila sprememba že uporabljena, preden jo ponovno uporabi.
Z upoštevanjem teh praks lahko učinkovito izkoristite kompilacijske prehode za razširitev zmogljivosti Latte na zmogljiv in zanesljiv način, kar prispeva k varnejšemu, optimiziranemu ali funkcionalno bogatejšemu obdelovanju predlog.