Простой плагин для Twig или разворачиваем константы

от автора

Twig — отличный шаблонизатор и, в отличие от остальных, с которыми мне приходилось сталкиваться, со временем нравится мне все больше и больше. Достоинств у Twig много и одно из них — расширяемость.

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

Сама проблема — в константах внутри шаблонов. Бывают такие задачи, когда в шаблоне необходимо зашиться на какие-нибудь идентификаторы. Цифрами расставлять их — не совсем хорошо, а если для них еще и существуют константы — грех не воспользоваться функцией constant. Но дело в том, что после компиляции из шаблона она все равно вычисляется в рантайме. Получается что мы в процессе рефакторинга убиваем или переименовываем константу, а о шаблоне забываем. Успешно компилируем перед деплоем всю нашу тонну шаблонов, раскидываем на сервера и в итоге получаем огромную простыню ворнингов на нашу голову. Плохо? Отвратительно!

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

Тем, кто не знаком с 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 и резолвится в рантайме. Будем менять его на само значение константы.

Для расширений в шаблонизаторе предусмотрен класс 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;     }  }

Просто, интересно, полезно.

Надеюсь, кого-то это подтолкнет покопаться в Twig и попытаться расширить его чем-то кроме функций, да фильтров.

ссылка на оригинал статьи http://habrahabr.ru/post/200614/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *