Пользователь
0,0
рейтинг
2 ноября 2013 в 05:32

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

PHP*
Twig — отличный шаблонизатор и, в отличие от остальных, с которыми мне приходилось сталкиваться, со временем нравится мне все больше и больше. Достоинств у 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 и попытаться расширить его чем-то кроме функций, да фильтров. Готов выслушать любую критику, а утром — даже ответить на вопросы. Дискас!
Калашников Игорь @return
карма
71,2
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (37)

  • +1
    А если используется встроенный в Twig кэш шаблонов, который сохраняет компиляцию шаблона в файл и далее просто «дёргает» этот файл. Используя ваш метод придётся заново генерировать весь кэш шаблонов, которые используют константу.
    • 0
      ЕМНИП Twig сам генерирует новый кэш при изменении кода шаблона
      • 0
        так изменится только константа, шаблон останется прежним
        • 0
          Как я понял автор захотел заменить вызов constant('Users::TYPE_TROLL') на просто Users::TYPE_TROLL, что является изменением кода шаблона.
      • +1
        при деплое кэш обычно сбрасывают.
    • 0
      Я файловый кеш и имею в виду. Для того и сделано чтобы при генерировании кеша он ругнулся если константы нет, а если есть — подставил значение.
      • 0
        в скомпилированном классе шаблона
        ...
        if (((isset($context["usertype"]) ? $context["usertype"] : null) == 2)) {
        ...
        


        2 — это же значение константы Users::TYPE_TROLL? если да, то это строка так и будет выглядеть в кэше шаблона. и если вдруг значение Users::TYPE_TROLL изменится или в обще убрать константу то придётся сбрасывать кэш.
        • 0
          Я в посте описал проблему в самом начале. При выкатке в бой кеш шаблонов компилируется полностью, снуля и целиком, чтобы в бою не пришлось. Именно на этом этапе хорошо было бы ругнуться, чтобы можно было поправить ошибку до того, как она попала в бой. А если ошибки нет, то бонусом значение запилить вместо вызова в рантайме.

          Надеюсь, теперь понятно расписал :]
      • +1
        У автора проблема довольно простая. Каждый раз когда кто-то забывает поправить шаблон после изменения констант в моделях, где-то умирает котенок во время сборки проекта в каком-нибудь CI сервере сброс и разогрев кэша не вызывает никаких ошибок, и о том что есть какие-то проблемы с шаблонами мы узнаем только после деплоя. Автор же хотел что бы на этапе сбоорки проекта ДО деплоя, любые проблемы с шаблонами вызванные изменением констант останавливали сборку и CI сервер слал бы уведомление что все плохо и сломано.
  • +1
    Нельзя так делать. Шаблоны перекомпилируются по времени их изменения, а не по изменению классов, которые использует шаблон.
    Я бы такой шаблон написал так:

    {% if user.troll %}
        fuckoff
    {% else %}
        ok
    {% endif %}
    

    И соответственно метод User::isTroll()
    А вот на счет проверки — да, хорошая штука. Вроде как уже PR есть скоро в ядре подобная проверка будет
    • +2
      В моем случае шаблоны перекомпилируются при выкатке, сделано для ругани на этом этапе, чтобы не допустить кривые константы в бой.
      • 0
        Ну так у вас сейчас включен дебаг в Twig_Environment.
        А когда Вы его отключите, то Вам верно выше указали. В случае чего придётся сбрасывать кэш.
        Ну или держать постоянно на дебаге, но в таком случае смысла с кэширования нет.
        • +2
          Я даже не знаю, как мне еще объяснить свою проблему и ее решение :(
          Прочитайте что-ли пост.
          • 0
            Простите, что огорчаем)
  • +1
    А стоит ли вообще логикой приложения грузить шаблон?

    Имхо, если у вас есть некая проверка, не является ли пользователь троллем, то правильнее было бы это обсчитать в ваших моделях, а шаблону просто передать флаг userIsTroll.
    • 0
      Это для наглядности. Бывают места, где таких завязок очень много и выносить это в контроллер или модель не выгодно — получится еще более трудно поддерживаемая простыня свойств и функций, используемых только в одном месте.
      • +2
        Как раз правильно в модель выносить подобные проверки, чтобы не сверяться с константами. Как минимум это способствует сохранности чистоты в шаблонах и выносу логики. Оставили метод isBlabla и потом проверяете. Можете убрать константы, отрефакторить 10 раз, а isBlaBla всегда будет работать, а если нет — ошибку тут же увидите
        • 0
          Точно так же не увижу ошибки и точно так же про шаблон забудется :)
          • 0
            Омг. У вас будет ошибка, что twig не нашел свойства blabla, методов has, is.
            Еще раз, логика модельки остается логикой модельки, а ее свойства узнаются через методы.
            Вы пишите
            {{ user.troll ? 'fuck off' : 'ok' }}
            

            А в модели это может быть хоть как:
            public function isTroll()
            {
                return $this->status === self::TROLL_STATUS;
            }
            
            public function isTroll()
            {
                return $this->hasStatus('troll');
            }
            
            public function isTroll()
            {
                return 100 > count($this->getLastMessages(60*60*6));
            }
            
            • 0
              #!/usr/bin/env php
              <?php
              
              require 'vendor/autoload.php';
              
              class JustForTest { }
              
              $loader = new Twig_Loader_String();
              $twig = new Twig_Environment($loader);
              
              echo $twig->render('Hello, {{ test.notExists() }}', [
              	'test' => new JustForTest()
              ] );

              даже ворнинга не дает, выводит «Hello, ».

              И да, я согласен, что по возможности надо выносить такое дело в модели, но это не всегда выгодно, я уже писал выше.
              • +1
                Вы просто твиг готовить не умеете значит. twig.sensiolabs.org/doc/api.html#environment-options — обратите внимание на strict_variables.
                Вот исключение:
                Twig_Error_Runtime: Method "notExists" for object "JustForTest" does not exist in "Hello, {{ test.notExists() }}" at line 1 in /Users/hell0w0rd/Desktop/test/vendor/twig/twig/lib/Twig/Template.php on line 438
                
                • 0
                  Если вы умеете хорошо готовить, в том числе Twig, — приходите к нам на собеседование :)
                  Со strict_variables уже не так комфортно, мне кажется. Да и поздно уже его включать, когда шаблонов слишком много.

                  В любом случае это — уже отдельная тема для обсуждения.
                  • 0
                    Если принимаете стажеров-студентов, прийду) Для того чтобы было комфортно в симфони например есть тестовое окружение. Где все как на продакшене, но включен режим дебага, в тч strict_variables.
                    Или вы имеете ввиду что вам переодически нужно использовать несуществующие свойства объектов, а проверки писать не хочется? На мой взгляд выключить strict_vars, это как выключить варнинги и нотисы в php)
                    • –1
                      Да, я имею в виду что в существующих шаблонах сплошняком завязки на то, что отсутствующее свойство — аналог false. И, на самом деле, это слишком удобно чтобы от этого взять и отказаться.
                      Насчет стажеров-студентов не уверен, но вас никто не съест, если вы спросите это в отклике на наши вакансии.
                      • 0
                        А можно пример? Вы бы кстати это в статье указали, что подобное нужно когда отключены strict_vars потому-то потому-то)
                        • 0
                          Да вот пример — {% if errors %}...{% endif %}.
                          strict_variables не влияет на константы, а топик про них. Да и даже не особо про них, сколько поверхностно о системе расширений.
                          • 0
                            Ну тут опять же если везде в шаблонах возможно есть errors, а может и нет — я бы определил глобальную переменную, по умолчанию пустую, а если ошибки есть — переопределять ее в скопе шаблона при передаче аргументов.
                            Но конкретно в таком виде мне не понятно как у вас собираются аргументы для шаблонов. Обычно это как-то так:
                            $errors = [];
                            foreach($form as $field) {
                                $errors[] = $field->getError();
                            }
                            // Или
                            $errors = $form->getErrors();
                            

                            То есть в шаблон и так и так передадутся errors, но в хорошем случае массив будет пуст, и проверка нормально отработает, или же там что-то будет и проверка опять же нормально отработает.
                            Так что мое мнение уже озвучил — отключать strict равносильно отключению ошибок в php. Вы же вот так:
                            foreach($foo as $bar) {
                                $arr[] = $bar;
                            }
                            

                            Не пишите? Вот в шаблонах тоже на мой взгляд так не стоит.
                          • 0
                            Такой путь ведёт к результирующей каше.

                            Конечно, заманчиво ввести глобальный вывод ошибок, а для тех контроллеров, для которых это пока что не удалось внедрить, просто ничего не выводить. В итоге логика, которая должна везде работать одинаково, работает по-разному. Вывод ошибок нужен только там, где про ошибки вообще знают. А если про ошибки знают, то результат может быть только один — ошибок нет (errors = null, errors = [], etc.) или ошибки есть (errors = [...]). Это говорит в том числе и о том, что ошибки не забыли импортнуть в шаблон, ошибок действительно нет.

                            Если бы интерпретатор php изначально шёл в strict-режиме без возможности устраивать хороводы, то никто бы не говорил, что «неинициализированная переменная равна null — это удобно». Нестрогий режим может использоваться только на продакшне (и то только из-за того, что PHP изначально допускает различные режимы). На девелопменте — только strict со всем вытекающим (написал шаблон, шаблон развалился — исправляй).
                          • 0
                            Это совершенно некорректно и ведет к тем же проблемам, что и в старых версиях php делал register_globals и не выставленный в E_ALL уровень репортинга. Умом понимаю, почему Twig по умолчанию не включает стрикт по умолчанию, но сердце отказывается верить. Да и согласитесь,

                            {% if errors is defined %}

                            выглядят куда круче. В крайнем случае это может быть

                            {% if errors|default('') is empty %}

                            Когда у нас логика сложнее/ненормальнее.
                  • 0
                    Тема-то не отдельная. Какой смысл предлагать велосипеды из-за плохо прочитанной документации?
      • 0
        Если у вас в отображении есть логика, не касающаяся самого отображения, то можете перестать употреблять слова вроде «модель», «контроллер», «отображение».

        Шаблон должен быть таким, чтобы верстальщик мог его редактировать без помощи PHP-программиста. Соответственно, шаблон должен включать в себя только включение других шаблонов, плейсхолдеры, форматирующие функции и конструкции, помогающие организовывать ветвление и итерирование.

        С этой точки зрения верстальщику нужно знать только некий «контракт», например:
        user — объект, данные и параметры текущего пользователя;
        user.firstName — строка, имя пользователя;
        user.lastName — строка, фамилия пользователя;
        user.troll — булевый флаг, является ли пользователь троллем;
        users — список объектов user.

        Как именно реализуется user.troll и остальные вещи верстальщику знать не обязательно. Для решения задачи: «Вывести в таблице список пользователей с указанием имени и фамилии. Строки пользователей-троллей помечать классом warning.» верстальщику вы не нужны.
        • –1
          Вы живете в идеальном мире, где все просто и удобно, но на самом деле все не так.

          Это — действительно отдельная тема для обсуждения.
          • 0
            На самом деле, это всего лишь ваше неправильное мнение.

            Нет никого идеального мира. Контроллер разделяет логику и отображение. К контроллеру нужно относиться так же, как к API сервиса — контекст, который он задаёт шаблону — это все данные, которые шаблону нужно и можно знать.

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

            С этой точки зрения вообще сложно понять fabpot. Кто-кто, а он должен понимать, что предоставление таких расширений по умолчанию — это очень плохо.

            В итоге получаем следующее: github.com/fabpot/Twig/issues/1149

            Как результат, имеем аналог strict-mode для PHP — sandbox-mode. Это для тех, кто не привык использовать PHP в стиле «ай, пофиг, можно же», а привык думать и разделять.
            • 0
              Нет правильного или неправильного, есть задачи, которые нужно решать.
              Если вы думаете что я внезапно решу переписать все шаблоны и контроллеры — вы ошибаетесь.

              Предлагаю на этом дискуссию закончить.
      • +1
        Я могу представить только пару кейсов в которых нужно использовать константы в шаблонах:
        проверка на то, может ли пользователь делать что-то (тут лучше сделать карту правил для пользователей и там определить что им можно а что нельзя и проверять в шаблоне без привязки к модели и константам)
        в зависимости от типа контента нужно что-то другое вывести. тут опять же лучше уж использовать разные шаблоны и общие части вынести в базовый шаблон (слава наследованию шаблонов).

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

          user.hasPermission(constant(User::PERM_KILL_CAT)) или acl.subjectHasPermission(user, constant(Acl::PERM_KILL_CAT'))

          Вводить для каждого разрешения методы типа user.canKillCat (даже если использовать _call) по-моему избыточно.
    • +1
      Если тролль или не тролль пользователь определяется в результате анализа некоего статуса, представленного «перечислением», то для каждого статуса иметь флаг userIs<Status> (или метод user.Is<Status>()) несколько напряжно может быть. Использование констант в шаблоне тут вполне оправдано, если они используются для определения что пользователю показывать, а что нет (как инфу, так и интерфейсы). А собственно что ещё в шаблоне может быть :)

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