Расширение Twig
Twig можно расширить множеством способов; вы можете добавить дополнительные теги, фильтры, тесты, операторы, глобальные переменные и функции. Вы даже можете расширить сам парсер с помощью посетителей узлов.
Примечание
В первом разделе этой главы описывается, как расширить Twig. Если вы хотите повторно использовать свои изменения в разных проектах или поделиться ими с другими, вы должны создать расширение, как описано в следующем разделе.
Внимание
При расширении Twig без создания расширения Twig не сможет перекомпилировать ваши шаблоны при обновлении PHP-кода. Чтобы видеть свои изменения в реальном времени, отключите кэширование шаблонов или упакуйте код в расширение (см. следующий раздел этой главы).
Прежде чем расширять Twig, вы должны понять различия между всеми возможными точками расширения и когда их использовать.
Во-первых, помните, что Twig имеет две основные языковые конструкции:
-
{{ }}: используется для вывода результата вычисления выражения; -
{% %}: используется для выполнения операторов.
Чтобы понять, почему Twig предоставляет так много точек расширения, давайте посмотрим, как реализовать генератор Lorem ipsum (он должен знать количество слов для генерации).
Вы можете использовать lipsum тег:
{% lipsum 40 %}
Это работает, но использование тега для lipsum — не лучшая идея по крайней мере по трём причинам:
-
lipsumне является языковой конструкцией; -
Тег выводит что-то;
-
Тег негибок, поскольку его нельзя использовать в выражении:
{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
На самом деле, редко нужно создавать теги; и это хорошая новость, потому что теги — самая сложная точка расширения.
Теперь давайте воспользуемся lipsum фильтром:
{{ 40|lipsum }}
Опять же, это работает. Но фильтр должен преобразовывать переданное значение во что-то другое. Здесь мы используем значение для указания количества слов для генерации (поэтому 40 является аргументом фильтра, а не значением, которое мы хотим преобразовать).
Далее, давайте воспользуемся lipsum функцией:
{{ lipsum(40) }}
Вот и всё. В этом конкретном примере создание функции является точкой расширения, которую следует использовать. И вы можете использовать её везде, где разрешено выражение:
{{ 'some text' ~ lipsum(40) ~ 'some more text' }}
{% set lipsum = lipsum(40) %}
Наконец, вы также можете использовать объект global с методом, способным генерировать текст Lorem ipsum:
{{ text.lipsum(40) }}
Как правило, используйте функции для часто используемых функций и глобальные объекты для всего остального.
Помните об этом, когда хотите расширить Twig:
| Что? | Сложность реализации? | Как часто? | Когда? |
|---|---|---|---|
| Макрос | просто | часто | Генерация контента |
| Глобальный | просто | часто | Объект-помощник |
| Функция | просто | часто | Генерация контента |
| Фильтр | просто | часто | Преобразование значений |
| Тег | сложно | редко | Конструкция DSL-языка |
| Тест | просто | редко | Булево решение |
| Оператор | просто | редко | Преобразование значений |
Глобальные переменные
Глобальная переменная — это такая же переменная шаблона, за исключением того, что она доступна во всех шаблонах и макросах:
$twig = new \Twig\Environment($loader);
$twig->addGlobal('text', new Text());
Затем вы можете использовать переменную text в любом шаблоне:
{{ text.lipsum(40) }}
Фильтры
Создание фильтра заключается в сопоставлении имени с вызываемым элементом PHP:
// an anonymous function
$filter = new \Twig\TwigFilter('rot13', function ($string) {
return str_rot13($string);
});
// or a simple PHP function
$filter = new \Twig\TwigFilter('rot13', 'str_rot13');
// or a class static method
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
$filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');
// or a class method
$filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
// the one below needs a runtime implementation (see below for more information)
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
Первый аргумент, переданный конструктору \Twig\TwigFilter, — это имя фильтра, которое вы будете использовать в шаблонах, а второй — вызываемый элемент PHP, который нужно с ним связать.
Затем добавьте фильтр в среду Twig:
$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);
Вот как его использовать в шаблоне:
{{ 'Twig'|rot13 }}
{# will output Gjvt #}
При вызове Twig вызываемый элемент PHP получает левую часть фильтра (перед символом пайпа |) в качестве первого аргумента и дополнительные аргументы, переданные фильтру (в скобках ()) в качестве дополнительных аргументов.
Например, следующий код:
{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
компилируется примерно следующим образом:
<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>
Класс \Twig\TwigFilter принимает массив опций в качестве последнего аргумента:
$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);
Фильтры, осведомленные об окружении
Если вы хотите получить доступ к экземпляру текущей среды в своём фильтре, установите опцию needs_environment в значение true; Twig передаст текущую среду в качестве первого аргумента вызова фильтра:
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
// get the current charset for instance
$charset = $env->getCharset();
return str_rot13($string);
}, ['needs_environment' => true]);
Фильтры, осведомленные о контексте
Если вы хотите получить доступ к текущему контексту в своём фильтре, установите опцию needs_context в значение true; Twig передаст текущий контекст в качестве первого аргумента вызова фильтра (или второго, если needs_environment также установлено в значение true):
$filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
// ...
}, ['needs_context' => true]);
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
// ...
}, ['needs_context' => true, 'needs_environment' => true]);
Автоматическая экранизация
Если автоматическая экранизация включена, вывод фильтра может быть экранирован перед выводом. Если ваш фильтр действует как экранировщик (или явно выводит HTML или JavaScript-код), вам потребуется вывести исходный вывод. В таком случае установите опцию is_safe:
$filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);
Некоторые фильтры могут потребовать работы с вводом, который уже экранирован или безопасен, например, при добавлении (безопасных) HTML-тегов к исходно небезопасному выводу. В таком случае установите опцию pre_escape для экранирования входных данных перед их обработкой вашим фильтром:
$filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);
Многоаргументные фильтры
Если фильтр должен принимать произвольное количество аргументов, установите опцию is_variadic в значение true; Twig передаст дополнительные аргументы в качестве последнего аргумента вызова фильтра в виде массива:
$filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
// ...
}, ['is_variadic' => true]);
Обратите внимание, что именованные аргументы, переданные многоаргументного фильтру, не могут быть проверены на корректность, так как они автоматически попадут в массив опций.
Динамические фильтры
Имя фильтра, содержащее специальный символ *, — это динамический фильтр, и часть * будет соответствовать любой строке:
$filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
// ...
});
Следующие фильтры соответствуют вышеопределенному динамическому фильтру:
product_pathcategory_path
Динамический фильтр может определять более одной динамической части:
$filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {
// ...
});
Фильтр получает все значения динамических частей перед обычными аргументами фильтра, но после среды и контекста. Например, вызов 'foo'|a_path_b() приведет к передаче в фильтр следующих аргументов: ('a', 'b', 'foo').
Устаревшие фильтры
Вы можете пометить фильтр как устаревший, установив опцию deprecated в значение true. Вы также можете указать альтернативный фильтр, который заменяет устаревший, если это имеет смысл:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
Когда фильтр устаревает, Twig выводит сообщение об устаревании при компиляции шаблона с его использованием. Более подробную информацию см. в разделе Отображение сообщений об устаревании.
Функции
Функции определяются точно так же, как и фильтры, но вам нужно создать экземпляр \Twig\TwigFunction:
$twig = new \Twig\Environment($loader);
$function = new \Twig\TwigFunction('function_name', function () {
// ...
});
$twig->addFunction($function);
Функции поддерживают те же функции, что и фильтры, за исключением опций pre_escape и preserves_safety.
Тесты
Тесты определяются точно так же, как фильтры и функции, но вам нужно создать экземпляр \Twig\TwigTest:
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('test_name', function () {
// ...
});
$twig->addTest($test);
Тесты позволяют создавать пользовательскую специфичную для приложения логику для оценки булевых условий. В качестве простого примера давайте создадим тест Twig, который проверяет, являются ли объекты «красными»:
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('red', function ($value) {
if (isset($value->color) && $value->color == 'red') {
return true;
}
if (isset($value->paint) && $value->paint == 'red') {
return true;
}
return false;
});
$twig->addTest($test);
Функции тестов всегда должны возвращать true/false.
При создании тестов вы можете использовать опцию node_class для предоставления пользовательской компиляции теста. Это полезно, если ваш тест может быть скомпилирован в PHP-примитивы. Это используется во многих встроенных в Twig тестах:
namespace App;
use Twig\Environment;
use Twig\Node\Expression\TestExpression;
use Twig\TwigTest;
$twig = new Environment($loader);
$test = new TwigTest(
'odd',
null,
['node_class' => OddTestExpression::class]);
$twig->addTest($test);
class OddTestExpression extends TestExpression
{
public function compile(\Twig\Compiler $compiler)
{
$compiler
->raw('(')
->subcompile($this->getNode('node'))
->raw(' % 2 != 0')
->raw(')')
;
}
}
Приведенный выше пример показывает, как можно создать тесты, использующие класс узла. Класс узла имеет доступ к одному под-узлу, называемому node. Этот под-узел содержит значение, которое тестируется. Когда используется фильтр odd в коде, например:
{% if my_value is odd %}
Под-узел node будет содержать выражение my_value. Тесты на основе узлов также имеют доступ к узлу arguments. Этот узел будет содержать различные другие аргументы, которые были предоставлены вашему тесту.
Если вы хотите передать переменное количество позиционных или именованных аргументов в тест, установите опцию is_variadic в значение true. Тесты поддерживают динамические имена (см. динамические фильтры для синтаксиса).
Теги
Одним из самых захватывающих свойств движка шаблонов, такого как Twig, является возможность определять новые языковые конструкции. Это также самая сложная функция, так как вам необходимо понять, как работают внутренние механизмы Twig.
В большинстве случаев тег не нужен:
-
Если ваш тег генерирует вывод, используйте вместо него функцию.
-
Если ваш тег изменяет содержимое и возвращает его, используйте вместо него фильтр.
Например, если вы хотите создать тег, который преобразует текст в формате Markdown в HTML, создайте
markdownфильтр вместо него:{{ '**markdown** text'|markdown }}
Если вы хотите использовать этот фильтр для больших объемов текста, оберните его тегом apply:
{% apply markdown %} Title ===== Much better than creating a tag as you can **compose** filters. {% endapply %}
-
Если ваш тег ничего не выводит, но существует только из-за побочного эффекта, создайте функцию, которая ничего не возвращает, и вызовите её с помощью тега filter.
Например, если вы хотите создать тег, который записывает текст в журнал, создайте
logфункцию вместо него и вызовите её с помощью тега do:{% do log('Log some things') %}
Если вы всё ещё хотите создать тег для новой языковой конструкции, отлично!
Давайте создадим set тег, который позволяет определять простые переменные внутри шаблона. Этот тег можно использовать следующим образом:
{% set name = "value" %}
{{ name }}
{# should output value #}
Примечание
Тег set является частью ядра расширения и, как следствие, всегда доступен. Встроенная версия немного мощнее и по умолчанию поддерживает несколько присваиваний.
Для определения нового тега необходимо выполнить три шага:
- Определение класса обработчика токенов (ответственного за разбор кода шаблона);
- Определение класса узла (ответственного за преобразование обработанного кода в PHP);
- Регистрация тега.
Регистрация нового тега
Добавьте тег, вызвав addTokenParser метод на объекте \Twig\Environment:
$twig = new \Twig\Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());
Определение обработчика токенов
Теперь давайте рассмотрим фактический код этого класса:
class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
{
public function parse(\Twig\Token $token)
{
$parser = $this->parser;
$stream = $parser->getStream();
$name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
$stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
$value = $parser->getExpressionParser()->parseExpression();
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
}
public function getTag()
{
return 'set';
}
}
Метод getTag() должен вернуть тег, который мы хотим обработать, здесь это set.
Метод parse() вызывается всякий раз, когда парсер встречает тег set. Он должен вернуть экземпляр \Twig\Node\Node, который представляет узел (вызовы Project_Set_Node создания поясняются в следующем разделе).
Процесс разбора упрощается благодаря нескольким методам, которые можно вызывать из потока токенов ($this->parser->getStream()):
-
getCurrent(): Получает текущий токен в потоке. -
next(): Перемещается к следующему токену в потоке, но возвращает старый. -
test($type),test($value)илиtest($type, $value): Определяет, является ли текущий токен определённого типа или значения (или обоих). Значение может быть массивом нескольких возможных значений. -
expect($type[, $value[, $message]]): Если текущий токен не соответствует заданному типу/значению, выбрасывается синтаксическая ошибка. В противном случае, если тип и значение верны, токен возвращается, и поток переходит к следующему токену. -
look(): Смотрит на следующий токен, не потребляя его.
Разбор выражений выполняется путём вызова parseExpression(), как мы делали для тега set.
Подсказка
Лучший способ узнать все подробности процесса разбора — изучить существующие классы TokenParser.
Определение узла
Класс Project_Set_Node сам по себе довольно короткий:
class Project_Set_Node extends \Twig\Node\Node
{
public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
{
parent::__construct(['value' => $value], ['name' => $name], $line, $tag);
}
public function compile(\Twig\Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('$context[\''.$this->getAttribute('name').'\'] = ')
->subcompile($this->getNode('value'))
->raw(";\n")
;
}
}
Компилятор реализует гибкий интерфейс и предоставляет методы, которые помогают разработчику генерировать красивый и читабельный PHP-код:
-
subcompile(): Компилирует узел. -
raw(): Записывает заданную строку как есть. -
write(): Записывает заданную строку, добавляя отступ в начале каждой строки. -
string(): Записывает строку в кавычках. -
repr(): Записывает PHP-представление заданного значения (см.\Twig\Node\ForNodeдля примера использования). -
addDebugInfo(): Добавляет строку из исходного файла шаблона, связанную с текущим узлом, как комментарий. -
indent(): Увеличивает отступ сгенерированного кода (см.\Twig\Node\BlockNodeдля примера использования). -
outdent(): Уменьшает отступ сгенерированного кода (см.\Twig\Node\BlockNodeдля примера использования).
Создание расширения
Основная мотивация для написания расширения — перенести часто используемый код в переиспользуемый класс, например, для добавления поддержки интернационализации. Расширение может определять теги, фильтры, тесты, операторы, функции и посетителей узлов.
В большинстве случаев полезно создать одно расширение для вашего проекта, чтобы разместить все специфичные теги и фильтры, которые вы хотите добавить в Twig.
Подсказка
При упаковке вашего кода в расширение, Twig достаточно умен, чтобы перекомпилировать ваши шаблоны всякий раз, когда вы вносите изменения (когда auto_reload включён).
Расширение — это класс, который реализует следующий интерфейс:
interface \Twig\Extension\ExtensionInterface
{
/**
* Returns the token parser instances to add to the existing list.
*
* @return \Twig\TokenParser\TokenParserInterface[]
*/
public function getTokenParsers();
/**
* Returns the node visitor instances to add to the existing list.
*
* @return \Twig\NodeVisitor\NodeVisitorInterface[]
*/
public function getNodeVisitors();
/**
* Returns a list of filters to add to the existing list.
*
* @return \Twig\TwigFilter[]
*/
public function getFilters();
/**
* Returns a list of tests to add to the existing list.
*
* @return \Twig\TwigTest[]
*/
public function getTests();
/**
* Returns a list of functions to add to the existing list.
*
* @return \Twig\TwigFunction[]
*/
public function getFunctions();
/**
* Returns a list of operators to add to the existing list.
*
* @return array<array> First array of unary operators, second array of binary operators
*/
public function getOperators();
}
Чтобы сохранить ваш класс расширения чистым и компактным, наследуйтесь от встроенного класса \Twig\Extension\AbstractExtension вместо реализации интерфейса, так как он предоставляет пустые реализации для всех методов:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
}
Это расширение пока ничего не делает. Мы настроим его в следующих разделах.
Вы можете сохранить своё расширение в любом месте на файловой системе, так как все расширения должны быть явно зарегистрированы, чтобы быть доступными в ваших шаблонах.
Вы можете зарегистрировать расширение, используя метод addExtension() на вашем основном объекте Environment:
$twig = new \Twig\Environment($loader);
$twig->addExtension(new Project_Twig_Extension());
Подсказка
Расширения ядра Twig — отличные примеры работы с расширениями.
Переменные
Глобальные переменные можно зарегистрировать в расширении через метод getGlobals():
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
public function getGlobals(): array
{
return [
'text' => new Text(),
];
}
// ...
}
Функции
Функции можно зарегистрировать в расширении через метод getFunctions():
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
];
}
// ...
}
Фильтры
Чтобы добавить фильтр в расширение, вам нужно переопределить метод getFilters(). Этот метод должен вернуть массив фильтров для добавления в среду Twig:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getFilters()
{
return [
new \Twig\TwigFilter('rot13', 'str_rot13'),
];
}
// ...
}
Теги
Добавление тега в расширение можно осуществить путём переопределения метода getTokenParsers(). Этот метод должен вернуть массив тегов для добавления в среду Twig:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getTokenParsers()
{
return [new Project_Set_TokenParser()];
}
// ...
}
В данном коде мы добавили один новый тег, определённый классом Project_Set_TokenParser. Класс Project_Set_TokenParser отвечает за обработку тега и его компиляцию в PHP.
Операторы
Метод getOperators() позволяет добавлять новые операторы. Вот как добавить операторы !, ||, и &&:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getOperators()
{
return [
[
'!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
],
[
'||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
'&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
],
];
}
// ...
}
Тесты
Метод getTests() позволяет добавлять новые функции-тесты:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getTests()
{
return [
new \Twig\TwigTest('even', 'twig_test_even'),
];
}
// ...
}
Определение и выполнение
Реализации фильтров, функций и тестов Twig могут быть определены как любые допустимые вызываемые элементы PHP:
- функции/статические методы: Просты в реализации и быстры (используются всеми расширениями ядра Twig); но сложно для среды выполнения зависеть от внешних объектов;
- замыкания: Просты в реализации;
- методы объектов: Более гибкие и необходимы, если ваш код среды выполнения зависит от внешних объектов.
Самый простой способ использовать методы — определить их в самом расширении:
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', [$this, 'rot13']),
];
}
public function rot13($value)
{
return $this->rot13Provider->rot13($value);
}
}
Это очень удобно, но не рекомендуется, так как это заставляет компиляцию шаблонов зависеть от зависимостей среды выполнения, даже если они не нужны (например, подумайте о зависимости, которая подключается к базе данных).
Вы можете отделить определения расширений от их реализаций среды выполнения, зарегистрировав экземпляр \Twig\RuntimeLoader\RuntimeLoaderInterface в среде, который знает, как создавать такие классы среды выполнения (классы среды выполнения должны быть загружаемы автоматически):
class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
{
public function load($class)
{
// implement the logic to create an instance of $class
// and inject its dependencies
// most of the time, it means using your dependency injection container
if ('Project_Twig_RuntimeExtension' === $class) {
return new $class(new Rot13Provider());
} else {
// ...
}
}
}
$twig->addRuntimeLoader(new RuntimeLoader());
Примечание
Twig поставляется с совместимым с PSR-11 загрузчиком среды выполнения (\Twig\RuntimeLoader\ContainerRuntimeLoader).
Теперь можно перенести логику среды выполнения в новый класс Project_Twig_RuntimeExtension и использовать его непосредственно в расширении:
class Project_Twig_RuntimeExtension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function rot13($value)
{
return $this->rot13Provider->rot13($value);
}
}
class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
// or
new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
];
}
}
Тестирование расширения
Функциональные тесты
Вы можете создать функциональные тесты для расширений, создав следующую структуру файлов в вашей тестовой директории:
Fixtures/
filters/
foo.test
bar.test
functions/
foo.test
bar.test
tags/
foo.test
bar.test
IntegrationTest.php
Файл IntegrationTest.php должен выглядеть следующим образом:
use Twig\Test\IntegrationTestCase;
class Project_Tests_IntegrationTest extends IntegrationTestCase
{
public function getExtensions()
{
return [
new Project_Twig_Extension1(),
new Project_Twig_Extension2(),
];
}
public function getFixturesDir()
{
return __DIR__.'/Fixtures/';
}
}
Примеры фикстур можно найти в репозитории Twig в директории tests/Twig/Fixtures.
Тесты узлов
Тестирование посетителей узлов может быть сложным, поэтому расширяйте свои тестовые случаи от \Twig\Test\NodeTestCase. Примеры можно найти в репозитории Twig в директории tests/Twig/Node.
© 2009–2018 by the Twig Team
Licensed under the three clause BSD license.
The Twig logo is © 2010–2020 Symfony
https://twig.symfony.com/doc/3.x/advanced.html