Компилационни проходи
Компилационните проходи предоставят мощен механизъм за анализ и модификация на Latte шаблони след тяхното парсване в абстрактно синтактично дърво (AST) и преди генерирането на финалния PHP код. Това позволява напреднала манипулация на шаблони, оптимизации, проверки за сигурност (като Sandbox) и събиране на информация за шаблоните. Това ръководство ще ви преведе през създаването на собствени компилационни проходи.
Какво е компилационен проход?
За да разберете ролята на компилационните проходи, погледнете процеса на компилация на Latte. Както можете да видите, компилационните проходи оперират в ключова фаза, позволявайки дълбока намеса между първоначалното парсване и финалния изход на кода.
По същество, компилационният проход е просто PHP callable обект (като
функция, статичен метод или метод на инстанция), който приема един
аргумент: коренния възел на AST на шаблона, който винаги е инстанция на
Latte\Compiler\Nodes\TemplateNode
.
Основната цел на компилационния проход обикновено е една или и двете от следните:
- Анализ: Обхождане на AST и събиране на информация за шаблона (напр. намиране на всички дефинирани блокове, проверка на използването на специфични тагове, осигуряване на спазването на определени ограничения за сигурност).
- Модификация: Промяна на структурата на AST или атрибутите на възлите (напр. автоматично добавяне на HTML атрибути, оптимизиране на определени комбинации от тагове, замяна на остарели тагове с нови, прилагане на правила на sandbox).
Регистрация
Компилационните проходи се регистрират с помощта на метода getPasses()
на разширението. Този метод
връща асоциативен масив, където ключовете са уникални имена на
проходите (използвани вътрешно и за сортиране), а стойностите са PHP callable
обекти, имплементиращи логиката на прохода.
Проходите, регистрирани от основните разширения на 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“ callable обекта:
Можете да предоставите само 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 по мощен и надежден начин, допринасяйки за по-безопасна, по-оптимизирана или функционално по-богата обработка на шаблони.