Створення власних тегів
Ця сторінка надає вичерпний посібник зі створення власних тегів у 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 потужним шаблонізатором, а не просто мовою
розмітки.
У програмуванні такі одиниці, що виконують дії, часто називають
“statements” (операторами). Тому вузли, що представляють ці функціональні
теги Latte, зазвичай успадковуються від Latte\Compiler\Nodes\StatementNode
. Це
відрізняє їх від чисто структурних вузлів (як HTML-елементи) або вузлів,
що представляють значення (як вирази).
Ключові компоненти
Розглянемо основні компоненти, необхідні для створення власного тегу:
Функція для парсингу тегу
- Ця PHP callable функція парсить синтаксис тегу Latte (
{...}
) у вихідному шаблоні. - Вона отримує інформацію про тег (наприклад, його назву, позицію та чи є він n:атрибутом) через об'єкт Latte\Compiler\Tag.
- Її основним інструментом для парсингу аргументів та виразів
усередині роздільників тегу є об'єкт Latte\Compiler\TagParser, доступний
через
$tag->parser
(це інший парсер, ніж той, що парсить весь шаблон). - Для парних тегів вона використовує
yield
для сигналізації Latte, щоб парсити внутрішній вміст між початковим та кінцевим тегом. - Кінцевою метою функції парсингу є створення та повернення екземпляра класу вузла, який додається до AST.
- Зазвичай (хоча це не обов'язково) функцію парсингу реалізують як
статичний метод (часто називається
create
) безпосередньо у відповідному класі вузла. Це зберігає логіку парсингу та представлення вузла акуратно в одному пакеті, дозволяє доступ до приватних/захищених елементів класу, якщо потрібно, та покращує організацію.
Клас вузла
- Представляє логічну функцію вашого тегу в 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;'
, є маскою, в яку підставляються
наступні параметри. Заповнювач %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;'
використовується новий заповнювач
%node
, який вказує методу format()
, щоб взяти перший наступний
аргумент (яким є наш $formatNode
), викликати його метод print()
(який поверне його PHP-кодове представлення) та вставити результат на
позицію заповнювача.
Реалізація 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}
, вміст якого візуалізується
лише тоді, коли активний специфічний прапорець “режиму розробки”.
Представлення провайдерів
Іноді ваші теги потребують доступу до даних або сервісів, які не передаються безпосередньо як параметри шаблону. Наприклад, визначення, чи знаходиться програма в режимі розробки, доступ до об'єкта користувача або отримання конфігураційних значень. Latte надає механізм, що називається провайдерами (Providers) для цієї мети.
Провайдери реєструються у вашому розширенні за допомогою
методу getProviders()
. Цей метод повертає асоціативний масив, де
ключі — це назви, під якими провайдери будуть доступні в коді шаблону
під час виконання, а значення — це фактичні дані або об'єкти.
Усередині PHP-коду, згенерованого методом print()
вашого тегу, ви
можете отримати доступ до цих провайдерів через спеціальну
властивість об'єкта $this->global
. Оскільки ця властивість спільна
для всіх розширень, хорошою практикою є префіксування назв ваших
провайдерів для запобігання потенційних колізій імен з ключовими
провайдерами Latte або провайдерами з інших розширень третіх сторін.
Загальною конвенцією є використання короткого, унікального префікса,
пов'язаного з вашим виробником або назвою розширення. Для нашого
прикладу ми використаємо префікс 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-код, який під час
виконання перевіряє провайдера appDevMode
і виконує код для
внутрішнього вмісту лише тоді, коли прапорець встановлено в true.
public function print(PrintContext $context): string
{
// Генерує PHP-оператор 'if', який під час виконання перевіряє провайдера
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Якщо в режимі розробки, виводить внутрішній вміст
%node
}
XX,
$this->position, // Для %line коментаря
$this->content, // Вузол, що містить AST внутрішнього вмісту
);
}
Це просто. Ми використовуємо PrintContext::format()
для створення
стандартного PHP-оператора if
. Усередині if
ми розміщуємо
заповнювач %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.
Реєстрація та використання
Зареєструйте тег та провайдера у вашому розширенні:
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, // Реєстрація провайдера
];
}
}
// Під час реєстрації розширення:
$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
Тепер, коли ми розуміємо прості теги, парсинг аргументів, парні теги,
провайдерів та 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
на основі провайдера 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, // Перший заповнювач %node
$this->elseContent ?? new NopNode, // Другий заповнювач %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()
. Вони або безпосередньо
виводили вміст, або виконували просту умовну перевірку на основі
глобального провайдера. Однак багато тегів потребують керування
якоюсь формою стану під час візуалізації або включають оцінку
виразів користувача, які повинні бути виконані лише один раз для
продуктивності або коректності. Далі ми повинні розглянути, що
відбувається, коли наші власні теги вкладені.
Проілюструємо ці концепції, створивши тег {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()
викликається з новим заповнювачем
%raw
, який вставляє сирий рядок, наданий як відповідний
аргумент. Тут він вставляє унікальну назву змінної, збережену в
$countVar
(наприклад, $__count_1
). А що щодо %0.raw
та
%2.raw
? Це демонструє позиційні заповнювачі. Замість простого
%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()
дозволяє створювати теги, які є
контекстно-усвідомленими та забезпечують правильне використання в
межах структури вашого шаблону, що призводить до більш надійних та
зрозумілих шаблонів.
Заповнювачі PrintContext::format()
Ми часто використовували PrintContext::format()
для генерації PHP-коду в
методах print()
наших вузлів. Він приймає рядок маски та наступні
аргументи, які замінюють заповнювачі в масці. Ось резюме доступних
заповнювачів:
%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-коду. Він обробляє лапки, правильно екранує заповнювачі та автоматично додає коментарі з номером рядка. - Тимчасові змінні (
$__
): При генерації PHP-коду під час виконання, який потребує тимчасових змінних (наприклад, для зберігання проміжних підсумків, лічильників циклів), використовуйте конвенцію префікса$__
для уникнення колізій з користувацькими змінними та внутрішніми змінними Latte$ʟ_
. - Вкладеність та унікальні ID: Якщо ваш тег може бути вкладеним або
потребує стану, специфічного для екземпляра під час виконання,
використовуйте
$context->generateId()
у вашому методіprint()
для створення унікальних суфіксів для ваших тимчасових змінних$__
. - Провайдери для зовнішніх даних: Використовуйте провайдерів
(зареєстрованих через
Extension::getProviders()
) для доступу до даних або сервісів під час виконання ($this->global->…) замість жорсткого кодування значень або покладання на глобальний стан. Використовуйте префікси виробника для назв провайдерів. - Розгляньте n:атрибути: Якщо ваш парний тег логічно оперує на
одному HTML-елементі, Latte, ймовірно, надає автоматичну підтримку
n:атрибута
. Майте це на увазі для зручності користувача. Якщо ви створюєте тег, що модифікує атрибут, розгляньте, чи є чистийn:атрибут
найвідповіднішою формою. - Тестування: Пишіть тести для ваших тегів, охоплюючи як парсинг різних синтаксичних входів, так і коректність виводу згенерованого PHP-коду.
Дотримуючись цих вказівок, ви можете створювати потужні, надійні та підтримувані власні теги, які бездоганно інтегруються з шаблонізатором Latte.
Вивчення класів вузлів, що входять до складу Latte, є найкращим способом дізнатися всі деталі процесу парсингу.