Nette Documentation Preview

syntax
Проходы компиляции
******************
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Проходы компиляции

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

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

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

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

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

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

Регистрация

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

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\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) — вызываемые объекты:

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

(new NodeTraverser)->traverse(
	$templateNode,

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

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

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

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).

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' visitor не нужен для этой задачи
	);

	echo "Найден тег {do} $count раз.\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. Traverser заменит
				// исходный ConstantFetchNode этим $newNode.
				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 vs. leave для замены:

  • Используйте leave, если создание нового узла зависит от результатов обработки дочерних узлов старого узла, или если вы просто хотите убедиться, что дочерние узлы были посещены перед заменой (обычная практика).
  • Используйте enter, если вы хотите заменить узел перед тем, как его дочерние узлы вообще будут посещены.

Удаление узлов

Вы можете полностью удалить узел из AST, вернув специальную константу NodeTraverser::RemoveNode из посетителя.

Пример: Удалим все комментарии шаблона ({* ... *}), которые представлены CommentNode в AST, генерируемом ядром Latte (хотя обычно они обрабатываются раньше, это служит примером).

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

function removeCommentNodes(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, // Искать только в главной секции для эффективности
		fn($node) => $node instanceof ParametersNode,
	);
}

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 = getStaticAttributeValue($node->getAttributeNode('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') {
				// Убедимся, что узел атрибутов существует
				$node->attributes ??= new Nodes\FragmentNode;

				// Проверим, существует ли уже атрибут '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]; // Простой список

	$traverser = new NodeTraverser;
	(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(
					"Функция {$node->name}() не разрешена.",
					$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 мощным и надежным способом, что способствует более безопасной, оптимизированной или функционально богатой обработке шаблонов.