a Sensio Labs Product

Гибкий, быстрый и безопасный
шаблонизатор для PHP

Расширения Twig

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

Скорее всего, вы создадите одно расширение для вашего проекта, которое будет содержать все дополнительные тэги и фильтры.

Анатомия расширения

Класс расширения должен реализовывать следующий интерфейс:

interface Twig_ExtensionInterface
{
  /**
   * Инициализация переменных окружения.
   *
   * Здесь вы можете подключить дополнительные файлы, содержащие, к примеру, ваши фильтры.
   */
  public function initRuntime();
 
  /**
   * Возвращает список токенов для добавления в существующий список.
   *
   * @return array возвращает массив элементов Twig_TokenParser
   */
  public function getTokenParsers();
 
  /**
   * Возвращает преобразованные узлы, для добавления в существующий список.
   *
   * @return array Возвращает массив элементов Twig_NodeTransformer
   */
  public function getNodeTransformers();
 
  /**
   * Возвращает массив фильтров, добавляемых расширением.
   *
   * @return array Массив фильтров
   */
  public function getFilters();
 
  /**
   * Возвращает имя расширения.
   *
   * @return string Имя расширения
   */
  public function getName();
}

Чтобы код вашего расширения оставался чистым и простым, его можно унаследовать от класса Twig_Extension, вместо создания полной реализации интерфейса. Таким образом, вам необходимо реализовать только метод getName(), а Twig_Extension предоставить пустые реализации остальных методов.

Метод getName() должен вернуть уникальный идентификатор вашего расширения.

Теперь, создадим самый простейший класс расширения:

class Project_Twig_Extension extends Twig_Extension
{
  public function getName()
  {
    return 'project';
  }
}

Конечно же, сейчас это расширение ничего не делает. Дальше мы добавим в него тэги и фильтры.

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

Зарегистрировать расширение можно с помощью метода addExtension() в вашем приложении:

$twig = new Twig_Environment($loader);
$twig->addExtension(new Project_Twig_Extension());

Естественно, сначала надо загрузить файл расширения, используя require_once() или автозагрузчик (spl_autoload_register()).

Существующие наборы расширений - лучший пример того, как они работают.

Создаем новый фильтр

Этот раздел описывает как создать расширение для Twig 0.9.5 и выше.

Чаще всего дополнительный функционал требуется в фильтрах. Фильтр это обыкновенная функция или метод, который принимает значение слева от оператора фильтра (|), как первый аргумент, а параметры фильтра - как дополнительные аргументы.

Например, в коде шаблона есть следующий код:

{{ 'TWIG'|lower }}

Когда Twig компилирует этот шаблон, сначала он будет искать PHP функцию связанную с фильтром lower. Фильтр lower встроенный в Twig и является оберткой для функции strtolower(). После компиляции, сгенерированный PHP код будет эквивалентен:

<?php echo strtolower('TWIG') ?>

Как видите, строка 'TWIG' является первым аргументом функции PHP.

Фильтр может так же принимать дополнительные аргументы, например так:

{{ now|date('d/m/Y') }}

Здесь дополнительные параметры передадуться в функцию после основного аргумента и скомпилированный код будет эквивалентен:

<?php echo twig_date_format_filter($now, 'd/m/Y') ?>

Фильтры-функции

Создадим новый фильтр.

В этом разделе мы создадим фильтр rot13, который будет делать преобразование rot13 над строкой. Так он будет использоваться:

{{ "Twig"|rot13 }}
 
