Проходи компіляції
Проходи компіляції надають потужний механізм для аналізу та модифікації шаблонів Latte після їх розбору в абстрактне синтаксичне дерево (AST) і перед генерацією фінального PHP-коду. Це дозволяє здійснювати розширену маніпуляцію шаблонами, оптимізацію, перевірки безпеки (наприклад, Sandbox) та збір інформації про шаблони. Цей посібник проведе вас через створення власних проходів компіляції.
Що таке прохід компіляції?
Для розуміння ролі проходів компіляції зверніться до процесу компіляції Latte. Як ви можете бачити, проходи компіляції діють на ключовому етапі, дозволяючи глибоке втручання між початковим розбором та фінальним виведенням коду.
По суті, прохід компіляції — це просто PHP-об'єкт, що викликається
(наприклад, функція, статичний метод або метод екземпляра), який
приймає один аргумент: кореневий вузол AST шаблону, який завжди є
екземпляром Latte\Compiler\Nodes\TemplateNode
.
Основною метою проходу компіляції зазвичай є одна або обидві з наступних:
- Аналіз: Проходити AST та збирати інформацію про шаблон (наприклад, знайти всі визначені блоки, перевірити використання специфічних тегів, забезпечити дотримання певних обмежень безпеки).
- Модифікація: Змінити структуру AST або атрибути вузлів (наприклад, автоматично додати HTML-атрибути, оптимізувати певні комбінації тегів, замінити застарілі теги новими, реалізувати правила sandbox).
Реєстрація
Проходи компіляції реєструються за допомогою методу розширення getPasses()
. Цей метод повертає
асоціативний масив, де ключі є унікальними назвами проходів
(використовуються внутрішньо та для сортування), а значення — це
PHP-об'єкти, що викликаються, які реалізують логіку проходу.
Проходи, зареєстровані базовими розширеннями Latte та вашими власними
розширеннями, виконуються послідовно. Порядок може бути важливим,
особливо якщо один прохід залежить від результатів або модифікацій
іншого. Latte надає допоміжний механізм для контролю цього порядку, якщо
це необхідно; див. документацію до Extension::getPasses()
для деталей.
Приклад AST
Для кращого уявлення про AST, додаємо приклад. Це вихідний шаблон:
А це його представлення у вигляді AST:
Latte\Compiler\Nodes\TemplateNode( Latte\Compiler\Nodes\FragmentNode( - Latte\Essential\Nodes\ForeachNode( expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') ) value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') content: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode(' ') - Latte\Compiler\Nodes\Html\ElementNode('li')( content: Latte\Essential\Nodes\PrintNode( expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') name: Latte\Compiler\Nodes\Php\IdentifierNode('name') ) modifier: Latte\Compiler\Nodes\Php\ModifierNode( filters: - Latte\Compiler\Nodes\Php\FilterNode('upper') ) ) ) ) else: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode('no items found') ) ) ) )
Обхід AST за допомогою NodeTraverser
Ручне написання рекурсивних функцій для обходу складної структури AST є втомливим і схильним до помилок. Latte надає спеціальний інструмент для цієї мети: Latte\Compiler\NodeTraverser. Цей клас реалізує патерн проектування Visitor, завдяки якому обхід AST стає систематичним та легко керованим.
Базове використання включає створення екземпляра NodeTraverser
та
виклик його методу traverse()
, передаючи кореневий вузол AST та один
або два „visitor“ об'єкти, що викликаються:
Ви можете надати лише enter
visitor, лише leave
visitor, або обидва,
залежно від ваших потреб.
enter(Node $node)
: Ця функція виконується для кожного вузла
перед тим, як обхідник відвідає будь-яких дочірніх вузлів цього
вузла. Вона корисна для:
- Збору інформації під час обходу деревом вниз.
- Прийняття рішень перед обробкою дочірніх вузлів (наприклад, рішення про їх пропуск, див. Оптимізація обходу).
- Потенційних змін вузла перед відвідуванням дочірніх вузлів (менш поширене).
leave(Node $node)
: Ця функція виконується для кожного вузла
після того, як усі його дочірні вузли (та їхні цілі піддерева) були
повністю відвідані (як вхід, так і вихід). Це найпоширеніше місце для:
Обидва візитори enter
та leave
можуть опціонально повертати
значення для впливу на процес обходу. Повернення null
(або нічого)
продовжує обхід нормально, повернення екземпляра Node
замінює
поточний вузол, а повернення спеціальних констант, таких як
NodeTraverser::RemoveNode
або NodeTraverser::StopTraversal
, змінює потік, як
пояснено в наступних розділах.
Як працює обхід
NodeTraverser
внутрішньо використовує метод getIterator()
, який
повинен реалізувати кожен клас Node
(як обговорювалося в Створення власних тегів). Він
ітерує по дочірніх вузлах, отриманих за допомогою getIterator()
,
рекурсивно викликає traverse()
на них і забезпечує, що enter
та
leave
візитори викликаються в правильному порядку обходу в
глибину для кожного вузла в дереві, доступного через ітератори. Це
знову підкреслює, чому правильно реалізований getIterator()
у ваших
власних вузлах тегів є абсолютно необхідним для правильної роботи
проходів компіляції.
Давайте напишемо простий прохід, який підраховує, скільки разів у
шаблоні використано тег {do}
(представлений
Latte\Essential\Nodes\DoNode
).
У цьому прикладі нам знадобився лише visitor enter
для перевірки
типу кожного відвіданого вузла.
Далі ми дослідимо, як ці візитори фактично модифікують AST.
Модифікація AST
Однією з основних цілей проходів компіляції є модифікація
абстрактного синтаксичного дерева. Це дозволяє здійснювати потужні
перетворення, оптимізації або застосування правил безпосередньо до
структури шаблону перед генерацією PHP-коду. NodeTraverser
надає кілька
способів досягнення цього в рамках візиторів enter
та
leave
.
Важлива примітка: Модифікація AST вимагає обережності. Неправильні зміни — такі як видалення основних вузлів або заміна вузла несумісним типом — можуть призвести до помилок під час генерації коду або спричинити неочікувану поведінку під час виконання програми. Завжди ретельно тестуйте свої модифікаційні проходи.
Зміна властивостей вузлів
Найпростіший спосіб модифікувати дерево — це пряма зміна публічних властивостей вузлів, відвіданих під час обходу. Всі вузли зберігають свої розібрані аргументи, вміст або атрибути у публічних властивостях.
Приклад: Створимо прохід, який знаходить усі статичні текстові
вузли (TextNode
, що представляють звичайний HTML або текст поза тегами
Latte) і перетворює їхній вміст на великі літери безпосередньо в
AST.
У цьому прикладі visitor enter
перевіряє, чи є поточний $node
типу TextNode
. Якщо так, ми безпосередньо оновлюємо його публічну
властивість $content
за допомогою mb_strtoupper()
. Це безпосередньо
змінює вміст статичного тексту, збереженого в AST перед генерацією
PHP-коду. Оскільки ми модифікуємо об'єкт безпосередньо, нам не потрібно
нічого повертати з візитора.
Ефект: Якщо шаблон містив <p>Hello</p>{= $var }<span>World</span>
,
після цього проходу AST буде представляти щось на зразок:
<p>HELLO</p>{= $var }<span>WORLD</span>
. Це НЕ ВПЛИНЕ на вміст $var.
Заміна вузлів
Більш потужною технікою модифікації є повна заміна вузла іншим. Це
робиться поверненням нового екземпляра Node
з візитора
enter
або leave
. NodeTraverser
потім замінює початковий вузол
повернутим у структурі батьківського вузла.
Приклад: Створимо прохід, який знаходить усі використання
константи PHP_VERSION
(представлені ConstantFetchNode
) і замінює їх
безпосередньо рядковим літералом (StringNode
), що містить
фактичну версію PHP, виявлену під час компіляції. Це форма
оптимізації на етапі компіляції.
Тут visitor leave
ідентифікує специфічний ConstantFetchNode
для
PHP_VERSION
. Потім він створює абсолютно новий StringNode
, що
містить значення константи PHP_VERSION
на момент компіляції.
Повертаючи цей $newNode
, він повідомляє обхіднику замінити
початковий ConstantFetchNode
в AST.
Ефект: Якщо шаблон містив {= PHP_VERSION }
і компіляція виконується
на PHP 8.2.1, AST після цього проходу буде ефективно представляти
{= '8.2.1' }
.
Вибір enter
проти leave
для заміни:
- Використовуйте
leave
, якщо створення нового вузла залежить від результатів обробки дочірніх вузлів старого вузла, або якщо ви просто хочете переконатися, що дочірні вузли відвідані перед заміною (поширена практика). - Використовуйте
enter
, якщо ви хочете замінити вузол перед тим, як його дочірні вузли взагалі будуть відвідані.
Видалення вузлів
Ви можете повністю видалити вузол з AST, повернувши спеціальну
константу NodeTraverser::RemoveNode
з візитора.
Приклад: Видалимо всі коментарі шаблону ({* ... *}
), які
представлені CommentNode
в AST, згенерованому ядром Latte (хоча зазвичай
вони обробляються раніше, це служить прикладом).
Застереження: Використовуйте RemoveNode
обережно. Видалення
вузла, який містить основний вміст або впливає на структуру (наприклад,
видалення вузла вмісту циклу), може призвести до пошкоджених шаблонів
або недійсного згенерованого коду. Найбезпечніше це робити для вузлів,
які дійсно є необов'язковими або самодостатніми (як коментарі або теги
налагодження) або для порожніх структурних вузлів (наприклад, порожній
FragmentNode
може бути безпечно видалений у деяких контекстах
проходом для очищення).
Ці три методи — зміна властивостей, заміна вузлів та видалення вузлів — надають основні інструменти для маніпуляції AST у межах ваших проходів компіляції.
Оптимізація обходу
AST шаблонів може бути досить великим, потенційно містячи тисячі
вузлів. Обхід кожного окремого вузла може бути зайвим і вплинути на
швидкість компіляції, якщо ваш прохід цікавиться лише специфічними
частинами дерева. NodeTraverser
пропонує способи оптимізації
обходу:
Пропуск дочірніх вузлів
Якщо ви знаєте, що як тільки ви натрапите на певний тип вузла, жоден з
його нащадків не може містити вузли, які ви шукаєте, ви можете вказати
обхіднику пропустити відвідування його дочірніх вузлів. Це робиться
поверненням константи NodeTraverser::DontTraverseChildren
з візитора
enter
. Це дозволяє пропустити цілі гілки під час обходу,
потенційно заощаджуючи значний час, особливо в шаблонах зі складними
PHP-виразами всередині тегів.
Зупинка обходу
Якщо ваш прохід потребує знайти лише перше входження чогось
(специфічний тип вузла, виконання умови), ви можете повністю зупинити
весь процес обходу, як тільки його знайдете. Це досягається
поверненням константи NodeTraverser::StopTraversal
з візитора enter
або
leave
. Метод traverse()
припинить відвідування будь-яких
подальших вузлів. Це дуже ефективно, якщо вам потрібна лише перша
відповідність у потенційно дуже великому дереві.
Корисний помічник NodeHelpers
Хоча NodeTraverser
пропонує детальний контроль, Latte також надає
зручний допоміжний клас, Latte\Compiler\NodeHelpers, який
інкапсулює NodeTraverser
для кількох поширених завдань пошуку та
аналізу, часто вимагаючи менше підготовчого коду.
find(Node $startNode, callable $filter): array
Цей статичний метод знаходить усі вузли в піддереві, що
починається з $startNode
(включно), які задовольняють callback
$filter
. Повертає масив відповідних вузлів.
Приклад: Знайти всі вузли змінних (VariableNode
) у всьому
шаблоні.
findFirst(Node $startNode, callable $filter): ?Node
Подібно до find
, але зупиняє обхід негайно після знаходження
першого вузла, який задовольняє callback $filter
. Повертає
знайдений об'єкт Node
або null
, якщо не знайдено жодного
відповідного вузла. Це, по суті, зручна обгортка навколо
NodeTraverser::StopTraversal
.
Приклад: Знайти вузол {parameters}
(те саме, що й ручний приклад
раніше, але коротше).
toValue(ExpressionNode $node, bool $constants = false): mixed
Цей статичний метод намагається обчислити ExpressionNode
на етапі
компіляції і повернути його відповідне PHP-значення. Він надійно
працює лише для простих літеральних вузлів (StringNode
,
IntegerNode
, FloatNode
, BooleanNode
, NullNode
) та екземплярів
ArrayNode
, що містять лише такі обчислювані елементи.
Якщо $constants
встановлено на true
, він також намагатиметься
розв'язати ConstantFetchNode
та ClassConstantFetchNode
, перевіряючи
defined()
та використовуючи constant()
.
Якщо вузол містить змінні, виклики функцій або інші динамічні
елементи, він не може бути обчислений на етапі компіляції, і метод
викине InvalidArgumentException
.
Приклад використання: Отримання статичного значення аргументу тегу під час компіляції для прийняття рішень на етапі компіляції.
toText(?Node $node): ?string
Цей статичний метод корисний для вилучення простого текстового вмісту з простих вузлів. Він працює переважно з:
TextNode
: Повертає його$content
.FragmentNode
: З'єднує результатtoText()
для всіх його дочірніх вузлів. Якщо якийсь дочірній вузол не може бути перетворений на текст (наприклад, міститьPrintNode
), повертаєnull
.NopNode
: Повертає порожній рядок.- Інші типи вузлів: Повертає
null
.
Приклад використання: Отримання статичного текстового вмісту значення HTML-атрибуту або простого HTML-елемента для аналізу під час компіляційного проходу.
NodeHelpers
може спростити ваші компіляційні проходи, надаючи
готові рішення для поширених завдань обходу та аналізу AST.
Практичні приклади
Застосуємо концепції обходу та модифікації AST для вирішення деяких практичних проблем. Ці приклади демонструють поширені патерни, що використовуються в компіляційних проходах.
Автоматичне додавання loading="lazy"
до
<img>
Сучасні браузери підтримують нативне ліниве завантаження для
зображень за допомогою атрибута loading="lazy"
. Створимо прохід, який
автоматично додає цей атрибут до всіх тегів <img>
, які ще не
мають атрибута loading
.
Пояснення:
- Visitor
enter
шукає вузлиHtml\ElementNode
з назвоюimg
. - Він ітерує по існуючих атрибутах (
$node->attributes->children
) і перевіряє, чи атрибутloading
вже присутній. - Якщо не знайдено, створює новий
Html\AttributeNode
, що представляєloading="lazy"
.
Перевірка викликів функцій
Компіляційні проходи є основою Latte Sandbox. Хоча справжній Sandbox є складним, ми можемо продемонструвати базовий принцип перевірки заборонених викликів функцій.
Мета: Запобігти використанню потенційно небезпечної функції
shell_exec
у межах виразів шаблону.
Пояснення:
- Ми визначаємо список заборонених назв функцій.
- Visitor
enter
перевіряєFunctionCallNode
. - Якщо назва функції (
$node->name
) є статичнимNameNode
, ми перевіряємо її рядкове представлення в нижньому регістрі проти нашого забороненого списку. - Якщо знайдено заборонену функцію, ми викидаємо
Latte\SecurityViolationException
, яка чітко вказує на порушення правила безпеки та зупиняє компіляцію.
Ці приклади показують, як компіляційні проходи з використанням
NodeTraverser
можуть бути використані для аналізу, автоматичних
модифікацій та застосування обмежень безпеки шляхом взаємодії
безпосередньо зі структурою AST шаблону.
Найкращі практики
При написанні компіляційних проходів пам'ятайте про ці рекомендації для створення надійних, підтримуваних та ефективних розширень:
- Порядок важливий: Будьте свідомі порядку, в якому виконуються
проходи. Якщо ваш прохід залежить від структури AST, створеної іншим
проходом (наприклад, базові проходи Latte або інший власний прохід), або
якщо інші проходи можуть залежати від ваших модифікацій,
використовуйте механізм сортування, наданий
Extension::getPasses()
для визначення залежностей (before
/after
). Див. документацію доExtension::getPasses()
для деталей. - Одна відповідальність: Намагайтеся створювати проходи, які виконують одне добре визначене завдання. Для складних перетворень розгляньте можливість розділення логіки на кілька проходів — можливо, один для аналізу та інший для модифікації на основі результатів аналізу. Це покращує зрозумілість та тестованість.
- Продуктивність: Пам'ятайте, що компіляційні проходи збільшують
час компіляції шаблону (хоча це зазвичай відбувається лише один раз,
доки шаблон не зміниться). Уникайте обчислювально складних операцій у
ваших проходах, якщо це можливо. Використовуйте оптимізації обходу,
такі як
NodeTraverser::DontTraverseChildren
таNodeTraverser::StopTraversal
, коли ви знаєте, що вам не потрібно відвідувати певні частини AST. - Використовуйте
NodeHelpers
: Для поширених завдань, таких як пошук специфічних вузлів або статичне обчислення простих виразів, перевірте, чиLatte\Compiler\NodeHelpers
не пропонує відповідний метод перед написанням власної логікиNodeTraverser
. Це може заощадити час та зменшити кількість підготовчого коду. - Обробка помилок: Якщо ваш прохід виявляє помилку або недійсний
стан в AST шаблону, викиньте
Latte\CompileException
(абоLatte\SecurityViolationException
для проблем безпеки) з чітким повідомленням та відповідним об'єктомPosition
(зазвичай$node->position
). Це надає корисний зворотний зв'язок розробнику шаблону. - Ідемпотентність (якщо можливо): В ідеалі, виконання вашого проходу кілька разів на тому самому AST має давати той самий результат, що й одноразове виконання. Це не завжди можливо, але спрощує налагодження та роздуми про взаємодію проходів, якщо цього досягнуто. Наприклад, переконайтеся, що ваш модифікаційний прохід перевіряє, чи модифікація вже була застосована, перш ніж застосовувати її знову.
Дотримуючись цих практик, ви можете ефективно використовувати компіляційні проходи для розширення можливостей Latte потужним та надійним способом, що сприяє безпечнішій, оптимізованішій або функціонально багатшій обробці шаблонів.