Twig — отличный шаблонизатор и, в отличие от остальных, с которыми мне приходилось сталкиваться, со временем нравится мне все больше и больше. Достоинств у Twig много и одно из них — расширяемость.
Некоторое время мне тихонько портила жизнь небольшая проблема, на которую лень было тратить время. Недавно я все же заставил себя и, думаю, решение ее хорошо бы подошло для небольшой статейки о плагинах в Twig.
Сама проблема — в константах внутри шаблонов. Бывают такие задачи, когда в шаблоне необходимо зашиться на какие-нибудь идентификаторы. Цифрами расставлять их — не совсем хорошо, а если для них еще и существуют константы — грех не воспользоваться функцией constant
. Но дело в том, что после компиляции из шаблона она все равно вычисляется в рантайме.
И что же у нас может получиться? Мы на волне рефакторинга убиваем или переименовываем константу, а о шаблоне забываем. И IDE забывает, даже хваленый PHPStorm. Успешно компилируем перед деплоем всю нашу гору шаблонов, раскидываем на сервера. Ничего не упало, просто работает все не очень, а на нашу голову сваливается огромная простыня одинаковых ворнингову. Плохо? Отвратительно!
Решение? Резолвить константы в процессе компиляции шаблона, что мы и попробуем в итоге сварганить.
Тем, кто не знаком с Twig или знаком не очень хорошо, расскажем (очень кратко) что каждый шаблон парсится плагинами (даже базовые возможности реализованы в шаблонизаторе с помощью плагинов), обрабатывается и компилируется в php-класс, у которого потом дергается метод display
. Для примера возьмем такой код шаблона, как раз с нашей константой:
{% if usertype == constant('Users::TYPE_TROLL') %}
Давай, до свидания!
{% else %}
Привет!
{% endif %}
Оно разберется в большое дерево, которое уже потом обрабатывается.
[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 и попытаться расширить его чем-то кроме функций, да фильтров.
Автор: return