{# отобразит Gjvt #}

Для добавления нового фильтра надо переопределить метод getFilters(). Он должен возвращать массив фильтров добавляемых в Twig:

class Project_Twig_Extension extends Twig_Extension
{
  public function getFilters()
  {
    return array(
      'rot13' => new Twig_Filter_Function('str_rot13'),
    );
  }
 
  public function getName()
  {
    return 'project';
  }
}

Как вы можете видеть в коде выше, метод getFilters() возвращает массив, в котором ключи это названия фильтров (rot13), а значения это реализация фильтров (new Twig_Filter_Function('str_rot13')).

Фильтр это всегда объект. В этом примере, мы определили фильтр как объект класса Twig_Filter_Function.

Класс Twig_Filter_Function используется когда вам надо определить реализацию фильтра как функцию. Первый аргумент конструктора Twig_Filter_Function - это имя вызываемой функции (здесь это str_rot13, встроенная PHP функция).

Предположим мы хотим иметь возможность добавить префикс к преобразованной строке:

{{ "Twig"|rot13('prefix_') }}
 
{# отобразит prefix_Gjvt #}

Так как функция str_rot13() не поддерживает такой функционал, создадим новую функцию:

function project_compute_rot13($string, $prefix = '')
{
  return $prefix.str_rot13($string);
}

Как вы видите, prefix передается в фильтр как дополнительный аргумент функции project_compute_rot13().

Эту функцию можно определить где угодно, но все же лучше расположить ее в том же файле, что и само расширение.

Новый код расширения очень похож на предыдущий:

class Project_Twig_Extension extends Twig_Extension
{
  public function getFilters()
  {
    return array(
      'rot13' => new Twig_Filter_Function('project_compute_rot13'),
    );
  }
 
  public function getName()
  {
    return 'project';
  }
}

Фильтры-методы классов

Вместо создания отдельной функции, создадим статический метод класса, для лучшей инкапсуляции.

Класс Twig_Filter_Function может быть использован для регистрации такого метода как фильтра:

class Project_Twig_Extension extends Twig_Extension
{
  public function getFilters()
  {
    return array(
      'rot13' => new Twig_Filter_Function('Project_Twig_Extension::rot13Filter'),
    );
  }
 
  static public function rot13Filter($string)
  {
    return str_rot13($string);
  }
 
  public function getName()
  {
    return 'project';
  }
}

Фильтр-метод объекта

Определение статического метода класса - хороший шаг к инкапсуляции, но определение фильтра как метод класса нашего расширения - гораздо лучше.

Это возможно при использовании класса Twig_Filter_Method вместо Twig_Filter_Function:

class Project_Twig_Extension extends Twig_Extension
{
  public function getFilters()
  {
    return array(
      'rot13' => new Twig_Filter_Method($this, 'rot13Filter'),
    );
  }
 
  public function rot13Filter($string)
  {
    return str_rot13($string);
  }
 
  public function getName()
  {
    return 'project';
  }
}

Первый аргумент конструктора Twig_Filter_Method это $this, т.е. объект расширения. Второй аргумент - название вызываемого метода.

Использование методов для создания фильтров позволяет создавать пакеты фильтров, без загрязнения глобального пространства имен. Это так же дает разработчику большую гибкость в проектировании.

Передача переменных окружения фильтрам

Класс Twig_Filter принимает переменную с параметрами в последнем аргументе. Например, если вы хотите иметь доступ к переменным окружения в фильтре, поставьте параметр needs_environment равный true:

$filter = new Twig_Filter_Function('str_rot13', array('needs_environment' => true));

Twig передаст переменные окружения в первом аргументе метода фильтр:

function twig_compute_rot13(Twig_Environment $env, $string)
{
  // для примера, получим текущую кодировку
  $charset = $env->getCharset();
 
  return str_rot13($string);
}

Автоматическое экранирование

Если включено автоматическое экранирование, то значение передаваемое первым аргументом в фильтр, будет экранировано. Если же ваш фильтр сам заботиться об экранировании, вам нужно необработанное значение переменной. В этом случае следует использовать параметр is_escaper:

$filter = new Twig_Filter_Function('urlencode', array('is_escaper' => true));

Дополнительные аргументы фильтров не зависят от is_escaper, и экранируются всегда.

Переопределение существующих фильтров

Этот раздел описывает как переопределить существующие фильтры в Twig 0.9.5 и выше.

Если встроенные в ядро фильтры не удовлетворяют вас по функционалу, вы легко можете переопределить их, создав свое расширение. Конечно же вам не надо копировать код всего расширения Twig. Вместо этого, вы можете расширить его и переопределить необходимые фильтры, перечислив их в методе getFilters():

class MyCoreExtension extends Twig_Extension_Core
{
  public function getFilters()
  {
    return array_merge(
      parent::getFilters(),
      array(
        'date' => Twig_Filter_Method($this, 'dateFilter')
      )
    );
  }
 
  public function dateFilter($timestamp, $format = 'F j, Y H:i')
  {
    return '...'.twig_date_format_filter($timestamp, $format);
  }
}

Здесь мы переопределили фильтр date. Для использования нового фильтра надо зарегистрировать расширение MyCoreExtension, вызвав метод addExtension():

$twig = new Twig_Environment($loader);
$twig->addExtension(new MyCoreExtension());

Могу предположить, что кому-нибудь будет интересно, как же это может работать, если расширения ядра уже загружены по умолчанию. Верно, загружены, но оба расширения имеют свои уникальные идентификаторы (они определяются в методе getName()). Регистрируя расширение с таким же именем, как и уже использующееся, вы перезаписываете его:

$twig->addExtension(new Twig_Extension_Core());
$twig->addExtension(new MyCoreExtension());

Создание новых тэгов

Одной из самых замечательных особенностей шаблонизатора является возможность создавать новые языковые конструкции - тэги.

Давайте создадим простой тэг set, позволяющий определять переменные в шаблоне. Тэг можно будет использовать следующим образом:

{% set name as "value" %}
 
{{ name }}
 
{# выведет value #}

Тэг set является частью ядра и доступен всегда. Он немного более функционален, чем приведенный здесь, например, поддерживает множественное назначение переменных (подробнее можно почитать в разделе Twig для верстальщика).

Для создания нового тэга надо сделать три шага:

  • Унаследовать класс от Twig_TokenParser (отвечает за парсинг шаблона)

  • Унаследовать класс от Twig_Node (отвечает за преобразование распарсенного кода в PHP код)

  • Зарегистрировать тэг

Регистрируем новый тэг

Чтобы добавить новый тэг надо переопределить метод getTokenParsers(). Он возвращает массив тегов добавляемых в Twig:

class Project_Twig_Extension extends Twig_Extension
{
  public function getTokenParsers()
  {
    return array(new Project_Set_TokenParser());
  }
 
  // ...
}

Здесь мы добавили один новый тэг, определенный в классе Project_Set_TokenParser. Класс Project_Set_TokenParser отвечает за парсинг тэгов и компиляцию шаблона в PHP код.

Создаем парсер

Посмотрим на код этого класса:

class Project_Set_TokenParser extends Twig_TokenParser
{
  public function parse(Twig_Token $token)
  {
    $lineno = $token->getLine();
    $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
    $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, 'as');
    $value = $this->parser->getExpressionParser()->parseExpression();
 
    $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
 
    return new Project_Set_Node($name, $value, $lineno, $this->getTag());
  }
 
  public function getTag()
  {
    return 'set';
  }
}

Метод getTag() возвращает название тэга, в данном случае set.

Метод parse() вызывается каждый раз, когда парсер встречает тэг set. Он должен вернуть объект класса Twig_Node, отвечающий за этот узел.

Разбор шаблона можно сделать более простым, используя набор методов потока токенов ($this->parser->getStream()):

  • test(): Проверяет тип и значение токена и возвращает его.

  • expect(): Проверяет существование токена и возвращает его (как и test()) или бросает синтаксическую ошибку, если токен не найден (второй аргумент содержит предполагаемое значение токена).

  • look(): Возвращает следующий токен. С его помощью вы можете получить следующий токен не переходя на него.

Эти выражения используются в parseExpression(), так же как мы использовали их для тэга set.

Лучший способ изучить детали парсинга, это разобраться в коде класса TokenParser.

Определяем узел

Класс Project_Set_Node очень прост:

class Project_Set_Node extends Twig_Node
{
  protected $name;
  protected $value;
 
  public function __construct($name, Twig_Node_Expression $value, $lineno)
  {
    parent::__construct($lineno);
 
    $this->name = $name;
    $this->value = $value;
  }
 
  public function compile($compiler)
  {
    $compiler
      ->addDebugInfo($this)
      ->write('$context[\''.$this->name.'\'] = ')
      ->subcompile($this->value)
      ->raw(";\n")
    ;
  }
}

Компилятор имеет гибкий интерфейс и предоставляет набор методов, помогающих генерировать красивый и понятный PHP код:

  • subcompile(): Компилирует узел.

  • raw(): Выводит строку, как есть.

  • write(): Выводит строку, добавляя отступы в начале каждой линии.

  • string(): Выводит строку.

  • repr(): Выводит PHP представление данного значения (смотри Twig_Node_For для примера).

  • pushContext(): Выталкивает текущее значение из стека (смотри Twig_Node_For для примера).

  • popContext():Добавляет данные в стек (смотри Twig_Node_For для примера).

  • addDebugInfo(): Добавляет в файл шаблона текущий узел как комментарий.

  • indent(): Делает отступы в генерируемом коде (смотри Twig_Node_Block для примера).

  • outdent(): Убирает отступы в генерируемом коде (смотри Twig_Node_Block для примера).

Продолжение следует...