Создание проходов компилятора

Проходы компилятора дают мощный механизм для анализа и изменения шаблонов Latte после того, как они разобраны в абстрактное синтаксическое дерево (AST), и до того, как сгенерирован итоговый PHP-код. Это открывает путь к продвинутой работе с шаблонами, оптимизациям, проверкам безопасности (как песочница) и сбору сведений о шаблонах. Это руководство проведёт вас через создание собственных проходов компилятора.

Что такое проход компилятора?

Чтобы понять роль проходов компилятора, посмотрите на процесс компиляции в Latte. Как видите, проходы компилятора работают на ключевом этапе, позволяя глубоко вмешаться между первоначальным разбором и итоговым выводом кода.

По сути проход компилятора – это просто PHP-callable (функция, статический метод или метод экземпляра), принимающий один аргумент: корневой узел AST шаблона, которым всегда является экземпляр Latte\Compiler\Nodes\TemplateNode.

Основная цель прохода компилятора обычно одна из двух или обе сразу:

  • Анализ: обойти AST и собрать сведения о шаблоне (например, найти все определённые блоки, проверить использование конкретных тегов, убедиться в соблюдении определённых требований безопасности).
  • Изменение: поменять структуру AST или свойства узлов (например, автоматически добавить HTML-атрибуты, оптимизировать определённые сочетания тегов, заменить устаревшие теги новыми, реализовать правила песочницы).

Регистрация

Проходы компилятора регистрируются через метод getPasses() расширения. Этот метод возвращает ассоциативный массив, где ключи – уникальные имена проходов (используются внутренне и для упорядочивания), а значения – PHP-callable, реализующие логику прохода.

use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;

class MyExtension extends Extension
{
	public function getPasses(): array
	{
		return [
			'modificationPass' => $this->modifyTemplateAst(...),
			// ... другие проходы ...
		];
	}

	public function modifyTemplateAst(TemplateNode $templateNode): void
	{
		// Реализация...
	}
}

Проходы, зарегистрированные базовыми расширениями Latte и вашими собственными расширениями, выполняются последовательно. Порядок может быть важен, особенно если один проход опирается на результаты или изменения другого. Latte предоставляет вспомогательный механизм управления этим порядком, подробности см. в документации по Extension::getPasses().

Пример AST

Чтобы лучше представить себе AST, приведём образец. Вот исходный шаблон:

{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}

А вот его представление в виде 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\Compiler\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. Этот класс реализует шаблон проектирования “Посетитель”, благодаря чему обход AST становится систематичным и управляемым.

Базовое использование сводится к созданию экземпляра NodeTraverser и вызову его метода traverse(), куда передаются корневой узел AST и один или два callable-“посетителя”:

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;

(new NodeTraverser)->traverse(
	$templateNode,

	// посетитель 'enter': вызывается при входе в узел (до его потомков)
	enter: function (Node $node) {
		echo "Entering node of type: " . $node::class . "\n";
		// Здесь узел можно исследовать
		if ($node instanceof Nodes\TextNode) {
			// echo "Found text: " . $node->content . "\n";
		}
	},

	// посетитель 'leave': вызывается при выходе из узла (после его потомков)
	leave: function (Node $node) {
		echo "Leaving node of type: " . $node::class . "\n";
		// Здесь можно что-то сделать после обработки потомков
	},
);

Вы можете передать только посетителя enter, только leave или обоих сразу, в зависимости от ваших нужд.

enter(Node $node): эта функция выполняется для каждого узла до того, как обходчик посетит его потомков. Она полезна для:

  • сбора сведений по мере спуска по дереву,
  • принятия решений до обработки потомков (например, решения их пропустить, см. Оптимизация обхода),
  • возможного изменения узла до посещения потомков (встречается реже).

leave(Node $node): эта функция выполняется для каждого узла после того, как все его потомки (и все их поддеревья) полностью посещены (и вход, и выход). Это самое частое место для:

  • замены узла после обработки его потомков,
  • удаления узлов из AST,
  • обобщения сведений, собранных со всего поддерева.

И enter, и leave могут по желанию вернуть значение, влияющее на ход обхода. Возврат null (или ничего) продолжает обход как обычно, возврат экземпляра Node заменяет текущий узел, а возврат специальных констант вроде NodeTraverser::RemoveNode или NodeTraverser::StopTraversal меняет ход обхода, как объясняется в следующих разделах.

Как работает обход

Внутри NodeTraverser использует метод getIterator(), который обязан реализовывать каждый класс Node (как обсуждается в Создании пользовательских тегов). Он перебирает потомков, отдаваемых getIterator(), рекурсивно вызывает для них traverse() и следит, чтобы посетители enter и leave вызывались в правильном порядке обхода в глубину для каждого узла дерева, доступного через итераторы. Это ещё раз подчёркивает, почему правильно реализованный getIterator() в узлах ваших пользовательских тегов абсолютно необходим для корректной работы проходов компилятора.

Напишем простой проход, который считает, сколько раз в шаблоне использован тег {do} (представленный классом Latte\Essential\Nodes\DoNode).

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\DoNode;

function countDoTags(TemplateNode $templateNode): void
{
	$count = 0;
	(new NodeTraverser)->traverse(
		$templateNode,
		enter: function (Node $node) use (&$count): void {
			if ($node instanceof DoNode) {
				$count++;
			}
		},
		// посетитель 'leave' для этой задачи не нужен
	);

	echo "Found {do} tag $count times.\n";
}

$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);

В этом примере нам понадобился только посетитель enter, чтобы проверять тип каждого встреченного узла.

Дальше мы разберём, как с помощью этих посетителей действительно изменять AST.

Изменение AST

Одно из главных назначений проходов компилятора – изменение абстрактного синтаксического дерева. Это открывает путь к мощным преобразованиям, оптимизациям или применению правил прямо к структуре шаблона до генерации PHP-кода. NodeTraverser предлагает несколько способов сделать это внутри посетителей enter и leave.

Важное замечание: изменение AST требует аккуратности. Неверные правки, например удаление важных узлов или замена узла несовместимым типом, могут привести к ошибкам при генерации кода или к неожиданному поведению во время выполнения. Всегда тщательно тестируйте свои изменяющие проходы.

Изменение свойств узла

Самый простой способ изменить дерево – прямо поменять публичные свойства узлов, встреченных при обходе. Все узлы хранят свои разобранные аргументы, содержимое или атрибуты в публичных свойствах.

Пример: создадим проход, который находит все узлы статического текста (TextNode, представляющие простой HTML или текст вне тегов Latte) и переводит их содержимое в верхний регистр прямо в AST.

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\TextNode;

function uppercaseStaticText(TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// Можно использовать 'enter', ведь у TextNode нет потомков для предварительной обработки
		enter: function (Node $node) {
			// Является ли этот узел блоком статического текста?
			if ($node instanceof TextNode) {
				// Да! Прямо меняем его публичное свойство 'content'.
				$node->content = mb_strtoupper(html_entity_decode($node->content));
			}
			// Возвращать ничего не нужно; изменение происходит на месте.
		},
	);
}

В этом примере посетитель 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, определённую во время компиляции. Это одна из форм оптимизации на этапе компиляции.

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;

function inlinePhpVersion(TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// Для замен часто используют 'leave', чтобы потомки (если они есть)
		// были обработаны первыми, хотя здесь подошёл бы и 'enter'.
		leave: function (Node $node) {
			// Это узел обращения к константе и имя константы 'PHP_VERSION'?
			if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
				// Создаём новый StringNode с текущей версией PHP
				$newNode = new StringNode(PHP_VERSION);

				// Необязательно, но полезно: копируем сведения о позиции
				$newNode->position = $node->position;

				// Возвращаем новый StringNode. Обходчик заменит им
				// исходный ConstantFetchNode.
				return $newNode;
			}
			// Если мы не возвращаем Node, исходный $node остаётся на месте.
		},
	);
}

Здесь посетитель 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.

Пример: уберём из вывода все HTML-комментарии (<!-- ... -->). Комментарии Latte {* ... *} таким способом не достать, потому что парсер отбрасывает их содержимое и заменяет их пустым NopNode, а не отдельным узлом комментария, но HTML-комментарии сохраняются как узлы Html\CommentNode, поэтому здесь мы можем их вырезать.

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Html\CommentNode;

function removeHtmlComments(TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// Здесь подходит 'enter', ведь для удаления комментария сведения о потомках не нужны
		enter: function (Node $node) {
			if ($node instanceof CommentNode) {
				// Сигнализируем обходчику удалить этот узел из AST
				return NodeTraverser::RemoveNode;
			}
		},
	);
}

Осторожно: пользуйтесь 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) во всём шаблоне.

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
use Latte\Compiler\Nodes\TemplateNode;

function findAllVariables(TemplateNode $templateNode): array
{
	return NodeHelpers::find(
		$templateNode,
		fn($node) => $node instanceof VariableNode,
	);
}

findFirst (Node $startNode, callable $filter)?Node

Похож на find, но останавливает обход сразу после нахождения первого узла, удовлетворяющего callback-функции $filter. Он возвращает найденный объект Node или null, если подходящий узел не найден. По сути это удобная обёртка над NodeTraverser::StopTraversal.

Пример: найти узел {parameters}.

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;

function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
	return NodeHelpers::findFirst(
		$templateNode->head, // Ради эффективности ищем только в секции head
		fn($node) => $node instanceof ParametersNode,
	);
}

clone (Latte\Compiler\Node $node)Node

Этот статический метод создаёт глубокую копию узла и всего его поддерева. Он полезен, когда вам нужно продублировать ветвь AST, например вставить изменённую копию узла, оставив оригинал нетронутым.

use Latte\Compiler\NodeHelpers;

$copy = NodeHelpers::clone($node);

toValue (ExpressionNode $node, bool $constants = false)mixed

Этот статический метод пытается вычислить ExpressionNode во время компиляции и вернуть соответствующее значение PHP. Он надёжно работает только для простых литеральных узлов (StringNode, IntegerNode, FloatNode, BooleanNode, NullNode) и для экземпляров ArrayNode, содержащих только такие вычислимые элементы.

