Създаване на персонализирани тагове
Тази страница предоставя изчерпателно ръководство за създаване на персонализирани тагове в Latte. Ще обсъдим всичко – от прости тагове до по-сложни сценарии с вложено съдържание и специфични нужди от парсване, като надграждаме разбирането ви за това как Latte компилира шаблони.
Персонализираните тагове осигуряват най-високо ниво на контрол върху синтаксиса на шаблона и логиката на рендиране, но са и най-сложната точка за разширяване. Преди да решите да създадете персонализиран таг, винаги обмисляйте дали не съществува по-просто решение или дали вече не съществува подходящ таг в стандартния набор. Използвайте персонализирани тагове само когато по-простите алтернативи не са достатъчни за вашите нужди.
Разбиране на процеса на компилация
За ефективно създаване на персонализирани тагове е полезно да се обясни как Latte обработва шаблони. Разбирането на този процес изяснява защо таговете са структурирани по този начин и как се вписват в по-широкия контекст.
Компилацията на шаблон в Latte, опростено, включва следните ключови стъпки:
- Лексикален анализ: Лексерът чете изходния код на шаблона (файл
.latte
) и го разделя на последователност от малки, отделни части, наречени токени (напр.{
,foreach
,$variable
,}
, HTML текст и т.н.). - Парсване: Парсерът взема този поток от токени и изгражда от него смислена дървовидна структура, представяща логиката и съдържанието на шаблона. Това дърво се нарича абстрактно синтактично дърво (AST).
- Компилационни проходи: Преди генерирането на PHP код, Latte изпълнява компилационни проходи. Това са функции, които обхождат цялото AST и могат да го модифицират или да събират информация. Тази стъпка е ключова за функции като сигурност (Sandbox) или оптимизация.
- Генериране на код: Накрая компилаторът обхожда (потенциално модифицираното) AST и генерира съответния код на PHP клас. Този PHP код е това, което всъщност рендира шаблона при изпълнение.
- Кеширане: Генерираният PHP код се съхранява на диск, което прави последващите рендирания много бързи, тъй като стъпки 1–4 се пропускат.
В действителност компилацията е малко по-сложна. Latte има два лексера и парсера: един за HTML шаблона и втори за PHP-подобния код вътре в таговете. Също така парсването не се извършва след токенизацията, а лексерът и парсерът работят паралелно в две “нишки” и се координират. Повярвайте ми, програмирането на това беше ракетна наука :-)
Целият процес, от зареждането на съдържанието на шаблона, през парсването, до генерирането на крайния файл, може да бъде изпълнен последователно с този код, с който можете да експериментирате и да извеждате междинни резултати:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Анатомия на таг
Създаването на напълно функционален персонализиран таг в Latte включва няколко взаимосвързани части. Преди да се заемем с имплементацията, нека разберем основните концепции и терминология, използвайки аналогия с HTML и Document Object Model (DOM).
Тагове срещу Възли (Аналогия с HTML)
В HTML пишем тагове като <p>
или <div>...</div>
. Тези
тагове са синтаксис в изходния код. Когато браузърът парсва този HTML,
той създава представяне в паметта, наречено Document Object Model (DOM). В DOM HTML
таговете са представени от възли (конкретно възли Element
в
терминологията на JavaScript DOM). С тези възли работим програмно (напр. с
помощта на JavaScript document.getElementById(...)
се връща възел Element). Тагът е
само текстово представяне в изходния файл; възелът е обектно
представяне в логическото дърво.
Latte работи по подобен начин:
- Във файла
.latte
на шаблона пишете Latte тагове, като{foreach ...}
и{/foreach}
. Това е синтаксисът, с който вие като автор на шаблона работите. - Когато Latte парсва шаблона, той изгражда Abstract Syntax Tree (AST). Това дърво е съставено от възли. Всеки Latte таг, HTML елемент, част от текст или израз в шаблона се превръща в един или повече възли в това дърво.
- Основният клас за всички възли в AST е
Latte\Compiler\Node
. Точно както DOM има различни типове възли (Element, Text, Comment), AST на Latte има различни типове възли. Ще се сблъскате сLatte\Compiler\Nodes\TextNode
за статичен текст,Latte\Compiler\Nodes\Html\ElementNode
за HTML елементи,Latte\Compiler\Nodes\Php\ExpressionNode
за изрази вътре в таговете и ключово за персонализирани тагове, възли, наследяващи отLatte\Compiler\Nodes\StatementNode
.
Защо StatementNode
?
HTML елементите (Html\ElementNode
) основно представят структура и
съдържание. PHP изразите (Php\ExpressionNode
) представят стойности или
изчисления. Но какво да кажем за Latte тагове като {if}
, {foreach}
или нашия собствен {datetime}
? Тези тагове изпълняват действия,
управляват потока на програмата или генерират изход въз основа на
логика. Те са функционални единици, които правят Latte мощен шаблонен
engine, а не просто маркиращ език.
В програмирането такива единици, изпълняващи действия, често се
наричат “statements” (инструкции). Затова възлите, представящи тези
функционални Latte тагове, обикновено наследяват от
Latte\Compiler\Nodes\StatementNode
. Това ги отличава от чисто структурните
възли (като HTML елементи) или възлите, представящи стойности (като
изрази).
Ключови компоненти
Нека разгледаме основните компоненти, необходими за създаване на персонализиран таг:
Функция за парсване на таг
- Тази PHP callable функция парсва синтаксиса на Latte тага (
{...}
) в изходния шаблон. - Получава информация за тага (като неговото име, позиция и дали е n:атрибут) чрез обекта Latte\Compiler\Tag.
- Нейният основен инструмент за парсване на аргументи и изрази вътре в
ограничителите на тага е обектът Latte\Compiler\TagParser, достъпен чрез
$tag->parser
(това е различен парсер от този, който парсва целия шаблон). - За сдвоени тагове използва
yield
, за да сигнализира на Latte да парсва вътрешното съдържание между началния и крайния таг. - Крайната цел на парсващата функция е да създаде и върне инстанция на класа на възела, която се добавя към AST.
- Практика е (макар и да не е задължително) да се имплементира
парсващата функция като статичен метод (често наречен
create
) директно в съответния клас на възела. Това поддържа парсващата логика и представянето на възела спретнато в един пакет, позволява достъп до private/protected елементи на класа, ако е необходимо, и подобрява организацията.
Клас на възела
- Представлява логическата функция на вашия таг в Abstract Syntax Tree (AST).
- Съдържа парсваната информация (като аргументи или съдържание) като
публични свойства. Тези свойства често съдържат други инстанции на
Node
(напр.ExpressionNode
за парсвани аргументи,AreaNode
за парсвано съдържание). - Методът
print(PrintContext $context): string
генерира PHP код (инструкция или серия от инструкции), който изпълнява действието на тага по време на рендиране на шаблона. - Методът
getIterator(): \Generator
предоставя достъп до дъщерните възли (аргументи, съдържание) за обхождане от компилационните проходи. Трябва да предоставя референции (&
), за да позволи на проходите потенциално да модифицират или заменят подвъзли. - След като целият шаблон е парсван в AST, Latte изпълнява серия от компилационни проходи. Тези проходи
обхождат цялото AST, използвайки метода
getIterator()
, предоставен от всеки възел. Те могат да инспектират възли, да събират информация и дори да модифицират дървото (напр. чрез промяна на публичните свойства на възлите или пълна замяна на възли). Този дизайн, изискващ комплексенgetIterator()
, е фундаментален. Той позволява на мощни функции като Sandbox да анализират и потенциално да променят поведението на всяка част от шаблона, включително вашите персонализирани тагове, осигурявайки сигурност и консистентност.
Регистрация чрез разширение
- Трябва да информирате Latte за вашия нов таг и коя парсваща функция трябва да се използва за него. Това се случва в рамките на Latte разширение.
- Вътре във вашия клас на разширението имплементирате метода
getTags(): array
. Този метод връща асоциативен масив, където ключовете са имената на таговете (напр.'mytag'
,'n:myattribute'
), а стойностите са PHP callable функции, представляващи техните съответни парсващи функции (напр.MyNamespace\DatetimeNode::create(...)
).
Резюме: Функцията за парсване на таг преобразува изходния код
на шаблона на вашия таг във възел на AST. Класът на възела след
това може да преобразува себе си в изпълним PHP код за
компилирания шаблон и предоставя достъп до своите подвъзли за
компилационните проходи чрез getIterator()
. Регистрацията чрез
разширение свързва името на тага с парсващата функция и уведомява
Latte за него.
Сега ще разгледаме как да имплементираме тези компоненти стъпка по стъпка.
Създаване на прост таг
Нека се заемем със създаването на вашия първи персонализиран Latte таг.
Ще започнем с много прост пример: таг с име {datetime}
, който извежда
текущата дата и час. Първоначално този таг няма да приема никакви
аргументи, но ще го подобрим по-късно в секцията Парсване на аргументи на таг. Той също така няма
вътрешно съдържание.
Този пример ще ви преведе през основните стъпки: дефиниране на класа
на възела, имплементиране на неговите методи print()
и
getIterator()
, създаване на парсваща функция и накрая регистриране
на тага.
Цел: Имплементиране на {datetime}
за извеждане на текущата дата
и час с помощта на PHP функцията date()
.
Създаване на класа на възела
Първо, имаме нужда от клас, който ще представлява нашия таг в Abstract Syntax
Tree (AST). Както беше обсъдено по-горе, наследяваме от
Latte\Compiler\Nodes\StatementNode
.
Създайте файл (напр. DatetimeNode.php
) и дефинирайте класа:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Функцията за парсване на таг, извиквана, когато е намерен {datetime}.
*/
public static function create(Tag $tag): self
{
// Нашият прост таг в момента не приема никакви аргументи, така че не трябва да парсваме нищо
$node = $tag->node = new self;
return $node;
}
/**
* Генерира PHP код, който ще бъде изпълнен при рендиране на шаблона.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Предоставя достъп до дъщерните възли за компилационните проходи на Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Когато Latte срещне {datetime}
в шаблона, той извиква парсващата
функция create()
. Нейната задача е да върне инстанция на
DatetimeNode
.
Методът print()
генерира PHP код, който ще бъде изпълнен при
рендиране на шаблона. Извикваме метода $context->format()
, който
съставя крайния низ от PHP код за компилирания шаблон. Първият аргумент,
'echo date('Y-m-d H:i:s') %line;'
, е маска, в която се попълват следващите
параметри. Placeholder-ът %line
казва на метода format()
да използва
втория аргумент, който е $this->position
, и да вмъкне коментар като
/* line 15 */
, който свързва генерирания PHP код обратно към
оригиналния ред на шаблона, което е ключово за дебъгване.
Свойството $this->position
се наследява от базовия клас Node
и
се задава автоматично от парсера на Latte. То съдържа обект Latte\Compiler\Position, който показва
къде е намерен тагът в изходния файл .latte
.
Методът getIterator()
е от съществено значение за компилационните
проходи. Той трябва да предоставя всички дъщерни възли, но нашият прост
DatetimeNode
в момента няма никакви аргументи или съдържание,
следователно няма дъщерни възли. Въпреки това, методът все още трябва
да съществува и да бъде генератор, т.е. ключовата дума yield
трябва
да присъства по някакъв начин в тялото на метода.
Регистрация чрез разширение
Накрая, нека информираме Latte за новия таг. Създайте клас на разширение (напр.
MyLatteExtension.php
) и регистрирайте тага в неговия метод getTags()
.
<?php
namespace App\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Връща списък с тагове, предоставени от това разширение.
* @return array<string, callable> Карта: 'име-на-таг' => парсваща-функция
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// По-късно регистрирайте повече тагове тук
];
}
}
След това регистрирайте това разширение в Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Създайте шаблон:
<p>Страницата е генерирана: {datetime}</p>
Очакван изход:
<p>Страницата е генерирана: 2023-10-27 11:00:00</p>
Резюме на тази фаза
Успешно създадохме основен персонализиран таг {datetime}
.
Дефинирахме неговото представяне в AST (DatetimeNode
), обработихме
неговото парсване (create()
), специфицирахме как трябва да генерира
PHP код (print()
), осигурихме достъп до неговите деца за обхождане
(getIterator()
) и го регистрирахме в Latte.
В следващата секция ще подобрим този таг, така че да приема аргументи, и ще покажем как да парсваме изрази и да управляваме дъщерни възли.
Парсване на аргументи на таг
Нашият прост таг {datetime}
работи, но не е много гъвкав. Нека го
подобрим, така че да приема незадължителен аргумент: форматиращ низ за
функцията date()
. Изискваният синтаксис ще бъде
{datetime $format}
.
Цел: Да се модифицира {datetime}
, така че да приема
незадължителен PHP израз като аргумент, който ще бъде използван като
форматиращ низ за date()
.
Представяне на TagParser
Преди да модифицираме кода, е важно да разберем инструмента, който ще
използваме Latte\Compiler\TagParser.
Когато основният парсер на Latte (TemplateParser
) срещне Latte таг като
{datetime ...}
или n:атрибут, той делегира парсването на съдържанието
вътре в тага (частта между {
и }
или стойността на
атрибута) на специализиран TagParser
.
Този TagParser
работи изключително с аргументите на тага.
Неговата задача е да обработва токените, представляващи тези
аргументи. Ключово е, че трябва да обработи цялото съдържание,
което му е предоставено. Ако вашата парсваща функция приключи, но
TagParser
не е достигнал края на аргументите (проверява се чрез
$tag->parser->isEnd()
), Latte ще хвърли изключение, тъй като това показва,
че вътре в тага са останали неочаквани токени. Обратно, ако тагът
изисква аргументи, трябва да извикате $tag->expectArguments()
в
началото на вашата парсваща функция. Този метод проверява дали има
аргументи и хвърля полезно изключение, ако тагът е бил използван без
никакви аргументи.
TagParser
предлага полезни методи за парсване на различни видове
аргументи:
parseExpression(): ExpressionNode
: Парсва PHP-подобен израз (променливи, литерали, оператори, извиквания на функции/методи и т.н.). Обработва синтактичната захар на Latte, като например третирането на прости буквено-цифрови низове като низове в кавички (напр.foo
се парсва, сякаш е'foo'
).parseUnquotedStringOrExpression(): ExpressionNode
: Парсва или стандартен израз, или низ без кавички. Низовете без кавички са последователности, позволени от Latte без кавички, често използвани за неща като пътища до файлове (напр.{include ../file.latte}
). Ако парсва низ без кавички, връщаStringNode
.parseArguments(): ArrayNode
: Парсва аргументи, разделени със запетаи, потенциално с ключове, като10, name: 'John', true
.parseModifier(): ModifierNode
: Парсва филтри като|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Парсва PHP указания за тип катоint
,?string
,array|Foo
.
За по-сложни или ниско ниво нужди от парсване, можете директно да
взаимодействате с потока от
токени чрез $tag->parser->stream
. Този обект предоставя методи за
проверка и обработка на отделни токени:
$tag->parser->stream->is(...): bool
: Проверява дали текущият токен съответства на някой от указаните типове (напр.Token::Php_Variable
) или литерални стойности (напр.'as'
) без да го консумира. Полезно за поглед напред.$tag->parser->stream->consume(...): Token
: Консумира текущия токен и премества позицията на потока напред. Ако са предоставени очаквани типове/стойности на токени като аргументи и текущият токен не съответства, хвърляCompileException
. Използвайте това, когато очаквате определен токен.$tag->parser->stream->tryConsume(...): ?Token
: Опитва се да консумира текущия токен само ако съответства на един от указаните типове/стойности. Ако съответства, консумира токена и го връща. Ако не съответства, оставя позицията на потока непроменена и връщаnull
. Използвайте това за незадължителни токени или когато избирате между различни синтактични пътища.
Актуализиране на парсващата функция
create()
С това разбиране, нека модифицираме метода create()
в
DatetimeNode
, така че да парсва незадължителния форматиращ аргумент с
помощта на $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
{
// Добавяме публично свойство за съхраняване на парсвания възел на израза за формат
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Проверяваме дали съществуват някакви токени
if (!$tag->parser->isEnd()) {
// Парсваме аргумента като PHP-подобен израз с помощта на TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... методите print() и getIterator() ще бъдат актуализирани по-нататък ...
}
Добавихме публично свойство $format
. В create()
сега
използваме $tag->parser->isEnd()
, за да проверим дали съществуват
аргументи. Ако да, $tag->parser->parseExpression()
обработва токените за
израза. Тъй като TagParser
трябва да обработи всички входни токени,
Latte автоматично ще хвърли грешка, ако потребителят напише нещо
неочаквано след израза за формат (напр. {datetime 'Y-m-d', unexpected}
).
Актуализиране на метода print()
Сега нека модифицираме метода print()
, така че да използва
парсвания израз за формат, съхранен в $this->format
. Ако не е
предоставен формат ($this->format
е null
), трябва да използваме
форматиращ низ по подразбиране, например 'Y-m-d H:i:s'
.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node отпечатва PHP кодовото представяне на $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
В променливата $formatNode
съхраняваме възела на AST, представляващ
форматиращия низ за PHP функцията date()
. Използваме тук оператора
за нулево сливане (??
). Ако потребителят е предоставил аргумент в
шаблона (напр. {datetime 'd.m.Y'}
), тогава свойството $this->format
съдържа съответния възел (в този случай StringNode
със стойност
'd.m.Y'
) и този възел се използва. Ако потребителят не е предоставил
аргумент (написал е само {datetime}
), свойството $this->format
е
null
и вместо това създаваме нов StringNode
с формат по
подразбиране 'Y-m-d H:i:s'
. Това гарантира, че $formatNode
винаги
съдържа валиден възел на AST за формата.
В маската 'echo date(%node) %line;'
се използва нов placeholder %node
,
който казва на метода format()
да вземе първия следващ аргумент
(който е нашият $formatNode
), да извика неговия метод print()
(който ще върне неговото PHP кодово представяне) и да вмъкне резултата на
позицията на placeholder-а.
Имплементиране на getIterator()
за
подвъзли
Нашият DatetimeNode
сега има дъщерен възел: изразът $format
.
Трябва да направим този дъщерен възел достъпен за компилационните
проходи, като го предоставим в метода getIterator()
. Не забравяйте да
предоставите референция (&
), за да позволите на проходите
потенциално да заменят възела.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Защо е толкова важно? Представете си Sandbox проход, който трябва да
провери дали аргументът $format
не съдържа забранено извикване на
функция (напр. {datetime dangerousFunction()}
). Ако getIterator()
не
предостави $this->format
, Sandbox проходът никога няма да види
извикването на dangerousFunction()
вътре в аргумента на нашия таг, което
би създало потенциална дупка в сигурността. Като му го предоставяме,
позволяваме на Sandbox (и други проходи) да проверяват и потенциално да
модифицират възела на израза $format
.
Използване на подобрения таг
Тагът сега правилно обработва незадължителния аргумент:
Формат по подразбиране: {datetime}
Персонализиран формат: {datetime 'd.m.Y'}
Използване на променлива: {datetime $userDateFormatPreference}
{* Това би причинило грешка след парсването на 'd.m.Y', тъй като ", foo" е неочаквано *}
{* {datetime 'd.m.Y', foo} *}
След това ще разгледаме създаването на сдвоени тагове, които обработват съдържанието между тях.
Обработка на сдвоени тагове
Досега нашият таг {datetime}
беше самозатварящ се
(концептуално). Той няма съдържание между началния и крайния таг. Много
полезни тагове обаче работят с блок от съдържание на шаблона. Те се
наричат сдвоени тагове. Примерите включват {if}...{/if}
,
{block}...{/block}
или персонализиран таг, който сега ще създадем:
{debug}...{/debug}
.
Този таг ще ни позволи да включим в нашите шаблони дебъг информация, която трябва да бъде видима само по време на разработка.
Цел: Да се създаде сдвоен таг {debug}
, чието съдържание се
рендира само когато е активен специфичен флаг “режим на
разработка”.
Представяне на Providers
Понякога вашите тагове се нуждаят от достъп до данни или услуги, които не се предават директно като параметри на шаблона. Например, определяне дали приложението е в режим на разработка, достъп до обект на потребител или получаване на конфигурационни стойности. Latte предоставя механизъм, наречен Providers за тази цел.
Providers се регистрират във вашето разширение с помощта на метода
getProviders()
. Този метод връща асоциативен масив, където ключовете
са имената, под които providers ще бъдат достъпни в кода на шаблона по време
на изпълнение, а стойностите са действителните данни или обекти.
Вътре в PHP кода, генериран от метода print()
на вашия таг, можете да
получите достъп до тези providers чрез специалното свойство на обекта
$this->global
. Тъй като това свойство се споделя между всички
разширения, добра практика е да добавяте префикс към имената на
вашите providers, за да предотвратите потенциални конфликти на имена с
ключови providers на Latte или providers от други разширения на трети страни. Често
срещана конвенция е да се използва кратък, уникален префикс, свързан с
вашия производител или име на разширение. За нашия пример ще
използваме префикс app
и флагът за режим на разработка ще бъде
достъпен като $this->global->appDevMode
.
Ключовата дума yield
за парсване на
съдържание
Как казваме на парсера на Latte да обработи съдържанието между
{debug}
и {/debug}
? Тук влиза в игра ключовата дума yield
.
Когато yield
се използва във функцията create()
, функцията се
превръща в PHP генератор.
Нейното изпълнение се спира и контролът се връща към основния
TemplateParser
. След това TemplateParser
продължава да парсва
съдържанието на шаблона, докато не срещне съответния затварящ таг
({/debug}
в нашия случай).
След като бъде намерен затварящият таг, TemplateParser
възобновява
изпълнението на нашата функция create()
точно след инструкцията
yield
. Стойността, върната от инструкцията yield
, е масив,
съдържащ два елемента:
AreaNode
, представляващ парсваното съдържание между началния и крайния таг.- Обект
Tag
, представляващ затварящия таг (напр.{/debug}
).
Нека създадем клас DebugNode
и неговия метод create
, използващ
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
{
// Публично свойство за съхраняване на парсваното вътрешно съдържание
public AreaNode $content;
/**
* Парсваща функция за сдвоен таг {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // забележете типа на връщане
{
$node = $tag->node = new self;
// Спиране на парсването, получаване на вътрешното съдържание и крайния таг, когато е намерен {/debug}
[$node->content, $endTag] = yield;
return $node;
}
// ... print() и getIterator() ще бъдат имплементирани по-нататък ...
}
Забележка: $endTag
е null
, ако тагът се използва като
n:атрибут, т.е. <div n:debug>...</div>
.
Имплементиране на print()
за
условно рендиране
Методът print()
сега трябва да генерира PHP код, който по време на
изпълнение проверява provider-а appDevMode
и изпълнява кода за
вътрешното съдържание само ако флагът е true.
public function print(PrintContext $context): string
{
// Генерира PHP инструкция 'if', която по време на изпълнение проверява provider-а
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Ако е в режим на разработка, извежда вътрешното съдържание
%node
}
XX,
$this->position, // За %line коментар
$this->content, // Възел, съдържащ AST на вътрешното съдържание
);
}
Това е просто. Използваме PrintContext::format()
, за да създадем
стандартна PHP инструкция if
. Вътре в if
поставяме placeholder
%node
за $this->content
. Latte рекурсивно ще извика
$this->content->print($context)
, за да генерира PHP код за вътрешната част на
тага, но само ако $this->global->appDevMode
се оцени като true по време на
изпълнение.
Имплементиране на getIterator()
за
съдържание
Точно както при възела на аргумента в предишния пример, нашият
DebugNode
сега има дъщерен възел: AreaNode $content
. Трябва да го
направим достъпен, като го предоставим в getIterator()
:
public function &getIterator(): \Generator
{
// Предоставя референция към възела на съдържанието
yield $this->content;
}
Това позволява на компилационните проходи да слязат в съдържанието
на нашия таг {debug}
, което е важно, дори ако съдържанието се
рендира условно. Например, Sandbox трябва да анализира съдържанието,
независимо дали appDevMode
е true или false.
Регистрация и използване
Регистрирайте тага и provider-а във вашето разширение:
class MyLatteExtension extends Extension
{
// Предполагаме, че $isDevelopmentMode се определя някъде (напр. от конфигурацията)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Регистрация на новия таг
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Регистрация на provider-а
];
}
}
// При регистрация на разширението:
$isDev = true; // Определете това въз основа на средата на вашето приложение
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
И неговото използване в шаблона:
<p>Обикновено съдържание, видимо винаги.</p>
{debug}
<div class="debug-panel">
ID на текущия потребител: {$user->id}
Време на заявката: {=time()}
</div>
{/debug}
<p>Друго обикновено съдържание.</p>
Интеграция на n:атрибути
Latte предлага удобен съкратен запис за много сдвоени тагове: n:атрибути. Ако имате сдвоен таг като
{tag}...{/tag}
и искате неговият ефект да се приложи директно към един
HTML елемент, често можете да го запишете по-икономично като атрибут
n:tag
на този елемент.
За повечето стандартни сдвоени тагове, които дефинирате (като нашия
{debug}
), Latte автоматично ще позволи съответната версия на n:
атрибута. По време на регистрацията не е необходимо да правите нищо
допълнително:
{* Стандартно използване на сдвоен таг *}
{debug}<div>Информация за дебъгване</div>{/debug}
{* Еквивалентно използване с n:атрибут *}
<div n:debug>Информация за дебъгване</div>
И двете версии ще рендират <div>
само ако
$this->global->appDevMode
е true. Префиксите inner-
и tag-
също
работят според очакванията.
Понякога логиката на вашия таг може да се нуждае от леко различно
поведение в зависимост от това дали се използва като стандартен сдвоен
таг или като n:атрибут, или дали се използва префикс като n:inner-tag
или n:tag-tag
. Обектът Latte\Compiler\Tag
, предаден на вашата парсваща
функция create()
, предоставя тази информация:
$tag->isNAttribute(): bool
: Връщаtrue
, ако тагът се парсва като n:атрибут$tag->prefix: ?string
: Връща префикса, използван с n:атрибута, който може да бъдеnull
(не е n:атрибут),Tag::PrefixNone
,Tag::PrefixInner
илиTag::PrefixTag
Сега, когато разбираме простите тагове, парсването на аргументи,
сдвоените тагове, providers и n:атрибутите, нека се заемем с по-сложен
сценарий, включващ тагове, вложени в други тагове, използвайки нашия
таг {debug}
като отправна точка.
Междинни тагове
Някои сдвоени тагове позволяват или дори изискват други тагове да се
появят вътре в тях преди крайния затварящ таг. Те се наричат
междинни тагове. Класически примери включват
{if}...{elseif}...{else}...{/if}
или {switch}...{case}...{default}...{/switch}
.
Нека разширим нашия таг {debug}
с поддръжка на незадължителна
клауза {else}
, която ще бъде рендирана, когато приложението не
е в режим на разработка.
Цел: Да се модифицира {debug}
, така че да поддържа
незадължителен междинен таг {else}
. Крайният синтаксис трябва да
бъде {debug} ... {else} ... {/debug}
.
Парсване на междинни тагове с помощта на
yield
Вече знаем, че yield
спира парсващата функция create()
и връща
парсваното съдържание заедно с крайния таг. yield
обаче предлага
повече контрол: можете да му предоставите масив от имена на междинни
тагове. Когато парсерът срещне някой от тези указани тагове на
същото ниво на влагане (т.е. като преки деца на родителския таг, не
вътре в други блокове или тагове вътре в него), той също спира
парсването.
Когато парсването спре поради междинен таг, то спира парсването на
съдържанието, възобновява генератора create()
и предава обратно
частично парсваното съдържание и междинния таг сам по себе си
(вместо крайния затварящ таг). Нашата функция create()
след това
може да обработи този междинен таг (напр. да парсва неговите аргументи,
ако има такива) и отново да използва yield
, за да парсва
следващата част от съдържанието до крайния затварящ таг или
друг очакван междинен таг.
Нека модифицираме DebugNode::create()
, така че да очаква {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
{
// Съдържание за частта {debug}
public AreaNode $thenContent;
// Незадължително съдържание за частта {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield и очакваме или {/debug} или {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Проверяваме дали тагът, при който сме спрели, е бил {else}
if ($nextTag?->name === 'else') {
// Yield отново за парсване на съдържанието между {else} и {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() и getIterator() ще бъдат актуализирани по-нататък ...
}
Сега yield ['else']
казва на Latte да спре парсването не само за
{/debug}
, но и за {else}
. Ако {else}
бъде намерен, $nextTag
ще съдържа обект Tag
за {else}
. След това отново използваме
yield
без аргументи, което означава, че сега очакваме само крайния
таг {/debug}
, и съхраняваме резултата в $node->elseContent
. Ако
{else}
не е бил намерен, $nextTag
би бил Tag
за {/debug}
(или null
, ако се използва като n:атрибут) и $node->elseContent
би
останал null
.
Имплементиране на print()
с {else}
Методът print()
трябва да отразява новата структура. Той трябва
да генерира PHP инструкция if/else
, базирана на provider-а devMode
.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Код за клона 'then' (съдържание на {debug})
} else {
%node // Код за клона 'else' (съдържание на {else})
}
XX,
$this->position, // Номер на ред за условието 'if'
$this->thenContent, // Първи placeholder %node
$this->elseContent ?? new NopNode, // Втори placeholder %node
);
}
Това е стандартна PHP структура if/else
. Използваме %node
два
пъти; format()
замества предоставените възли последователно.
Използваме ?? new NopNode
, за да избегнем грешки, ако
$this->elseContent
е null
– NopNode
просто не
отпечатва нищо.
Имплементиране на getIterator()
за
двете съдържания
Сега имаме потенциално два дъщерни възела на съдържание
($thenContent
и $elseContent
). Трябва да предоставим и двата, ако
съществуват:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Използване на подобрения таг
Тагът сега може да бъде използван с незадължителна клауза
{else}
:
{debug}
<p>Показване на дебъг информация, защото devMode е ВКЛЮЧЕНО.</p>
{else}
<p>Дебъг информацията е скрита, защото devMode е ИЗКЛЮЧЕНО.</p>
{/debug}
Обработка на състояние и влагане
Нашите предишни примери ({datetime}
, {debug}
) бяха относително
без състояние в рамките на своите методи print()
. Те или директно
извеждаха съдържание, или извършваха проста условна проверка,
базирана на глобален provider. Много тагове обаче трябва да управляват
някаква форма на състояние по време на рендиране или включват
оценка на потребителски изрази, които трябва да бъдат изпълнени само
веднъж поради производителност или коректност. Освен това трябва да
обмислим какво се случва, когато нашите персонализирани тагове са
вложени.
Нека илюстрираме тези концепции, като създадем таг
{repeat $count}...{/repeat}
. Този таг ще повтори своето вътрешно съдържание
$count
пъти.
Цел: Имплементиране на {repeat $count}
, който повтаря своето
съдържание указан брой пъти.
Нуждата от временни и уникални променливи
Представете си, че потребителят напише:
{repeat rand(1, 5)} Съдържание {/repeat}
Ако наивно генерираме PHP for
цикъл по този начин в нашия метод
print()
:
// Опростен, НЕПРАВИЛЕН генериран код
for ($i = 0; $i < rand(1, 5); $i++) {
// извеждане на съдържание
}
Това би било грешно! Изразът rand(1, 5)
би бил преизчислен при
всяка итерация на цикъла, което би довело до непредсказуем брой
повторения. Трябва да оценим израза $count
веднъж преди
началото на цикъла и да съхраним резултата му.
Ще генерираме PHP код, който първо оценява израза за броя и го
съхранява във временна променлива по време на изпълнение. За да
предотвратим конфликти с променливи, дефинирани от потребителя на
шаблона, и вътрешни променливи на Latte (като $ʟ_...
), ще
използваме конвенцията за префикс $__
(двойно долно тире) за
нашите временни променливи.
Генерираният код тогава би изглеждал така:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// извеждане на съдържание
}
Сега да разгледаме влагането:
{repeat $countA} {* Външен цикъл *}
{repeat $countB} {* Вътрешен цикъл *}
...
{/repeat}
{/repeat}
Ако както външният, така и вътрешният таг {repeat}
генерират код,
използващ едни и същи имена на временни променливи (напр.
$__count
и $__i
), вътрешният цикъл би презаписал променливите
на външния цикъл, което би нарушило логиката.
Трябва да гарантираме, че временните променливи, генерирани за всяка
инстанция на тага {repeat}
, са уникални. Постигаме това с
помощта на PrintContext::generateId()
. Този метод връща уникално цяло число
по време на фазата на компилация. Можем да добавим това ID към имената на
нашите временни променливи.
Така че вместо $__count
, ще генерираме $__count_1
за първия таг
repeat, $__count_2
за втория и т.н. Подобно за брояча на цикъла ще
използваме $__i_1
, $__i_2
и т.н.
Имплементиране на RepeatNode
Нека създадем класа на възела.
<?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;
/**
* Парсваща функция за {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // уверява се, че $count е предоставен
$node = $tag->node = new self;
// Парсва израза за броя
$node->count = $tag->parser->parseExpression();
// Получаване на вътрешното съдържание
[$node->content] = yield;
return $node;
}
/**
* Генерира PHP 'for' цикъл с уникални имена на променливи.
*/
public function print(PrintContext $context): string
{
// Генериране на уникални имена на променливи
$id = $context->generateId();
$countVar = '$__count_' . $id; // напр. $__count_1, $__count_2, и т.н.
$iteratorVar = '$__i_' . $id; // напр. $__i_1, $__i_2, и т.н.
return $context->format(
<<<'XX'
// Оценка на израза за броя *веднъж* и съхраняване
%raw = (int) (%node);
// Цикъл с използване на съхранения брой и уникална итерационна променлива
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Рендиране на вътрешното съдържание
}
XX,
$countVar, // %0 - Променлива за съхраняване на броя
$this->count, // %1 - Възел на израза за броя
$iteratorVar, // %2 - Име на итерационната променлива на цикъла
$this->position, // %3 - Коментар с номер на ред за самия цикъл
$this->content // %4 - Възел на вътрешното съдържание
);
}
/**
* Предоставя дъщерните възли (израз за броя и съдържание).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
Методът create()
парсва изисквания израз $count
с помощта на
parseExpression()
. Първо се извиква $tag->expectArguments()
. Това
гарантира, че потребителят е предоставил нещо след {repeat}
.
Докато $tag->parser->parseExpression()
би се провалило, ако нищо не е
предоставено, съобщението за грешка може да бъде за неочакван
синтаксис. Използването на expectArguments()
предоставя много по-ясна
грешка, конкретно посочваща, че липсват аргументи за тага
{repeat}
.
Методът print()
генерира PHP код, отговорен за изпълнението на
логиката на повторение по време на изпълнение. Започва с генериране на
уникални имена за временните PHP променливи, които ще са му нужни.
Методът $context->format()
се извиква с нов placeholder %raw
, който
вмъква суровия низ, предоставен като съответен аргумент. Тук той
вмъква уникалното име на променлива, съхранено в $countVar
(напр.
$__count_1
). А какво да кажем за %0.raw
и %2.raw
? Това
демонстрира позиционни placeholders. Вместо просто %raw
, който
взема следващия наличен суров аргумент, %2.raw
изрично взема
аргумента на индекс 2 (който е $iteratorVar
) и вмъква неговата сурова
низова стойност. Това ни позволява да използваме повторно низа
$iteratorVar
, без да го предаваме многократно в списъка с аргументи за
format()
.
Това внимателно конструирано извикване на format()
генерира
ефективен и безопасен PHP цикъл, който правилно обработва израза за броя
и избягва конфликти на имена на променливи, дори когато таговете
{repeat}
са вложени.
Регистрация и използване
Регистрирайте тага във вашето разширение:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Регистрация на тага repeat
];
}
}
Използвайте го в шаблона, включително влагане:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Вътрешен цикъл</td>
{/repeat}
</tr>
{/repeat}
Този пример демонстрира как да се обработва състояние (броячи на
цикли) и потенциални проблеми с влагането с помощта на временни
променливи с префикс $__
и уникални с ID от
PrintContext::generateId()
.
Чисти n:атрибути
Докато много n:атрибути
като n:if
или n:foreach
служат
като удобни съкращения за техните двойници в сдвоени тагове
({if}...{/if}
, {foreach}...{/foreach}
), Latte също позволява дефинирането на
тагове, които съществуват само под формата на n:атрибут. Те често
се използват за модифициране на атрибути или поведение на HTML елемента,
към който са прикрепени.
Стандартните примери, вградени в Latte, включват n:class
, който помага за динамичното
изграждане на атрибута class
, и n:attr
, който може да зададе
множество произволни атрибути.
Нека създадем наш собствен чист n:атрибут: n:confirm
, който добавя
JavaScript диалогов прозорец за потвърждение преди извършване на действие
(като следване на връзка или изпращане на формуляр).
Цел: Имплементиране на n:confirm="'Сигурни ли сте?'"
, който
добавя обработчик onclick
за предотвратяване на действието по
подразбиране, ако потребителят отмени диалоговия прозорец за
потвърждение.
Имплементиране на ConfirmNode
Нуждаем се от клас Node и парсваща функция.
<?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;
}
/**
* Генерира код на атрибута 'onclick' с правилно екраниране.
*/
public function print(PrintContext $context): string
{
// Гарантира правилно екраниране за контекстите на JavaScript и HTML атрибут.
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;
}
}
Методът print()
генерира PHP код, който в крайна сметка по време на
рендиране на шаблона извежда HTML атрибута onclick="..."
. Обработката
на вложени контексти (JavaScript вътре в HTML атрибут) изисква внимателно
екраниране. Филтърът LR\Filters::escapeJs(%node)
се извиква по време на
изпълнение и екранира съобщението правилно за използване вътре в
JavaScript (изходът би бил като "Sure?"
). След това филтърът
LR\Filters::escapeHtmlAttr(...)
екранира знаците, които са специални в HTML
атрибутите, така че това би променило изхода на
return confirm("Sure?")
. Това двустепенно екраниране по време на
изпълнение гарантира, че съобщението е безопасно за JavaScript и
резултатният JavaScript код е безопасен за вмъкване в HTML атрибута
onclick
.
Регистрация и използване
Регистрирайте n:атрибута във вашето разширение. Не забравяйте
префикса n:
в ключа:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // Регистрация на n:confirm
];
}
}
Сега можете да използвате n:confirm
върху връзки, бутони или
елементи на формуляр:
<a href="delete.php?id=123" n:confirm='"Наистина ли искате да изтриете елемент {$id}?"'>Изтриване</a>
Генериран HTML:
<a href="delete.php?id=123" onclick="return confirm("Наистина ли искате да изтриете елемент 123?")">Изтриване</a>
Когато потребителят кликне върху връзката, браузърът изпълнява кода
onclick
, показва диалоговия прозорец за потвърждение и преминава
към delete.php
само ако потребителят кликне върху “OK”.
Този пример демонстрира как може да се създаде чист n:атрибут за
модифициране на поведението или атрибутите на своя хост HTML елемент
чрез генериране на подходящ PHP код в неговия метод print()
. Не
забравяйте за двойното екраниране, което често се изисква: веднъж за
целевия контекст (JavaScript в този случай) и отново за контекста на HTML
атрибута.
Напреднали теми
Докато предишните секции покриват основните концепции, тук са няколко по-напреднали теми, на които може да попаднете при създаването на персонализирани Latte тагове.
Режими на изход на тагове
Обектът Tag
, предаден на вашата функция create()
, има
свойство outputMode
. Това свойство влияе върху това как Latte третира
околните празни пространства и индентация, особено когато тагът се
използва на собствен ред. Можете да модифицирате това свойство във
вашата функция create()
.
Tag::OutputKeepIndentation
(По подразбиране за повечето тагове като{=...}
): Latte се опитва да запази индентацията преди тага. Новите редове след тага обикновено се запазват. Това е подходящо за тагове, които извеждат съдържание в реда.Tag::OutputRemoveIndentation
(По подразбиране за блокови тагове като{if}
,{foreach}
): Latte премахва водещата индентация и потенциално един следващ нов ред. Това помага да се поддържа генерираният PHP код по-чист и предотвратява допълнителни празни редове в HTML изхода, причинени от самия таг. Използвайте това за тагове, които представляват контролни структури или блокове, които сами по себе си не трябва да добавят празни пространства.Tag::OutputNone
(Използва се от тагове като{var}
,{default}
): Подобно наRemoveIndentation
, но сигнализира по-силно, че самият таг не произвежда директен изход, потенциално влияейки върху обработката на празни пространства около него още по-агресивно. Подходящо за декларативни или задаващи тагове.
Изберете режима, който най-добре отговаря на целта на вашия таг. За
повечето структурни или контролни тагове обикновено е подходящ
OutputRemoveIndentation
.
Достъп до родителски/най-близки тагове
Понякога поведението на тага трябва да зависи от контекста, в който
се използва, конкретно в кой родителски таг(ове) се намира. Обектът
Tag
, предаден на вашата функция create()
, предоставя метода
closestTag(array $classes, ?callable $condition = null): ?Tag
точно за тази цел.
Този метод търси нагоре в йерархията на текущо отворените тагове
(включително HTML елементи, представени вътрешно по време на парсване) и
връща обекта Tag
на най-близкия предшественик, който отговаря на
специфични критерии. Ако не бъде намерен съответстващ предшественик,
връща null
.
Масивът $classes
указва какъв вид предшествени тагове търсите.
Проверява дали асоциираният възел на предшествения таг
($ancestorTag->node
) е инстанция на този клас.
function create(Tag $tag)
{
// Търсене на най-близкия предшествен таг, чийто възел е инстанция на ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Можем да получим достъп до самата инстанция на ForeachNode:
$foreachNode = $foreachTag->node;
}
}
Забележете $foreachTag->node
: Това работи само защото е конвенция в
разработката на Latte тагове незабавно да се присвои създаденият възел
на $tag->node
в рамките на метода create()
, както винаги сме
правили.
Понякога само сравнението на типа на възела не е достатъчно. Може да
се наложи да проверите специфично свойство на потенциалния
предшествен таг или неговия възел. Незадължителният втори аргумент за
closestTag()
е callable, който приема потенциалния предшествен обект
Tag
и трябва да връща дали е валидно съвпадение.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Условие: блокът трябва да е динамичен
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
Използването на closestTag()
позволява създаването на тагове, които
са контекстно осъзнати и налагат правилно използване в рамките на
структурата на вашия шаблон, което води до по-здрави и разбираеми
шаблони.
Placeholders на PrintContext::format()
Често сме използвали PrintContext::format()
, за да генерираме PHP код в
методите print()
на нашите възли. Той приема низ-маска и следващи
аргументи, които заместват placeholders в маската. Ето резюме на наличните
placeholders:
%node
: Аргументът трябва да бъде инстанция наNode
. Извиква методаprint()
на възела и вмъква резултантния низ от PHP код.%dump
: Аргументът е всяка PHP стойност. Експортира стойността в валиден PHP код. Подходящо за скалари, масиви, null.$context->format('echo %dump;', 'Hello')
→echo 'Hello';
$context->format('$arr = %dump;', [1, 2])
→$arr = [1, 2];
%raw
: Вмъква аргумента директно в изходния PHP код без никакво екраниране или модификация. Използвайте с повишено внимание, предимно за вмъкване на предварително генерирани фрагменти от PHP код или имена на променливи.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: Аргументът трябва да бъдеExpression\ArrayNode
. Извежда елементите на масива, форматирани като аргументи за извикване на функция или метод (разделени със запетаи, обработва именувани аргументи, ако присъстват).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: Аргументът трябва да бъде обектPosition
(обикновено$this->position
). Вмъква PHP коментар/* line X */
, указващ номера на реда на източника.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Генерира PHP код, който по време на изпълнение екранира вътрешния израз, използвайки текущите контекстно осъзнати правила за екраниране.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: Аргументът трябва да бъдеModifierNode
. Генерира PHP код, който прилага филтрите, указани вModifierNode
, към вътрешното съдържание, включително контекстно осъзнато екраниране, ако не е забранено с|noescape
.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Подобно на%modify
, но предназначено за модифициране на блокове от уловено съдържание (често HTML).
Можете изрично да се позовавате на аргументи по техния индекс (от
нула): %0.node
, %1.dump
, %2.raw
и т.н. Това позволява
повторното използване на аргумент няколко пъти в маската, без да го
предавате многократно на format()
. Вижте примера с тага {repeat}
,
където бяха използвани %0.raw
и %2.raw
.
Пример за комплексно парсване на аргументи
Докато parseExpression()
, parseArguments()
и т.н., покриват много случаи,
понякога се нуждаете от по-сложна логика за парсване, използваща
по-ниско ниво TokenStream
, достъпно чрез $tag->parser->stream
.
Цел: Да се създаде таг {embedYoutube $videoID, width: 640, height: 480}
. Искаме да
парсваме изискваното ID на видеото (низ или променлива), последвано от
незадължителни двойки ключ-стойност за размерите.
<?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;
// Парсване на изискваното ID на видеото
$node->videoId = $tag->parser->parseExpression();
// Парсване на незадължителни двойки ключ-стойност
$stream = $tag->parser->stream; // Получаване на потока от токени
while ($stream->tryConsume(',')) { // Изисква разделяне със запетая
// Очакване на идентификатор 'width' или 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Очакване на разделител двоеточие
$value = $tag->parser->parseExpression(); // Парсване на израза за стойност
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Неизвестен аргумент '$key'. Очаквано 'width' или 'height'.", $keyToken->position);
}
}
return $node;
}
}
Това ниво на контрол ви позволява да дефинирате много специфични и комплексни синтаксиси за вашите персонализирани тагове чрез директно взаимодействие с потока от токени.
Използване на AuxiliaryNode
Latte предоставя общи “спомагателни” възли за специални ситуации по
време на генериране на код или в рамките на компилационни проходи. Това
са AuxiliaryNode
и Php\Expression\AuxiliaryNode
.
Считайте AuxiliaryNode
за гъвкав контейнерен възел, който делегира
своите основни функционалности – генериране на код и излагане на
дъщерни възли – на аргументите, предоставени в неговия конструктор:
- Делегиране на
print()
: Първият аргумент на конструктора е PHP closure. Когато Latte извиква методаprint()
наAuxiliaryNode
, той изпълнява тази предоставена closure. Closure приемаPrintContext
и всички възли, предадени във втория аргумент на конструктора, което ви позволява да дефинирате напълно персонализирана логика за генериране на PHP код по време на изпълнение. - Делегиране на
getIterator()
: Вторият аргумент на конструктора е масив от обектиNode
. Когато Latte трябва да обходи децата наAuxiliaryNode
(напр. по време на компилационни проходи), неговият методgetIterator()
просто предоставя възлите, изброени в този масив.
Пример:
$node = new AuxiliaryNode(
// 1. Тази closure става тялото на print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Тези възли се предоставят от метода getIterator() и се предават на closure по-горе
[$argumentNode1, $argumentNode2]
);
Latte предоставя два различни типа въз основа на това къде трябва да вмъкнете генерирания код:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Използвайте това, когато трябва да генерирате част от PHP код, която представлява изразLatte\Compiler\Nodes\AuxiliaryNode
: Използвайте това за по-общи цели, когато трябва да вмъкнете блок от PHP код, представляващ една или повече инструкции
Важна причина да използвате AuxiliaryNode
вместо стандартни възли
(като StaticMethodCallNode
) в рамките на вашия метод print()
или
компилационен проход е контролът на видимостта за последващи
компилационни проходи, особено тези, свързани със сигурността, като
Sandbox.
Разгледайте сценарий: Вашият компилационен проход трябва да обвие
предоставен от потребителя израз ($userExpr
) с извикване на
специфична, доверена помощна функция myInternalSanitize($userExpr)
. Ако
създадете стандартен възел new FunctionCallNode('myInternalSanitize', [$userExpr])
, той
ще бъде напълно видим за обхождането на AST. Ако Sandbox проходът се изпълни
по-късно и myInternalSanitize
не е в неговия списък с разрешени, Sandbox
може да блокира или модифицира това извикване, потенциално
нарушавайки вътрешната логика на вашия таг, дори ако вие, авторът
на тага, знаете, че това специфично извикване е безопасно и необходимо.
Можете следователно да генерирате извикването директно в рамките на
closure на AuxiliaryNode
.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... вътре в print() или компилационен проход ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Директно генериране на PHP код
$userExpr,
),
// ВАЖНО: Все още предайте оригиналния възел на потребителския израз тук!
[$userExpr],
);
В този случай Sandbox проходът вижда AuxiliaryNode
, но не анализира PHP
кода, генериран от неговата closure. Той не може директно да блокира
извикването на myInternalSanitize
, генерирано вътре в closure.
Докато самият генериран PHP код е скрит от проходите, входовете към
този код (възлите, представляващи потребителски данни или изрази)
трябва все още да бъдат обходими. Затова вторият аргумент на
конструктора на AuxiliaryNode
е от съществено значение. Трябва да
предадете масив, съдържащ всички оригинални възли (като $userExpr
в
примера по-горе), които вашата closure използва. getIterator()
на
AuxiliaryNode
ще предостави тези възли, позволявайки на
компилационни проходи като Sandbox да ги анализират за потенциални
проблеми.
Добри практики
- Ясна цел: Уверете се, че вашият таг има ясна и необходима цел. Не създавайте тагове за задачи, които могат лесно да бъдат решени с помощта на филтри или функции.
- Правилно имплементирайте
getIterator()
: Винаги имплементирайтеgetIterator()
и предоставяйте референции (&
) към всички дъщерни възли (аргументи, съдържание), които са били парсвани от шаблона. Това е необходимо за компилационните проходи, сигурността (Sandbox) и потенциални бъдещи оптимизации. - Публични свойства за възли: Направете свойствата, съдържащи дъщерни възли, публични, за да могат компилационните проходи да ги модифицират при необходимост.
- Използвайте
PrintContext::format()
: Използвайте методаformat()
за генериране на PHP код. Той обработва кавички, правилно екранира placeholders и добавя коментари с номер на ред автоматично. - Временни променливи (
$__
): При генериране на PHP код по време на изпълнение, който се нуждае от временни променливи (напр. за съхраняване на междинни суми, броячи на цикли), използвайте конвенцията за префикс$__
, за да избегнете конфликти с потребителски променливи и вътрешни променливи на Latte$ʟ_
. - Влагане и уникални ID: Ако вашият таг може да бъде вложен или се
нуждае от състояние, специфично за инстанцията по време на изпълнение,
използвайте
$context->generateId()
в рамките на вашия методprint()
, за да създадете уникални суфикси за вашите временни променливи$__
. - Providers за външни данни: Използвайте providers (регистрирани чрез
Extension::getProviders()
) за достъп до данни или услуги по време на изпълнение ($this->global->…) вместо твърдо кодиране на стойности или разчитане на глобално състояние. Използвайте префикси на производителя за имената на providers. - Обмислете n:атрибути: Ако вашият сдвоен таг логически оперира
върху един HTML елемент, Latte вероятно предоставя автоматична поддръжка
на
n:атрибут
. Имайте това предвид за удобство на потребителя. Ако създавате таг, модифициращ атрибут, обмислете дали чистn:атрибут
е най-подходящата форма. - Тестване: Пишете тестове за вашите тагове, покриващи както парсването на различни синтактични входове, така и коректността на изхода на генерирания PHP код.
Като следвате тези насоки, можете да създавате мощни, здрави и поддържаеми персонализирани тагове, които се интегрират безпроблемно с шаблонния engine на Latte.
Изучаването на класовете на възлите, които са част от Latte, е най-добрият начин да научите всички подробности за процеса на парсване.