Twig — отличный шаблонизатор и, в отличие от остальных, с которыми мне приходилось сталкиваться, со временем нравится мне все больше и больше. Достоинств у Twig много и одно из них — расширяемость.
Некоторое время мне тихонько портила жизнь небольшая проблема, на которую лень было тратить время. Недавно я все же заставил себя и, думаю, решение ее хорошо бы подошло для небольшой статейки о плагинах в Twig.
Сама проблема — в константах внутри шаблонов. Бывают такие задачи, когда в шаблоне необходимо зашиться на какие-нибудь идентификаторы. Цифрами расставлять их — не совсем хорошо, а если для них еще и существуют константы — грех не воспользоваться функцией
И что же у нас может получиться? Мы на волне рефакторинга убиваем или переименовываем константу, а о шаблоне забываем. И IDE забывает, даже хваленый PHPStorm. Успешно компилируем пер��д деплоем всю нашу гору шаблонов, раскидываем на сервера. Ничего не упало, просто работает все не очень, а на нашу голову сваливается огромная простыня одинаковых ворнингов. Плохо? Отвратительно!
Решение? Резолвить константы в процессе компиляции шаблона, на отсутствующие — ругаться.
Тем, кто не знаком с Twig или знаком не очень хорошо, расскажем (очень кратко) что каждый шаблон парсится плагинами (даже базовые возможности реализованы в шаблонизаторе с помощью плагинов), обрабатывается и компилируется в php-класс, у которого потом дергается метод
Шаблон разберется в относительно большое дерево объектов.
Оно дополнительно обрабатывается [сюда нам нужно вклиниться] и в итоге компилируется вот в такой файл (тоже слегка укороченный вариант):
Чтобы посмотреть на проблему своими глазами убираем константу из кода и на рендере ловим:
Именно вызов
Для расширений в шаблонизаторе предусмотрен класс
Нам нужно перед компиляцией просто пройтись по всем нодам, найти среди них функцию constant с обычным текстовым аргументом и поменять на его значение, либо ругнуться на то, что такой константы нет.
Вот таким наш node visitor и получается:
Вот так мы и заменили чуть ли не треть нашего дерева шаблона обычным значением.
Просто, интересно, полезно.
Надеюсь, кого-то это подтолкнет покопаться в Twig и попытаться расширить его чем-то кроме функций, да ��ильтров. Готов выслушать любую критику, а утром — даже ответить на вопросы. Дискас!
Некоторое время мне тихонько портила жизнь небольшая проблема, на которую лень было тратить время. Недавно я все же заставил себя и, думаю, решение ее хорошо бы подошло для небольшой статейки о плагинах в Twig.
Сама проблема — в константах внутри шаблонов. Бывают такие задачи, когда в шаблоне необходимо зашиться на какие-нибудь идентификаторы. Цифрами расставлять их — не совсем хорошо, а если для них еще и существуют константы — грех не воспользоваться функцией
constant. Но дело в том, что после компиляции из шаблона она все равно вычисляется в рантайме. И что же у нас может получиться? Мы на волне рефакторинга убиваем или переименовываем константу, а о шаблоне забываем. И IDE забывает, даже хваленый PHPStorm. Успешно компилируем пер��д деплоем всю нашу гору шаблонов, раскидываем на сервера. Ничего не упало, просто работает все не очень, а на нашу голову сваливается огромная простыня одинаковых ворнингов. Плохо? Отвратительно!
Решение? Резолвить константы в процессе компиляции шаблона, на отсутствующие — ругаться.
Тем, кто не знаком с Twig или знаком не очень хорошо, расскажем (очень кратко) что каждый шаблон парсится плагинами (даже базовые возможности реализованы в шаблонизаторе с помощью плагинов), обрабатывается и компилируется в php-класс, у которого потом дергается метод
display. Для примера возьмем такой код шаблона, как раз с нашей константой:{% if usertype == constant('Users::TYPE_TROLL') %} Давай, до свидания! {% else %} Привет! {% endif %}
Шаблон разберется в относительно большое дерево объектов.
Здесь немного укороченный, но все равно большой вывод print_r представления нашего шаблона
[body] => Twig_Node_Body Object ( [nodes:protected] => Array ( [0] => Twig_Node_If Object ( [nodes:protected] => Array ( [tests] => Twig_Node Object ( [nodes:protected] => Array ( [0] => Twig_Node_Expression_Binary_Equal Object ( [nodes:protected] => Array ( [left] => Twig_Node_Expression_Name Object ( [attributes:protected] => Array ( [name] => usertype ) ) [right] => Twig_Node_Expression_Function Object ( [nodes:protected] => Array ( [arguments] => Twig_Node Object ( [nodes:protected] => Array ( [0] => Twig_Node_Expression_Constant Object ( [attributes:protected] => Array ( [value] => Users::TYPE_TROLL ) ) ) ) ) [attributes:protected] => Array ( [name] => constant ) ) ) ) [1] => Twig_Node_Text Object ( [attributes:protected] => Array ( [data] => Давай, до свидания! ) ) ) ) [else] => Twig_Node_Text Object ( [attributes:protected] => Array ( [data] => Привет! ) ) ) ) ) )
Оно дополнительно обрабатывается [сюда нам нужно вклиниться] и в итоге компилируется вот в такой файл (тоже слегка укороченный вариант):
class __TwigTemplate_long_long_hash extends Twig_Template { protected function doDisplay(array $context, array $blocks = array()) { if (((isset($context["usertype"]) ? $context["usertype"] : null) == twig_constant("Users::TYPE_TROLL"))) { echo "Давай, до свидания!"; } else { echo "Привет!"; } } }
$context здесь — то, что попало в кучу переменных на вход этому шаблону. Надеюсь, все понятно и ничего объяснять не надо. Функция twig_constant практически не отличается от стандартной constant и резолвится в рантайме.Чтобы посмотреть на проблему своими глазами убираем константу из кода и на рендере ловим:
PHP Warning: constant(): Couldn't find constant Users::TYPE_TROLL in vendor/twig/twig/lib/Twig/Extension/Core.php on line 1387Именно вызов
twig_constant в компилированном варианте нам нужно заменить на значение константы.Для расширений в шаблонизаторе предусмотрен класс
Twig_Extension, от которого мы и наследуем наше расширение. Расширение может предоставлять шаблонизатору наборы функций, фильтров и прочей ерунды, какой только можно придумать, через специальные методы, которые вы можете сами найти в интерфейсе Twig_ExtensionInterface. Нас интересует метод getNodeVisitors, который возвращает массив объектов, через которых будут пропущены все элементы распарсенного дерева шаблона перед его компиляцией.class Template_Extensions_ConstEvaluator extends Twig_Extension { public function getNodeVisitors() { return [ new Template_Extensions_NodeVisitor_ConstEvaluator() ]; } public function getName() { return 'const_evaluator'; } }
Нам нужно перед компиляцией просто пройтись по всем нодам, найти среди них функцию constant с обычным текстовым аргументом и поменять на его значение, либо ругнуться на то, что такой константы нет.
Вот таким наш node visitor и получается:
class Template_Extensions_NodeVisitor_ConstEvaluator implements Twig_NodeVisitorInterface { public function enterNode(Twig_NodeInterface $node, Twig_Environment $env) { // ищем ноду-функцию с названием constant и 1 аргументом if ($node instanceof Twig_Node_Expression_Function && 'constant' === $node->getAttribute('name') && 1 === $node->count() ) { // получаем аргументы функции $args = $node->getNode('arguments'); if ($args instanceof Twig_Node && 1 === $args->count() ) { $constNode = $args->getNode(0); // 1 текстовый аргумент if ($constNode instanceof Twig_Node_Expression_Constant && null !== $value = $constNode->getAttribute('value') ) { if (null === $constantEvaluated = constant($value)) { // не можем найти константу - ругаемся throw new Twig_Error( sprintf( "Can't evaluate constant('%s')", $value ) ); } // все нашлось, возвращаем вместо функции ноду со значением константы // не введитесь в заблуждение названием класса :] return new Twig_Node_Expression_Constant($constantEvaluated, $node->getLine()); } } } // все ок, возвращаем то, что получили, в целости и сохранности return $node; } public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env) { return $node; } }
Вот так мы и заменили чуть ли не треть нашего дерева шаблона обычным значением.
На всякий случай покажу, что же получилось в компилированном варианте
class __TwigTemplate_long_long_hash extends Twig_Template { protected function doDisplay(array $context, array $blocks = array()) { if (((isset($context["usertype"]) ? $context["usertype"] : null) == 2)) { echo "Давай, до свидания!"; } else { echo "Привет!"; } } }
Просто, интересно, полезно.
Надеюсь, кого-то это подтолкнет покопаться в Twig и попытаться расширить его чем-то кроме функций, да ��ильтров. Готов выслушать любую критику, а утром — даже ответить на вопросы. Дискас!