Если $constants установлено в true, он также попытается разрешить ConstantFetchNode и ClassConstantFetchNode, проверяя defined() и используя constant().

Если узел содержит переменные, вызовы функций или другие динамические элементы, вычислить его во время компиляции нельзя, и метод выбросит InvalidArgumentException.

Сценарий использования: получение статического значения аргумента тега во время компиляции, чтобы принять решение на этапе компиляции.

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\ExpressionNode;

function getStaticStringArgument(ExpressionNode $argumentNode): ?string
{
	try {
		$value = NodeHelpers::toValue($argumentNode);
		return is_string($value) ? $value : null;
	} catch (\InvalidArgumentException $e) {
		// Аргумент не был статической строковой константой
		return null;
	}
}

toText (?Node $node): ?string

Этот статический метод полезен для извлечения простого текстового содержимого из несложных узлов. Он работает прежде всего с:

  • TextNode: возвращает его $content.
  • FragmentNode: соединяет результат toText() для всех своих потомков. Если какой-то потомок не преобразуется в текст (например, содержит PrintNode), возвращает null.
  • NopNode: возвращает пустую строку.
  • Другие типы узлов: возвращает null.

Сценарий использования: получение статического текстового содержимого значения HTML-атрибута или простого HTML-элемента для анализа в проходе компилятора.

use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;

function getStaticAttributeValue(AttributeNode $attr): ?string
{
	// $attr->value обычно является AreaNode (например, FragmentNode или TextNode)
	return NodeHelpers::toText($attr->value);
}

// Пример использования в проходе:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
//     $nameAttrValue = $node->getAttribute('name');
//     if ($nameAttrValue === 'description') { ... }
// }

NodeHelpers может упростить ваши проходы компилятора, предлагая готовые решения для распространённых задач обхода и анализа AST.

Практические примеры

Применим понятия обхода и изменения AST к решению нескольких практических задач. Эти примеры показывают распространённые приёмы, используемые в проходах компилятора.

Автоматическое добавление loading="lazy" к <img>

Современные браузеры поддерживают нативную отложенную загрузку изображений через атрибут loading="lazy". Создадим проход, который автоматически добавляет этот атрибут ко всем тегам <img>, у которых атрибута loading ещё нет.

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Html;

function addLazyLoading(Nodes\TemplateNode $templateNode): void
{
	(new NodeTraverser)->traverse(
		$templateNode,
		// Можно использовать 'enter', ведь мы меняем узел напрямую
		// и это решение не зависит от потомков.
		enter: function (Node $node) {
			// Это HTML-элемент с именем 'img'?
			if ($node instanceof Html\ElementNode && $node->name === 'img') {
				// Проверяем, есть ли уже атрибут 'loading' (без учёта регистра)
				foreach ($node->attributes->children as $attrNode) {
					if ($attrNode instanceof Html\AttributeNode
						&& $attrNode->name instanceof Nodes\TextNode // Статическое имя атрибута
						&& strtolower($attrNode->name->content) === 'loading'
					) {
						return; // Уже есть, ничего не делаем
					}
				}

				// Добавляем пробел впереди, если атрибуты не пусты
				if ($node->attributes->children) {
					$node->attributes->children[] = new Nodes\TextNode(' ');
				}

				// Создаём новый узел атрибута: loading="lazy"
				$node->attributes->children[] = new Html\AttributeNode(
					name: new Nodes\TextNode('loading'),
					value: new Nodes\TextNode('lazy'),
					quote: '"',
				);
				// Изменение сделано на месте, возвращать ничего не нужно.
			}
		},
	);
}

Пояснение:

  • Посетитель enter ищет узлы Html\ElementNode с именем img.
  • Он перебирает существующие атрибуты ($node->attributes->children), чтобы проверить, нет ли уже атрибута loading.
  • Если не нашёл, создаёт новый Html\AttributeNode, представляющий loading="lazy", и добавляет его (при необходимости с предшествующим пробелом).

Проверка вызовов функций

Проходы компилятора лежат в основе песочницы Latte. Настоящая песочница устроена сложнее, но мы можем показать базовый принцип проверки запрещённых вызовов функций.

Цель: запретить использование потенциально опасной функции shell_exec в выражениях шаблона.

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Php;
use Latte\SecurityViolationException;

function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void
{
	$forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Простой список

	(new NodeTraverser)->traverse(
		$templateNode,
		enter: function (Node $node) use ($forbiddenFunctions) {
			// Это узел прямого вызова функции?
			if ($node instanceof Php\Expression\FunctionCallNode
				&& $node->name instanceof Php\NameNode
				&& isset($forbiddenFunctions[strtolower((string) $node->name)])
			) {
				throw new SecurityViolationException(
					"Function {$node->name}() is not allowed.",
					$node->position,
				);
			}
		},
	);
}

Пояснение:

  • Мы задаём список запрещённых имён функций.
  • Посетитель 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 мощно и надёжно, делая обработку шаблонов безопаснее, оптимальнее и богаче возможностями.

версия: 3.x