Egyéni tagek létrehozása
Ez az oldal átfogó útmutatót nyújt az egyéni tagek létrehozásához a Latte-ban. Mindent megvitatunk az egyszerű tagektől a bonyolultabb, beágyazott tartalommal és specifikus parzolási igényekkel rendelkező forgatókönyvekig, építve arra a megértésre, hogy a Latte hogyan fordítja a sablonokat.
Az egyéni tagek a legmagasabb szintű ellenőrzést biztosítják a sablon szintaxisa és a renderelési logika felett, de egyben a legbonyolultabb bővítési pontot is jelentik. Mielőtt úgy döntene, hogy egyéni taget hoz létre, mindig fontolja meg, hogy nincs egyszerűbb megoldás, vagy hogy nem létezik-e már megfelelő tag a standard készletben. Csak akkor használjon egyéni tageket, ha az egyszerűbb alternatívák nem elegendőek az Ön igényeihez.
A fordítási folyamat megértése
Az egyéni tagek hatékony létrehozásához hasznos elmagyarázni, hogyan dolgozza fel a Latte a sablonokat. Ennek a folyamatnak a megértése tisztázza, hogy a tagek miért éppen így vannak strukturálva, és hogyan illeszkednek a tágabb kontextusba.
A sablon fordítása a Latte-ban, leegyszerűsítve, a következő kulcsfontosságú lépéseket tartalmazza:
- Lexikális elemzés: A lexer beolvassa a sablon forráskódját (a
.latte
fájlt), és kis, különálló részekre, úgynevezett tokenekre bontja (pl.{
,foreach
,$variable
,}
, HTML szöveg stb.). - Parzolás: A parser veszi ezt a tokenfolyamot, és egy értelmes fastruktúrát épít belőle, amely a sablon logikáját és tartalmát reprezentálja. Ezt a fát absztrakt szintaxisfának (AST) nevezik.
- Fordítási menetek: A PHP kód generálása előtt a Latte fordítási meneteket futtat. Ezek olyan függvények, amelyek végigjárják a teljes AST-t, és módosíthatják azt, vagy információkat gyűjthetnek. Ez a lépés kulcsfontosságú az olyan funkciókhoz, mint a biztonság (Sandbox) vagy az optimalizálás.
- Kódgenerálás: Végül a fordító végigjárja a (potenciálisan módosított) AST-t, és generálja a megfelelő PHP osztálykódot. Ez a PHP kód az, ami ténylegesen rendereli a sablont futás közben.
- Gyorsítótárazás: A generált PHP kód a lemezre kerül mentésre, ami a későbbi rendereléseket nagyon gyorssá teszi, mivel az 1–4. lépések kihagyásra kerülnek.
Valójában a fordítás egy kicsit bonyolultabb. A Latte két lexerrel és parserrel rendelkezik: egy a HTML sablonhoz és egy másik a tageken belüli PHP-szerű kódhoz. És a parzolás sem a tokenizálás után történik, hanem a lexer és a parser párhuzamosan fut két “szálon”, és koordinálnak. Hidd el, ennek a programozása rakétatudomány volt :-)
Az egész folyamat, a sablon tartalmának betöltésétől a parzoláson át a végső fájl generálásáig, ezzel a kóddal szekvenálható, amellyel kísérletezhet és kiírathatja a köztes eredményeket:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Egy tag anatómiája
Egy teljesen működőképes egyéni tag létrehozása a Latte-ban több összekapcsolt részből áll. Mielőtt belekezdenénk a implementációba, értsük meg az alapvető koncepciókat és terminológiát, a HTML és a Document Object Model (DOM) analógiáját használva.
Tagek vs. Csomópontok (Analógia a HTML-lel)
A HTML-ben tageket írunk, mint <p>
vagy <div>...</div>
. Ezek a tagek a
forráskódban lévő szintaxist jelentik. Amikor a böngésző parzolja ezt a HTML-t, létrehoz egy memóriabeli
reprezentációt, amelyet Document Object Model (DOM)-nak neveznek. A DOM-ban a HTML tageket csomópontok
reprezentálják (konkrétan Element
csomópontok a JavaScript DOM terminológiájában). Ezekkel a
csomópontokkal dolgozunk programozottan (pl. a JavaScript document.getElementById(...)
egy Element
csomópontot ad vissza). A tag csak egy szöveges reprezentáció a forrásfájlban; a csomópont egy objektum reprezentáció a
logikai fában.
A Latte hasonlóan működik:
- A
.latte
sablonfájlban Latte tageket ír, mint{foreach ...}
és{/foreach}
. Ez az a szintaxis, amellyel Ön, mint sablon szerző dolgozik. - Amikor a Latte parzolja a sablont, egy Absztrakt Szintaxisfát (AST) épít. Ez a fa csomópontokból áll. Minden Latte tag, HTML elem, szövegrész vagy kifejezés a sablonban egy vagy több csomóponttá válik ebben a fában.
- Az AST összes csomópontjának alaposztálya a
Latte\Compiler\Node
. Ahogy a DOM-nak különböző típusú csomópontjai vannak (Element, Text, Comment), úgy a Latte AST-jének is különböző típusú csomópontjai vannak. Találkozni fog aLatte\Compiler\Nodes\TextNode
-dal a statikus szöveghez, aLatte\Compiler\Nodes\Html\ElementNode
-dal a HTML elemekhez, aLatte\Compiler\Nodes\Php\ExpressionNode
-dal a tageken belüli kifejezésekhez, és kulcsfontosságúan az egyéni tagekhez, aLatte\Compiler\Nodes\StatementNode
-ból öröklődő csomópontokkal.
Miért StatementNode
?
A HTML elemek (Html\ElementNode
) elsősorban struktúrát és tartalmat reprezentálnak. A PHP kifejezések
(Php\ExpressionNode
) értékeket vagy számításokat reprezentálnak. De mi a helyzet az olyan Latte tagekkel, mint
{if}
, {foreach}
vagy a saját {datetime}
tagünk? Ezek a tagek akciókat hajtanak
végre, vezérlik a programfolyamatot, vagy logikán alapuló kimenetet generálnak. Funkcionális egységek, amelyek a
Latte-t erőteljes sablon motorrá teszik, nem csak egy jelölőnyelvvé.
A programozásban az ilyen akciókat végrehajtó egységeket gyakran “statements”-nek (utasításoknak) nevezik. Ezért
az ezeket a funkcionális Latte tageket reprezentáló csomópontok tipikusan a
Latte\Compiler\Nodes\StatementNode
-ból öröklődnek. Ez megkülönbözteti őket a tisztán strukturális
csomópontoktól (mint a HTML elemek) vagy az értékeket reprezentáló csomópontoktól (mint a kifejezések).
Kulcsfontosságú komponensek
Nézzük át a fő komponenseket, amelyek egy egyéni tag létrehozásához szükségesek:
Tag parzoló függvény
- Ez a PHP callable függvény parzolja a Latte tag szintaxisát (
{...}
) a forrássablonban. - Információkat kap a tagről (mint a neve, pozíciója és hogy n:attribútum-e) a Latte\Compiler\Tag objektumon keresztül.
- Elsődleges eszköze az argumentumok és kifejezések parzolására a tag határolóin belül a Latte\Compiler\TagParser objektum, amely a
$tag->parser
-en keresztül érhető el (ez egy másik parser, mint az, amelyik az egész sablont parzolja). - Páros tagek esetén a
yield
segítségével jelzi a Latte-nak, hogy parzolja a kezdő és záró tag közötti belső tartalmat. - A parzoló függvény végső célja egy csomópont osztály példányának létrehozása és visszaadása, amely hozzáadódik az AST-hez.
- Szokás (bár nem kötelező) a parzoló függvényt statikus metódusként (gyakran
create
-nek nevezve) implementálni közvetlenül a megfelelő csomópont osztályban. Ez a parzolási logikát és a csomópont reprezentációját szépen egy csomagban tartja, lehetővé teszi az osztály privát/védett elemeihez való hozzáférést, ha szükséges, és javítja a szervezést.
Csomópont osztály
- Reprezentálja a tag logikai funkcióját az Absztrakt Szintaxisfában (AST).
- Tartalmazza a parzolt információkat (mint argumentumok vagy tartalom) publikus property-ként. Ezek a property-k gyakran
más
Node
példányokat tartalmaznak (pl.ExpressionNode
a parzolt argumentumokhoz,AreaNode
a parzolt tartalomhoz). - A
print(PrintContext $context): string
metódus generálja a PHP kódot (utasítást vagy utasítássorozatot), amely végrehajtja a tag akcióját a sablon renderelése során. - A
getIterator(): \Generator
metódus hozzáférhetővé teszi a gyermek csomópontokat (argumentumok, tartalom) a fordítási menetek számára. Referenciákat (&
) kell biztosítania, hogy lehetővé tegye a menetek számára a potenciális módosítást vagy a gyermek csomópontok cseréjét. - Miután az egész sablon AST-vé parzolódott, a Latte egy sor fordítási menetet futtat. Ezek a menetek végigjárják a teljes
AST-t a minden csomópont által biztosított
getIterator()
metódus segítségével. Ellenőrizhetik a csomópontokat, információkat gyűjthetnek, és akár módosíthatják is a fát (pl. a csomópontok publikus property-jeinek megváltoztatásával vagy a csomópontok teljes cseréjével). Ez a tervezés, amely egy komplexgetIterator()
-t igényel, alapvető. Lehetővé teszi az olyan erőteljes funkciók számára, mint a Sandbox, hogy elemezzék és potenciálisan megváltoztassák a sablon bármely részének viselkedését, beleértve az Ön egyéni tagjeit is, biztosítva a biztonságot és a konzisztenciát.
Regisztráció kiterjesztésen keresztül
- Tájékoztatnia kell a Latte-t az új tagről és arról, hogy melyik parzoló függvényt kell hozzá használni. Ez egy Latte kiterjesztésen belül történik.
- A kiterjesztés osztályán belül implementálja a
getTags(): array
metódust. Ez a metódus egy asszociatív tömböt ad vissza, ahol a kulcsok a tag nevek (pl.'mytag'
,'n:myattribute'
), az értékek pedig a PHP callable függvények, amelyek a megfelelő parzoló függvényeiket reprezentálják (pl.MyNamespace\DatetimeNode::create(...)
).
Összefoglalás: A tag parzoló függvény átalakítja a tag forráskódját a sablonban egy AST
csomóponttá. A csomópont osztály ezután képes átalakítani önmagát futtatható PHP kóddá a
fordított sablonhoz, és hozzáférhetővé teszi a gyermek csomópontjait a fordítási menetek számára a
getIterator()
-on keresztül. A regisztráció kiterjesztésen keresztül összekapcsolja a tag nevét a
parzoló függvénnyel, és tudatja a Latte-val.
Most megvizsgáljuk, hogyan implementáljuk ezeket a komponenseket lépésről lépésre.
Egyszerű tag létrehozása
Kezdjünk bele az első egyéni Latte tag létrehozásába. Egy nagyon egyszerű példával kezdünk: egy
{datetime}
nevű taggel, amely kiírja az aktuális dátumot és időt. Kezdetben ez a tag nem fogad
argumentumokat, de később javítjuk a “Tag argumentumok parzolása” szakaszban.
Nincs belső tartalma sem.
Ez a példa végigvezet az alapvető lépéseken: a csomópont osztály definiálása, a print()
és
getIterator()
metódusainak implementálása, a parzoló függvény létrehozása, és végül a tag
regisztrálása.
Cél: Implementálni a {datetime}
taget az aktuális dátum és idő kiírására a PHP date()
függvényével.
A csomópont osztály létrehozása
Először szükségünk van egy osztályra, amely reprezentálja a tagünket az Absztrakt Szintaxisfában (AST). Ahogy fentebb
tárgyaltuk, a Latte\Compiler\Nodes\StatementNode
-ból öröklünk.
Hozzon létre egy fájlt (pl. DatetimeNode.php
) és definiálja az osztályt:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Tag parzoló függvény, amelyet akkor hívunk meg, ha {datetime} található.
*/
public static function create(Tag $tag): self
{
// Az egyszerű tagünk jelenleg nem fogad argumentumokat, így nem kell semmit parzolnunk
$node = $tag->node = new self;
return $node;
}
/**
* Generálja a PHP kódot, amely a sablon renderelésekor fut le.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Hozzáférést biztosít a gyermek csomópontokhoz a Latte fordítási menetei számára.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Amikor a Latte találkozik a {datetime}
taggel a sablonban, meghívja a create()
parzoló
függvényt. Ennek feladata egy DatetimeNode
példány visszaadása.
A print()
metódus generálja a PHP kódot, amely a sablon renderelésekor fut le. Meghívjuk a
$context->format()
metódust, amely összeállítja a végső PHP kódsztringet a fordított sablonhoz. Az első
argumentum, 'echo date('Y-m-d H:i:s') %line;'
, egy maszk, amelybe a következő paraméterek kerülnek
beillesztésre. A %line
helyettesítő karakter azt mondja a format()
metódusnak, hogy használja a
második argumentumot, ami a $this->position
, és illesszen be egy kommentárt, mint /* line 15 */
,
amely összekapcsolja a generált PHP kódot a sablon eredeti sorával, ami kulcsfontosságú a debuggoláshoz.
A $this->position
property az alap Node
osztályból öröklődik, és a Latte parser
automatikusan beállítja. Egy Latte\Compiler\Position objektumot tartalmaz, amely
jelzi, hol található a tag a forrás .latte
fájlban.
A getIterator()
metódus alapvető a fordítási menetekhez. Minden gyermek csomópontot biztosítania kell, de az
egyszerű DatetimeNode
-unknak jelenleg nincsenek argumentumai vagy tartalma, tehát nincsenek gyermek csomópontjai.
Azonban a metódusnak továbbra is léteznie kell, és generátornak kell lennie, azaz a yield
kulcsszónak
valamilyen módon jelen kell lennie a metódus törzsében.
Regisztráció kiterjesztésen keresztül
Végül tájékoztassuk a Latte-t az új tagről. Hozzon létre egy kiterjesztés osztályt (pl.
MyLatteExtension.php
) és regisztrálja a taget a getTags()
metódusában.
<?php
namespace App\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Visszaadja a kiterjesztés által biztosított tagek listáját.
* @return array<string, callable> Térkép: 'tag-neve' => parzolo-fuggveny
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Később itt regisztráljon több taget
];
}
}
Ezután regisztrálja ezt a kiterjesztést a Latte Engine-ben:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Hozzon létre egy sablont:
<p>Az oldal generálva: {datetime}</p>
Várható kimenet: <p>Az oldal generálva: 2023-10-27 11:00:00</p>
Ennek a fázisnak az összefoglalása
Sikeresen létrehoztunk egy alapvető egyéni {datetime}
taget. Definiáltuk a reprezentációját az AST-ben
(DatetimeNode
), kezeltük a parzolását (create()
), meghatároztuk, hogyan kell PHP kódot generálnia
(print()
), biztosítottuk, hogy a gyermekei hozzáférhetők legyenek a menetek számára
(getIterator()
), és regisztráltuk a Latte-ban.
A következő szakaszban javítjuk ezt a taget, hogy argumentumokat fogadjon el, és megmutatjuk, hogyan kell kifejezéseket parzolni és gyermek csomópontokat kezelni.
Tag argumentumok parzolása
Az egyszerű {datetime}
tagünk működik, de nem túl rugalmas. Javítsuk meg, hogy elfogadjon egy opcionális
argumentumot: egy formázó sztringet a date()
függvényhez. A kívánt szintaxis
{datetime $format}
lesz.
Cél: Módosítani a {datetime}
taget úgy, hogy elfogadjon egy opcionális PHP kifejezést
argumentumként, amelyet a date()
formázó sztringjeként használunk.
A TagParser
bemutatása
Mielőtt módosítanánk a kódot, fontos megérteni az eszközt, amelyet használni fogunk: a Latte\Compiler\TagParser-t. Amikor a Latte fő parsere
(TemplateParser
) találkozik egy Latte taggel, mint {datetime ...}
vagy egy n:attribútummal, a tag
belsejének (a {
és }
közötti résznek vagy az attribútum értékének) parzolását egy
specializált TagParser
-re delegálja.
Ez a TagParser
kizárólag a tag argumentumokkal dolgozik. Feladata a tokenek feldolgozása, amelyek ezeket
az argumentumokat reprezentálják. Kulcsfontosságú, hogy fel kell dolgoznia a teljes tartalmat, amelyet kap. Ha a
parzoló függvény befejeződik, de a TagParser
nem érte el az argumentumok végét (ellenőrizve a
$tag->parser->isEnd()
-en keresztül), a Latte kivételt dob, mert ez azt jelzi, hogy váratlan tokenek maradtak
a tagen belül. Ezzel szemben, ha a tag argumentumokat követel meg, akkor a parzoló függvény elején meg kell hívnia
a $tag->expectArguments()
-t. Ez a metódus ellenőrzi, hogy vannak-e argumentumok, és segítőkész kivételt
dob, ha a taget argumentumok nélkül használták.
A TagParser
hasznos metódusokat kínál különböző típusú argumentumok parzolására:
parseExpression(): ExpressionNode
: Parzol egy PHP-szerű kifejezést (változók, literálok, operátorok, függvény/metódus hívások stb.). Kezeli a Latte szintaktikai cukrait, mint például az egyszerű alfanumerikus sztringek idézőjeles sztringként való kezelése (pl. afoo
úgy parzolódik, mintha'foo'
lenne).parseUnquotedStringOrExpression(): ExpressionNode
: Parzol vagy egy standard kifejezést, vagy egy idézőjel nélküli sztringet. Az idézőjel nélküli sztringek a Latte által engedélyezett, idézőjelek nélküli szekvenciák, amelyeket gyakran használnak olyan dolgokhoz, mint a fájlútvonalak (pl.{include ../file.latte}
). Ha idézőjel nélküli sztringet parzol,StringNode
-ot ad vissza.parseArguments(): ArrayNode
: Parzol vesszővel elválasztott argumentumokat, potenciálisan kulcsokkal, mint10, name: 'John', true
.parseModifier(): ModifierNode
: Parzol szűrőket, mint|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Parzol PHP típus-hintet, mintint
,?string
,array|Foo
.
Bonyolultabb vagy alacsonyabb szintű parzolási igényekhez közvetlenül interakcióba léphet a tokenfolyammal a
$tag->parser->stream
-en keresztül. Ez az objektum metódusokat biztosít az egyes tokenek ellenőrzésére és
feldolgozására:
$tag->parser->stream->is(...): bool
: Ellenőrzi, hogy az aktuális token megfelel-e a megadott típusoknak (pl.Token::Php_Variable
) vagy literális értékeknek (pl.'as'
) anélkül, hogy elfogyasztaná. Hasznos előretekintéshez.$tag->parser->stream->consume(...): Token
: Elfogyasztja az aktuális tokent, és előre mozgatja a folyam pozícióját. Ha várt token típusok/értékek vannak megadva argumentumként, és az aktuális token nem felel meg,CompileException
-t dob. Használja ezt, ha vár egy bizonyos tokent.$tag->parser->stream->tryConsume(...): ?Token
: Megpróbálja elfogyasztani az aktuális tokent csak akkor, ha megfelel a megadott típusok/értékek egyikének. Ha megfelel, elfogyasztja a tokent és visszaadja. Ha nem felel meg, változatlanul hagyja a folyam pozícióját ésnull
-t ad vissza. Használja ezt opcionális tokenekhez, vagy ha különböző szintaktikai utak között választ.
A create()
parzoló függvény frissítése
Ezzel a megértéssel módosítsuk a create()
metódust a DatetimeNode
-ban, hogy parzolja az
opcionális formátum argumentumot a $tag->parser
segítségével.
<?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
{
// Hozzáadunk egy publikus property-t a parzolt formátum kifejezés csomópont tárolására
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Ellenőrizzük, hogy vannak-e tokenek
if (!$tag->parser->isEnd()) {
// Parzoljuk az argumentumot PHP-szerű kifejezésként a TagParser segítségével.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... a print() és getIterator() metódusok később frissülnek ...
}
Hozzáadtunk egy publikus $format
property-t. A create()
-ben most a
$tag->parser->isEnd()
-t használjuk annak ellenőrzésére, hogy léteznek-e argumentumok. Ha igen, a
$tag->parser->parseExpression()
feldolgozza a kifejezés tokenjeit. Mivel a TagParser
-nek fel kell
dolgoznia az összes bemeneti tokent, a Latte automatikusan hibát dob, ha a felhasználó valami váratlant ír a formátum
kifejezés után (pl. {datetime 'Y-m-d', unexpected}
).
A print()
metódus frissítése
Most módosítsuk a print()
metódust, hogy használja a $this->format
-ban tárolt parzolt
formátum kifejezést. Ha nem adtak meg formátumot ($this->format
null
), akkor egy alapértelmezett
formázó sztringet kell használnunk, például 'Y-m-d H:i:s'
.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// A %node kiírja a $formatNode PHP kód reprezentációját.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
A $formatNode
változóba tároljuk az AST csomópontot, amely a PHP date()
függvény formázó
sztringjét reprezentálja. Itt a null coalescing operátort (??
) használjuk. Ha a felhasználó megadott egy
argumentumot a sablonban (pl. {datetime 'd.m.Y'}
), akkor a $this->format
property a megfelelő
csomópontot tartalmazza (ebben az esetben egy StringNode
-ot 'd.m.Y'
értékkel), és ezt a csomópontot
használjuk. Ha a felhasználó nem adott meg argumentumot (csak {datetime}
-et írt), a $this->format
property null
, és ehelyett létrehozunk egy új StringNode
-ot az alapértelmezett
'Y-m-d H:i:s'
formátummal. Ez biztosítja, hogy a $formatNode
mindig egy érvényes AST csomópontot
tartalmazzon a formátumhoz.
Az 'echo date(%node) %line;'
maszkban egy új %node
helyettesítő karaktert használunk, amely azt
mondja a format()
metódusnak, hogy vegye az első következő argumentumot (ami a mi $formatNode
-unk),
hívja meg annak print()
metódusát (amely visszaadja a PHP kód reprezentációját), és illessze be az eredményt
a helyettesítő karakter pozíciójába.
A getIterator()
implementálása gyermek csomópontokhoz
A DatetimeNode
-unknak most van egy gyermek csomópontja: a $format
kifejezés. Muszáj ezt a
gyermek csomópontot hozzáférhetővé tennünk a fordítási menetek számára a getIterator()
metódusban
történő biztosítással. Ne felejtsen el referenciát (&
) biztosítani, hogy lehetővé tegye a menetek
számára a csomópont potenciális cseréjét.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Miért alapvető ez? Képzeljen el egy Sandbox menetet, amelynek ellenőriznie kell, hogy a $format
argumentum nem
tartalmaz-e tiltott függvényhívást (pl. {datetime dangerousFunction()}
). Ha a getIterator()
nem
biztosítja a $this->format
-ot, a Sandbox menet soha nem látná a dangerousFunction()
hívást a
tagünk argumentumán belül, ami potenciális biztonsági rést hozna létre. Annak biztosításával lehetővé tesszük a
Sandboxnak (és más meneteknek), hogy ellenőrizzék és potenciálisan módosítsák a $format
kifejezés
csomópontot.
A javított tag használata
A tag most helyesen kezeli az opcionális argumentumot:
Alapértelmezett formátum: {datetime}
Egyéni formátum: {datetime 'd.m.Y'}
Változó használata: {datetime $userDateFormatPreference}
{* Ez hibát okozna a 'd.m.Y' parzolása után, mert a ", foo" váratlan *}
{* {datetime 'd.m.Y', foo} *}
Ezután megnézzük a páros tagek létrehozását, amelyek a közöttük lévő tartalmat dolgozzák fel.
Páros tagek kezelése
Eddig a {datetime}
tagünk önlezáró volt (koncepcionálisan). Nem volt tartalma a kezdő és záró tag
között. Sok hasznos tag azonban egy sablon tartalomblokkjával dolgozik. Ezeket páros tageknek nevezik. Példák erre a
{if}...{/if}
, {block}...{/block}
vagy egy egyéni tag, amelyet most létrehozunk:
{debug}...{/debug}
.
Ez a tag lehetővé teszi számunkra, hogy hibakeresési információkat tartalmazzunk a sablonjainkban, amelyek csak fejlesztés közben legyenek láthatók.
Cél: Létrehozni egy {debug}
páros taget, amelynek tartalma csak akkor renderelődik, ha egy specifikus
“fejlesztői mód” jelző aktív.
Szolgáltatók bemutatása
Néha a tageknek olyan adatokhoz vagy szolgáltatásokhoz kell hozzáférniük, amelyeket nem közvetlenül sablon paraméterként adnak át. Például annak meghatározása, hogy az alkalmazás fejlesztői módban van-e, hozzáférés a felhasználói objektumhoz, vagy konfigurációs értékek lekérése. A Latte egy szolgáltatók (Providers) nevű mechanizmust biztosít erre a célra.
A szolgáltatókat a kiterjesztésben
regisztráljuk a getProviders()
metódus segítségével. Ez a metódus egy asszociatív tömböt ad vissza, ahol a
kulcsok azok a nevek, amelyek alatt a szolgáltatók elérhetők lesznek a sablon futásidejű kódjában, az értékek pedig a
tényleges adatok vagy objektumok.
A tag print()
metódusa által generált PHP kódon belül ezekhez a szolgáltatókhoz a
$this->global
objektum speciális property-jén keresztül férhet hozzá. Mivel ez a property megosztott az
összes kiterjesztés között, jó gyakorlat előtaggal ellátni a szolgáltatók nevét, hogy elkerüljük a potenciális
névütközéseket a Latte kulcsfontosságú szolgáltatóival vagy más harmadik féltől származó kiterjesztések
szolgáltatóival. Gyakori konvenció egy rövid, egyedi előtag használata, amely a gyártóhoz vagy a kiterjesztés nevéhez
kapcsolódik. Példánkhoz az app
előtagot fogjuk használni, és a fejlesztői mód jelzője
$this->global->appDevMode
-ként lesz elérhető.
A yield
kulcsszó a tartalom parzolásához
Hogyan mondjuk meg a Latte parsernek, hogy dolgozza fel a {debug}
és {/debug}
közötti
tartalmat? Itt jön képbe a yield
kulcsszó.
Amikor a yield
-et használjuk a create()
függvényben, a függvény PHP generátorrá válik. Végrehajtása
felfüggesztődik, és a vezérlés visszatér a fő TemplateParser
-hez. A TemplateParser
ezután
folytatja a sablon tartalmának parzolását, amíg nem találkozik a megfelelő záró taggel ({/debug}
a mi
esetünkben).
Amint a záró taget megtalálja, a TemplateParser
folytatja a create()
függvényünk
végrehajtását közvetlenül a yield
utasítás után. A yield
utasítás által visszaadott
érték egy két elemet tartalmazó tömb:
- Egy
AreaNode
, amely a kezdő és záró tag között parzolt tartalmat reprezentálja. - Egy
Tag
objektum, amely a záró taget reprezentálja (pl.{/debug}
).
Hozzuk létre a DebugNode
osztályt és annak create
metódusát, amely a yield
-et
használja.
<?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
{
// Publikus property a parzolt belső tartalom tárolására
public AreaNode $content;
/**
* Parzoló függvény a {debug} ... {/debug} páros taghez.
*/
public static function create(Tag $tag): \Generator // vegye észre a visszatérési típust
{
$node = $tag->node = new self;
// Parzolás felfüggesztése, belső tartalom és záró tag lekérése, amikor {/debug} található
[$node->content, $endTag] = yield;
return $node;
}
// ... a print() és getIterator() később implementálódik ...
}
Megjegyzés: Az $endTag
null
, ha a taget n:attribútumként használják, azaz
<div n:debug>...</div>
.
A print()
implementálása feltételes rendereléshez
A print()
metódusnak most olyan PHP kódot kell generálnia, amely futásidőben ellenőrzi az
appDevMode
szolgáltatót, és csak akkor hajtja végre a belső tartalom kódját, ha a jelző igaz.
public function print(PrintContext $context): string
{
// Generál egy PHP 'if' utasítást, amely futásidőben ellenőrzi a szolgáltatót
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Ha fejlesztői módban van, kiírja a belső tartalmat
%node
}
XX,
$this->position, // A %line kommentárhoz
$this->content, // A belső tartalom AST-jét tartalmazó csomópont
);
}
Ez egyszerű. A PrintContext::format()
-ot használjuk egy standard PHP if
utasítás
létrehozásához. Az if
-en belül a %node
helyettesítő karaktert helyezzük el a
$this->content
-hez. A Latte rekurzívan meghívja a $this->content->print($context)
-et, hogy
generálja a PHP kódot a tag belső részéhez, de csak akkor, ha a $this->global->appDevMode
futásidőben
igazra értékelődik.
A getIterator()
implementálása tartalomhoz
Ahogy az előző példában az argumentum csomópontnál, a DebugNode
-unknak most van egy gyermek csomópontja: az
AreaNode $content
. Hozzáférhetővé kell tennünk a getIterator()
-ban történő biztosítással:
public function &getIterator(): \Generator
{
// Referenciát biztosít a tartalom csomóponthoz
yield $this->content;
}
Ez lehetővé teszi a fordítási menetek számára, hogy leereszkedjenek a {debug}
tagünk tartalmába, ami akkor
is fontos, ha a tartalom feltételesen renderelődik. Például a Sandboxnak elemeznie kell a tartalmat, függetlenül attól,
hogy az appDevMode
igaz vagy hamis.
Regisztráció és használat
Regisztrálja a taget és a szolgáltatót a kiterjesztésében:
class MyLatteExtension extends Extension
{
// Feltételezzük, hogy az $isDevelopmentMode valahol meghatározásra kerül (pl. konfigurációból)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Az új tag regisztrálása
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // A szolgáltató regisztrálása
];
}
}
// A kiterjesztés regisztrálásakor:
$isDev = true; // Határozza meg ezt az alkalmazás környezete alapján
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
És használata a sablonban:
<p>Mindig látható normál tartalom.</p>
{debug}
<div class="debug-panel">
Aktuális felhasználó ID-ja: {$user->id}
Kérés ideje: {=time()}
</div>
{/debug}
<p>További normál tartalom.</p>
n:attribútum integráció
A Latte kényelmes rövidítést kínál sok páros taghez: az n:attribútumokat. Ha van egy páros tagje, mint
{tag}...{/tag}
, és azt szeretné, hogy hatása közvetlenül egyetlen HTML elemre vonatkozzon, gyakran tömörebben
írhatja n:tag
attribútumként ezen az elemen.
A legtöbb standard páros taghez, amelyet definiál (mint a mi {debug}
tagünk), a Latte automatikusan
engedélyezi a megfelelő n:
attribútum verziót. A regisztráció során nem kell semmi extrát tennie:
{* Standard páros tag használata *}
{debug}<div>Hibakeresési információk</div>{/debug}
{* Ekvivalens használat n:attribútummal *}
<div n:debug>Hibakeresési információk</div>
Mindkét verzió csak akkor rendereli a <div>
-et, ha a $this->global->appDevMode
igaz. Az
inner-
és tag-
előtagok is a várt módon működnek.
Néha a tag logikájának kissé eltérően kell viselkednie attól függően, hogy standard páros tagként vagy
n:attribútumként használják-e, vagy hogy használnak-e előtagot, mint n:inner-tag
vagy n:tag-tag
. A
Latte\Compiler\Tag
objektum, amelyet a create()
parzoló függvénynek adnak át, biztosítja ezt az
információt:
$tag->isNAttribute(): bool
:true
-t ad vissza, ha a taget n:attribútumként parzolják.$tag->prefix: ?string
: Visszaadja az n:attribútummal használt előtagot, ami lehetnull
(nem n:attribútum),Tag::PrefixNone
,Tag::PrefixInner
vagyTag::PrefixTag
.
Most, hogy megértettük az egyszerű tageket, az argumentumok parzolását, a páros tageket, a szolgáltatókat és az
n:attribútumokat, foglalkozzunk egy bonyolultabb forgatókönyvvel, amely más tagekbe ágyazott tageket tartalmaz, a
{debug}
tagünket kiindulópontként használva.
Köztes tagek
Néhány páros tag lehetővé teszi, vagy akár megköveteli, hogy más tagek jelenjenek meg bennük a végső záró
tag előtt. Ezeket köztes tageknek nevezik. Klasszikus példák a {if}...{elseif}...{else}...{/if}
vagy a
{switch}...{case}...{default}...{/switch}
.
Bővítsük a {debug}
tagünket, hogy támogasson egy opcionális {else}
klauzult, amely akkor
renderelődik, amikor az alkalmazás nincs fejlesztői módban.
Cél: Módosítani a {debug}
taget, hogy támogasson egy opcionális {else}
köztes taget.
A végső szintaxisnak {debug} ... {else} ... {/debug}
-nak kell lennie.
Köztes tagek parzolása yield
-del
Már tudjuk, hogy a yield
felfüggeszti a create()
parzoló függvényt, és visszaadja a parzolt
tartalmat a záró taggel együtt. A yield
azonban több kontrollt kínál: megadhat neki egy tömböt a köztes
tag nevekből. Amikor a parser találkozik ezen megadott tagek bármelyikével ugyanazon a beágyazási szinten (azaz
a szülő tag közvetlen gyermekeiként, nem más blokkokon vagy tageken belül), szintén leállítja a parzolást.
Amikor a parzolás egy köztes tag miatt áll le, leállítja a tartalom parzolását, folytatja a create()
generátort, és visszaadja a részben parzolt tartalmat és magát a köztes taget (a végső záró tag helyett). A
create()
függvényünk ezután feldolgozhatja ezt a köztes taget (pl. parzolhatja az argumentumait, ha voltak), és
újra használhatja a yield
-et a tartalom következő részének parzolására egészen a végső
záró tagig vagy egy másik várt köztes tagig.
Módosítsuk a DebugNode::create()
-et, hogy várja az {else}
-t:
<?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
{
// Tartalom a {debug} részhez
public AreaNode $thenContent;
// Opcionális tartalom az {else} részhez
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield és várni vagy a {/debug}-ot vagy az {else}-t
[$node->thenContent, $nextTag] = yield ['else'];
// Ellenőrizni, hogy a tag, amelynél megálltunk, {else} volt-e
if ($nextTag?->name === 'else') {
// Újra yield a tartalom parzolásához az {else} és {/debug} között
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... a print() és getIterator() később frissülnek ...
}
Most a yield ['else']
azt mondja a Latte-nak, hogy ne csak a {/debug}
-ra, hanem az
{else}
-re is álljon le a parzolással. Ha {else}
található, a $nextTag
tartalmazni fogja
az {else}
Tag
objektumát. Ezután újra használjuk a yield
-et argumentumok nélkül, ami
azt jelenti, hogy most már csak a végső {/debug}
taget várjuk, és az eredményt a
$node->elseContent
-be mentjük. Ha az {else}
nem található, a $nextTag
a
{/debug}
Tag
-ja lenne (vagy null
, ha n:attribútumként használják), és a
$node->elseContent
null
maradna.
A print()
implementálása {else}
-szel
A print()
metódusnak tükröznie kell az új struktúrát. Generálnia kell egy PHP if/else
utasítást a devMode
szolgáltató alapján.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Kód a 'then' ághoz ({debug} tartalom)
} else {
%node // Kód az 'else' ághoz ({else} tartalom)
}
XX,
$this->position, // Sorszám az 'if' feltételhez
$this->thenContent, // Első %node helyettesítő
$this->elseContent ?? new NopNode, // Második %node helyettesítő
);
}
Ez egy standard PHP if/else
struktúra. Kétszer használjuk a %node
-ot; a format()
sorban helyettesíti a megadott csomópontokat. A ?? new NopNode
-ot használjuk a hibák elkerülésére, ha a
$this->elseContent
null
– a NopNode
egyszerűen nem nyomtat ki semmit.
A getIterator()
implementálása mindkét tartalomhoz
Most potenciálisan két gyermek tartalom csomópontunk van ($thenContent
és $elseContent
).
Mindkettőt biztosítanunk kell, ha léteznek:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
A javított tag használata
A tag most már használható az opcionális {else}
klauzullal:
{debug}
<p>Hibakeresési információk megjelenítése, mert a devMode BE van kapcsolva.</p>
{else}
<p>A hibakeresési információk elrejtve, mert a devMode KI van kapcsolva.</p>
{/debug}
Állapot és beágyazás kezelése
Korábbi példáink ({datetime}
, {debug}
) viszonylag állapotmentesek voltak a print()
metódusaikon belül. Vagy közvetlenül kiírták a tartalmat, vagy egyszerű feltételes ellenőrzést végeztek egy globális
szolgáltató alapján. Sok tagnek azonban valamilyen formában állapotot kell kezelnie a renderelés során, vagy
felhasználói kifejezések kiértékelését foglalja magában, amelyeket csak egyszer kellene futtatni a teljesítmény vagy a
helyesség érdekében. Továbbá figyelembe kell vennünk, mi történik, ha az egyéni tagjeink beágyazva vannak.
Illusztráljuk ezeket a koncepciókat egy {repeat $count}...{/repeat}
tag létrehozásával. Ez a tag a belső
tartalmát $count
-szor ismétli meg.
Cél: Implementálni a {repeat $count}
taget, amely a tartalmát a megadott számú alkalommal
ismétli meg.
Ideiglenes és egyedi változók szükségessége
Képzelje el, hogy a felhasználó ezt írja:
{repeat rand(1, 5)} Tartalom {/repeat}
Ha naivan generálnánk egy PHP for
ciklust így a print()
metódusunkban:
// Egyszerűsített, HELYTELEN generált kód
for ($i = 0; $i < rand(1, 5); $i++) {
// tartalom kiírása
}
Ez rossz lenne! A rand(1, 5)
kifejezés minden ciklus iterációban újra kiértékelődne, ami
kiszámíthatatlan számú ismétléshez vezetne. Ki kell értékelnünk a $count
kifejezést egyszer a ciklus
kezdete előtt, és tárolnunk kell az eredményét.
Generálunk egy PHP kódot, amely először kiértékeli a darabszám kifejezést, és egy ideiglenes futásidejű
változóba menti. Annak érdekében, hogy elkerüljük az ütközéseket a sablon felhasználója által definiált
változókkal és a Latte belső változóival (mint a $ʟ_...
), a $__
(dupla aláhúzás)
előtag konvenciót használjuk az ideiglenes változóinkhoz.
A generált kód ekkor így nézne ki:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// tartalom kiírása
}
Most vegyük fontolóra a beágyazást:
{repeat $countA} {* Külső ciklus *}
{repeat $countB} {* Belső ciklus *}
...
{/repeat}
{/repeat}
Ha mind a külső, mind a belső {repeat}
tag olyan kódot generálna, amely ugyanazokat az ideiglenes
változóneveket használja (pl. $__count
és $__i
), a belső ciklus felülírná a külső ciklus
változóit, ami megbontaná a logikát.
Biztosítanunk kell, hogy az egyes {repeat}
tag példányokhoz generált ideiglenes változók egyediek
legyenek. Ezt a PrintContext::generateId()
segítségével érjük el. Ez a metódus egy egyedi egész számot ad
vissza a fordítási fázisban. Ezt az ID-t hozzáfűzhetjük az ideiglenes változóink nevéhez.
Tehát a $__count
helyett $__count_1
-et generálunk az első repeat taghez, $__count_2
-t
a másodikhoz, és így tovább. Hasonlóképpen a ciklusszámlálóhoz $__i_1
-et, $__i_2
-t stb.
használunk.
A RepeatNode
implementálása
Hozzuk létre a csomópont osztályt.
<?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;
/**
* Parzoló függvény a {repeat $count} ... {/repeat} taghez
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // biztosítja, hogy a $count meg legyen adva
$node = $tag->node = new self;
// Parzolja a darabszám kifejezést
$node->count = $tag->parser->parseExpression();
// Belső tartalom lekérése
[$node->content] = yield;
return $node;
}
/**
* Generál egy PHP 'for' ciklust egyedi változónevekkel.
*/
public function print(PrintContext $context): string
{
// Egyedi változónevek generálása
$id = $context->generateId();
$countVar = '$__count_' . $id; // pl. $__count_1, $__count_2, stb.
$iteratorVar = '$__i_' . $id; // pl. $__i_1, $__i_2, stb.
return $context->format(
<<<'XX'
// A darabszám kifejezés kiértékelése *egyszer* és tárolása
%raw = (int) (%node);
// Ciklus a tárolt darabszámmal és egyedi iterációs változóval
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Belső tartalom renderelése
}
XX,
$countVar, // %0 - Változó a darabszám tárolására
$this->count, // %1 - Kifejezés csomópont a darabszámhoz
$iteratorVar, // %2 - A ciklus iterációs változójának neve
$this->position, // %3 - Kommentár a sorszámmal magához a ciklushoz
$this->content // %4 - Belső tartalom csomópont
);
}
/**
* Biztosítja a gyermek csomópontokat (darabszám kifejezés és tartalom).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
A create()
metódus parzolja a kötelező $count
kifejezést a parseExpression()
segítségével. Először a $tag->expectArguments()
kerül meghívásra. Ez biztosítja, hogy a felhasználó
megadott valamit a {repeat}
után. Míg a $tag->parser->parseExpression()
meghiúsulna, ha
semmit sem adtak volna meg, a hibaüzenet váratlan szintaxisról szólhatna. Az expectArguments()
használata sokkal
világosabb hibát ad, konkrétan megjelölve, hogy hiányoznak az argumentumok a {repeat}
taghez.
A print()
metódus generálja a PHP kódot, amely felelős az ismétlési logika futásidejű végrehajtásáért.
Az egyedi nevek generálásával kezdődik az ideiglenes PHP változókhoz, amelyekre szüksége lesz.
A $context->format()
metódus egy új %raw
helyettesítő karakterrel kerül meghívásra, amely
beilleszti a megfelelő argumentumként megadott nyers sztringet. Itt beilleszti a $countVar
-ban tárolt
egyedi változónevet (pl. $__count_1
). És mi a helyzet a %0.raw
-val és a %2.raw
-val? Ez
a pozíciós helyettesítőket demonstrálja. Ahelyett, hogy csak a %raw
-t használnánk, amely a
következő elérhető nyers argumentumot veszi, a %2.raw
expliciten a 2-es indexű argumentumot veszi (ami a
$iteratorVar
), és beilleszti annak nyers sztring értékét. Ez lehetővé teszi számunkra, hogy újra
felhasználjuk a $iteratorVar
sztringet anélkül, hogy többször átadnánk a format()
argumentumlistájában.
Ez a gondosan megkonstruált format()
hívás hatékony és biztonságos PHP ciklust generál, amely helyesen
kezeli a darabszám kifejezést, és elkerüli a változónév-ütközéseket még akkor is, ha a {repeat}
tagek
beágyazva vannak.
Regisztráció és használat
Regisztrálja a taget a kiterjesztésében:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // A repeat tag regisztrálása
];
}
}
Használja a sablonban, beleértve a beágyazást is:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Belső ciklus</td>
{/repeat}
</tr>
{/repeat}
Ez a példa demonstrálja, hogyan kell kezelni az állapotot (ciklusszámlálók) és a potenciális beágyazási problémákat
ideiglenes, $__
előtagú és a PrintContext::generateId()
-től származó egyedi ID-vel ellátott
változók segítségével.
Tiszta n:attribútumok
Míg sok n:attribútum
, mint az n:if
vagy n:foreach
, kényelmes rövidítésként
szolgál a páros tag megfelelőikhez ({if}...{/if}
, {foreach}...{/foreach}
), a Latte lehetővé teszi
olyan tagek definiálását is, amelyek csak n:attribútum formájában léteznek. Ezeket gyakran használják a HTML elem
attribútumainak vagy viselkedésének módosítására, amelyhez csatolva vannak.
A Latte-ba beépített standard példák közé tartozik a n:class
, amely segít dinamikusan összeállítani a
class
attribútumot, és a n:attr
, amely több
tetszőleges attribútumot állíthat be.
Hozzuk létre saját tiszta n:attribútumunkat: n:confirm
, amely egy JavaScript megerősítő párbeszédablakot
ad hozzá egy művelet végrehajtása előtt (mint egy link követése vagy egy űrlap elküldése).
Cél: Implementálni az n:confirm="'Biztos benne?'"
-t, amely hozzáad egy onclick
kezelőt az
alapértelmezett művelet megakadályozására, ha a felhasználó megszakítja a megerősítő párbeszédablakot.
A ConfirmNode
implementálása
Szükségünk van egy Node osztályra és egy parzoló függvényre.
<?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;
}
/**
* Generálja az 'onclick' attribútum kódját helyes escapeléssel.
*/
public function print(PrintContext $context): string
{
// Biztosítja a helyes escapelést mind a JavaScript, mind a HTML attribútum kontextusokhoz.
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;
}
}
A print()
metódus generálja a PHP kódot, amely végül a sablon renderelése során kiírja a
onclick="..."
HTML attribútumot. A beágyazott kontextusok (JavaScript egy HTML attribútumon belül) kezelése
gondos escapelést igényel. A LR\Filters::escapeJs(%node)
szűrő futásidőben kerül meghívásra, és helyesen
escapeli az üzenetet a JavaScripten belüli használathoz (a kimenet olyan lenne, mint "Sure?"
). Ezután a
LR\Filters::escapeHtmlAttr(...)
szűrő escapeli azokat a karaktereket, amelyek speciálisak a HTML attribútumokban,
így ez a kimenetet return confirm("Sure?")
-re változtatná. Ez a kétlépcsős futásidejű
escapelés biztosítja, hogy az üzenet biztonságos legyen a JavaScript számára, és az eredményül kapott JavaScript kód
biztonságos legyen a onclick
HTML attribútumba való beágyazáshoz.
Regisztráció és használat
Regisztrálja az n:attribútumot a kiterjesztésében. Ne felejtse el az n:
előtagot a kulcsban:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // Az n:confirm regisztrálása
];
}
}
Most már használhatja az n:confirm
-ot linkeken, gombokon vagy űrlap elemeken:
<a href="delete.php?id=123" n:confirm='"Valóban törölni szeretné a(z) {$id} elemet?"'>Törlés</a>
Generált HTML:
<a href="delete.php?id=123" onclick="return confirm("Valóban törölni szeretné a(z) 123 elemet?")">Törlés</a>
Amikor a felhasználó a linkre kattint, a böngésző végrehajtja az onclick
kódot, megjeleníti a
megerősítő párbeszédablakot, és csak akkor navigál a delete.php
-re, ha a felhasználó az “OK”-ra
kattint.
Ez a példa demonstrálja, hogyan lehet létrehozni egy tiszta n:attribútumot a gazda HTML elem viselkedésének vagy
attribútumainak módosítására a megfelelő PHP kód generálásával a print()
metódusában. Ne feledkezzen meg
a gyakran szükséges dupla escapelésről: egyszer a célkontextushoz (ebben az esetben JavaScript), és újra a HTML attribútum
kontextusához.
Haladó témák
Míg az előző szakaszok az alapvető koncepciókat fedték le, itt van néhány haladóbb téma, amellyel találkozhat az egyéni Latte tagek létrehozása során.
Tag kimeneti módok
A create()
függvénynek átadott Tag
objektumnak van egy outputMode
property-je. Ez a
property befolyásolja, hogyan kezeli a Latte a környező szóközöket és behúzásokat, különösen, ha a taget saját
sorában használják. Ezt a property-t módosíthatja a create()
függvényében.
Tag::OutputKeepIndentation
(Alapértelmezett a legtöbb tagnél, mint{=...}
): A Latte megpróbálja megőrizni a tag előtti behúzást. A tag utáni új sorok általában megmaradnak. Ez alkalmas olyan tagekhez, amelyek soron belüli tartalmat írnak ki.Tag::OutputRemoveIndentation
(Alapértelmezett a blokk tageknél, mint{if}
,{foreach}
): A Latte eltávolítja a kezdő behúzást és potenciálisan egy következő új sort. Ez segít tisztábban tartani a generált PHP kódot, és megakadályozza a HTML kimenetben a tag által okozott további üres sorokat. Használja ezt olyan tagekhez, amelyek vezérlési struktúrákat vagy blokkokat reprezentálnak, amelyeknek maguknak nem szabadna szóközöket hozzáadniuk.Tag::OutputNone
(Olyan tagek használják, mint{var}
,{default}
): Hasonló aRemoveIndentation
-höz, de erősebben jelzi, hogy a tag maga nem produkál közvetlen kimenetet, potenciálisan még agresszívebben befolyásolva a körülötte lévő szóközök kezelését. Alkalmas deklaratív vagy beállító tagekhez.
Válassza ki azt a módot, amely a legjobban megfelel a tag céljának. A legtöbb strukturális vagy vezérlő taghez
általában az OutputRemoveIndentation
a megfelelő.
Szülő/legközelebbi tagek elérése
Néha egy tag viselkedésének attól a kontextustól kell függenie, amelyben használják, konkrétan attól, hogy melyik
szülő tag(ek)ben található. A create()
függvénynek átadott Tag
objektum biztosítja a
closestTag(array $classes, ?callable $condition = null): ?Tag
metódust pontosan erre a célra.
Ez a metódus felfelé keres a jelenleg nyitott tagek hierarchiájában (beleértve a parzolás során belsőleg reprezentált
HTML elemeket is), és visszaadja a legközelebbi ős Tag
objektumát, amely megfelel a specifikus kritériumoknak.
Ha nem található megfelelő ős, null
-t ad vissza.
Az $classes
tömb meghatározza, milyen típusú ős tageket keres. Ellenőrzi, hogy az ős tag társított
csomópontja ($ancestorTag->node
) ennek az osztálynak a példánya-e.
function create(Tag $tag)
{
// A legközelebbi ős tag keresése, amelynek csomópontja ForeachNode példány
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Hozzáférhetünk magához a ForeachNode példányhoz:
$foreachNode = $foreachTag->node;
}
}
Vegye észre a $foreachTag->node
-ot: Ez csak azért működik, mert konvenció a Latte tag fejlesztésben, hogy
azonnal hozzárendeljük a létrehozott csomópontot a $tag->node
-hoz a create()
metóduson belül,
ahogy mindig is tettük.
Néha a csomópont típusának egyszerű összehasonlítása nem elegendő. Lehet, hogy ellenőriznie kell egy potenciális ős
tag vagy annak csomópontjának specifikus property-jét. A closestTag()
opcionális második argumentuma egy
callable, amely megkapja a potenciális ős Tag
objektumot, és vissza kell adnia, hogy érvényes egyezés-e.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Feltétel: a blokknak dinamikusnak kell lennie
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
A closestTag()
használata lehetővé teszi olyan tagek létrehozását, amelyek kontextus-tudatosak és
kikényszerítik a helyes használatot a sablon struktúráján belül, ami robusztusabb és érthetőbb sablonokhoz vezet.
PrintContext::format()
helyettesítő karakterek
Gyakran használtuk a PrintContext::format()
-ot PHP kód generálására a csomópontjaink print()
metódusaiban. Elfogad egy maszk sztringet és további argumentumokat, amelyek helyettesítik a maszkban lévő helyettesítő
karaktereket. Itt van egy összefoglaló az elérhető helyettesítő karakterekről:
%node
: Az argumentumnakNode
példánynak kell lennie. Meghívja a csomópontprint()
metódusát, és beilleszti az eredményül kapott PHP kódsztringet.%dump
: Az argumentum bármilyen PHP érték. Exportálja az értéket érvényes PHP kódba. Alkalmas skalárokhoz, tömbökhöz, null-hoz.$context->format('echo %dump;', 'Hello')
→echo 'Hello';
$context->format('$arr = %dump;', [1, 2])
→$arr = [1, 2];
%raw
: Beilleszti az argumentumot közvetlenül a kimeneti PHP kódba bármilyen escapelés vagy módosítás nélkül. Használja óvatosan, elsősorban előre generált PHP kódrészletek vagy változónevek beillesztésére.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: Az argumentumnakExpression\ArrayNode
-nak kell lennie. Kiírja a tömb elemeit függvény- vagy metódushívás argumentumaként formázva (vesszővel elválasztva, kezeli a névvel ellátott argumentumokat, ha vannak).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: Az argumentumnakPosition
objektumnak kell lennie (általában$this->position
). Beilleszt egy PHP kommentárt/* line X */
, amely a forrás sorszámát jelzi.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Generál egy PHP kódot, amely futásidőben escapeli a belső kifejezést az aktuális kontextus-tudatos escapelési szabályok szerint.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: Az argumentumnakModifierNode
-nak kell lennie. Generál egy PHP kódot, amely alkalmazza aModifierNode
-ban megadott szűrőket a belső tartalomra, beleértve a kontextus-tudatos escapelést, hacsak nincs letiltva a|noescape
-pel.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Hasonló a%modify
-hoz, de elfogott tartalomblokkok (gyakran HTML) módosítására szolgál.
Explicit módon hivatkozhat az argumentumokra az indexük alapján (nullától kezdve): %0.node
,
%1.dump
, %2.raw
, stb. Ez lehetővé teszi egy argumentum többszöri újrafelhasználását a maszkban
anélkül, hogy ismételten át kellene adni a format()
-nak. Lásd a {repeat}
tag példáját, ahol a
%0.raw
és %2.raw
volt használva.
Komplex argumentum parzolási példa
Míg a parseExpression()
, parseArguments()
, stb. sok esetet lefednek, néha bonyolultabb parzolási
logikára van szükség az alacsonyabb szintű TokenStream
használatával, amely a
$tag->parser->stream
-en keresztül érhető el.
Cél: Létrehozni egy {embedYoutube $videoID, width: 640, height: 480}
taget. Parzolni akarjuk a kötelező
videó ID-t (sztring vagy változó), amelyet opcionális kulcs-érték párok követnek a méretekhez.
<?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;
// Kötelező videó ID parzolása
$node->videoId = $tag->parser->parseExpression();
// Opcionális kulcs-érték párok parzolása
$stream = $tag->parser->stream; // Tokenfolyam lekérése
while ($stream->tryConsume(',')) { // Vesszővel való elválasztást követel meg
// 'width' vagy 'height' azonosító várása
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Kettőspont elválasztó várása
$value = $tag->parser->parseExpression(); // Érték kifejezés parzolása
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Ismeretlen argumentum '$key'. Várt 'width' vagy 'height'.", $keyToken->position);
}
}
return $node;
}
}
Ez a kontrollszint lehetővé teszi nagyon specifikus és komplex szintaxisok definiálását az egyéni tagekhez a tokenfolyammal való közvetlen interakció révén.
Az AuxiliaryNode
használata
A Latte általános “segéd” csomópontokat biztosít speciális helyzetekhez a kódgenerálás során vagy a fordítási
meneteken belül. Ezek az AuxiliaryNode
és a Php\Expression\AuxiliaryNode
.
Tekintse az AuxiliaryNode
-ot egy rugalmas konténer csomópontnak, amely delegálja alapvető
funkcionalitásait – a kódgenerálást és a gyermek csomópontok kiadását – a konstruktorában megadott
argumentumoknak:
print()
delegálása: A konstruktor első argumentuma egy PHP closure. Amikor a Latte meghívja aprint()
metódust azAuxiliaryNode
-on, végrehajtja ezt a megadott closure-t. A closure megkapja aPrintContext
-et és a konstruktor második argumentumában átadott csomópontokat, lehetővé téve teljesen egyéni PHP kódgenerálási logika definiálását futásidőben.getIterator()
delegálása: A konstruktor második argumentuma egyNode
objektumokból álló tömb. Amikor a Latte-nak végig kell járnia azAuxiliaryNode
gyermekeit (pl. fordítási menetek során), annakgetIterator()
metódusa egyszerűen kiadja a ebben a tömbben felsorolt csomópontokat.
Példa:
$node = new AuxiliaryNode(
// 1. Ez a closure lesz a print() törzse
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Ezeket a csomópontokat adja ki a getIterator() metódus, és adja át a fenti closure-nek
[$argumentNode1, $argumentNode2]
);
A Latte két különböző típust biztosít attól függően, hogy hova kell beillesztenie a generált kódot:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Használja ezt, ha olyan PHP kódrészletet kell generálnia, amely egy kifejezést reprezentál.Latte\Compiler\Nodes\AuxiliaryNode
: Használja ezt általánosabb célokra, amikor egy vagy több utasítást reprezentáló PHP kódblokkot kell beillesztenie.
Fontos ok az AuxiliaryNode
használatára a standard csomópontok (mint a StaticMethodCallNode
)
helyett a print()
metódusában vagy egy fordítási menetben a láthatóság ellenőrzése a következő
fordítási menetek számára, különösen azok számára, amelyek biztonsággal kapcsolatosak, mint a Sandbox.
Vegye fontolóra a következő forgatókönyvet: A fordítási menete egy felhasználó által megadott kifejezést
($userExpr
) egy specifikus, megbízható segédfüggvény myInternalSanitize($userExpr)
hívásába kell
csomagolnia. Ha egy standard new FunctionCallNode('myInternalSanitize', [$userExpr])
csomópontot hoz létre, az
teljesen látható lesz az AST menet számára. Ha a Sandbox menet később fut, és a myInternalSanitize
nincs az engedélyezett listáján, a Sandbox blokkolhatja vagy módosíthatja ezt a hívást, potenciálisan
megbontva a tag belső logikáját, még akkor is, ha Ön, a tag szerzője, tudja, hogy ez a specifikus hívás
biztonságos és szükséges. Ezért generálhatja a hívást közvetlenül az AuxiliaryNode
closure-én belül.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... a print() vagy egy fordítási menetben ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Közvetlen PHP kód generálása
$userExpr,
),
// FONTOS: Itt továbbra is adja át az eredeti felhasználói kifejezés csomópontot!
[$userExpr],
);
Ebben az esetben a Sandbox menet látja az AuxiliaryNode
-ot, de nem elemzi a closure által generált PHP
kódot. Nem tudja közvetlenül blokkolni a closure belsejében generált myInternalSanitize
hívást.
Míg maga a generált PHP kód rejtve van a menetek elől, a kód bemenetei (a felhasználói adatokat vagy
kifejezéseket reprezentáló csomópontok) továbbra is bejárhatónak kell lenniük. Ezért az AuxiliaryNode
konstruktorának második argumentuma alapvető. Muszáj átadnia egy tömböt, amely tartalmazza az összes eredeti
csomópontot (mint a $userExpr
a fenti példában), amelyet a closure használ. Az AuxiliaryNode
getIterator()
-ja kiadja ezeket a csomópontokat, lehetővé téve a fordítási meneteknek, mint a Sandbox,
hogy elemezzék őket potenciális problémák szempontjából.
Bevált gyakorlatok
- Világos cél: Győződjön meg róla, hogy a tagjának világos és szükséges célja van. Ne hozzon létre tageket olyan feladatokhoz, amelyeket könnyen meg lehet oldani szűrőkkel vagy függvényekkel.
- Implementálja helyesen a
getIterator()
-t: Mindig implementálja agetIterator()
-t, és biztosítson referenciákat (&
) minden gyermek csomóponthoz (argumentumok, tartalom), amelyeket a sablonból parzoltak. Ez elengedhetetlen a fordítási menetekhez, a biztonsághoz (Sandbox) és a potenciális jövőbeli optimalizációkhoz. - Publikus property-k a csomópontokhoz: Tegye publikussá a gyermek csomópontokat tartalmazó property-ket, hogy a fordítási menetek szükség esetén módosíthassák őket.
- Használja a
PrintContext::format()
-ot: Használja ki aformat()
metódust PHP kód generálására. Kezeli az idézőjeleket, helyesen escapeli a helyettesítő karaktereket, és automatikusan hozzáadja a sorszám kommentárokat. - Ideiglenes változók (
$__
): Amikor futásidejű PHP kódot generál, amely ideiglenes változókat igényel (pl. köztes összegek tárolására, ciklusszámlálókhoz), használja a$__
előtag konvenciót, hogy elkerülje az ütközéseket a felhasználói változókkal és a Latte belső$ʟ_
változóival. - Beágyazás és egyedi ID-k: Ha a tagja beágyazható, vagy példány-specifikus állapotra van szüksége
futásidőben, használja a
$context->generateId()
-t aprint()
metódusán belül, hogy egyedi utótagokat hozzon létre az ideiglenes$__
változóihoz. - Szolgáltatók külső adatokhoz: Használjon szolgáltatókat (regisztrálva az
Extension::getProviders()
-en keresztül) futásidejű adatokhoz vagy szolgáltatásokhoz való hozzáféréshez ($this->global->...
) ahelyett, hogy értékeket hardkódolna vagy globális állapotra támaszkodna. Használjon gyártói előtagokat a szolgáltatónevekhez. - Fontolja meg az n:attribútumokat: Ha a páros tagja logikailag egyetlen HTML elemen működik, a Latte valószínűleg
automatikus
n:attribútum
támogatást nyújt. Tartsa ezt szem előtt a felhasználói kényelem érdekében. Ha attribútum-módosító taget hoz létre, fontolja meg, hogy egy tisztan:attribútum
a legmegfelelőbb forma-e. - Tesztelés: Írjon teszteket a tagjeihez, lefedve mind a különböző szintaktikai bemenetek parzolását, mind a generált PHP kód kimenetének helyességét.
Ezen irányelvek követésével erőteljes, robusztus és karbantartható egyéni tageket hozhat létre, amelyek zökkenőmentesen integrálódnak a Latte sablonmotorral.
A Latte részét képező csomópont osztályok tanulmányozása a legjobb módja annak, hogy megtanulja a parzolási folyamat minden részletét.