Рецепты
Отображение уведомлений о устаревании
Введено в версии 1.21: Это работает начиная с Twig 1.21.
Устаревшие функции генерируют уведомления об устаревании (через вызов функции PHP trigger_error()). По умолчанию они беззвучны и никогда не отображаются и не регистрируются.
Чтобы удалить все устаревшие функции из ваших шаблонов, напишите и запустите скрипт по аналогии со следующим:
require_once __DIR__.'/vendor/autoload.php';
$twig = create_your_twig_env();
$deprecations = new \Twig\Util\DeprecationCollector($twig);
print_r($deprecations->collectDir(__DIR__.'/templates'));
Метод collectDir() компилирует все шаблоны, найденные в каталоге, перехватывает уведомления об устаревании и возвращает их.
Подсказка
Если ваши шаблоны не хранятся на файловой системе, используйте метод collect() вместо него. collect() принимает Traversable, который должен возвращать имена шаблонов в качестве ключей и содержимое шаблонов в качестве значений (как это делает \Twig\Util\TemplateDirIterator).
Однако этот код не найдёт все устаревания (например, использование устаревших классов Twig). Чтобы перехватить все уведомления, зарегистрируйте обработчик пользовательских ошибок, как показано ниже:
$deprecations = [];
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
});
// run your application
print_r($deprecations);
Обратите внимание, что большинство уведомлений об устаревании вызываются во время компиляции, поэтому они не будут сгенерированы, когда шаблоны уже кэшированы.
Подсказка
Если вы хотите управлять уведомлениями об устаревании из своих тестов PHPUnit, обратитесь к пакету symfony/phpunit-bridge, который упрощает этот процесс.
Условное отображение макета
Работа с Ajax означает, что одно и то же содержимое иногда отображается как есть, а иногда оформляется с помощью макета. Поскольку имена шаблонов макета Twig могут быть любым допустимым выражением, вы можете передать переменную, которая вычисляется как true при выполнении запроса через Ajax и выбрать макет соответственно:
{% extends request.ajax ? "base_ajax.html" : "base.html" %}
{% block content %}
This is the content to be displayed.
{% endblock %}
Динамическое включение шаблона
При включении шаблона его имя не обязательно должно быть строкой. Например, имя может зависеть от значения переменной:
{% include var ~ '_foo.html' %}
Если var вычисляется как index, будет отрисовываться шаблон index_foo.html.
Фактически, имя шаблона может быть любым допустимым выражением, таким как следующее:
{% include var|default('index') ~ '_foo.html' %}
Переопределение шаблона, который также расширяет себя
Шаблон можно настроить двумя способами:
- Наследование: Шаблон расширяет родительский шаблон и переопределяет некоторые блоки;
- Замена: Если вы используете загрузчик файловой системы, Twig загружает первый шаблон, который находит в списке настроенных каталогов; шаблон, найденный в каталоге, заменяет другой шаблон из каталога, расположенного дальше в списке.
Но как объединить оба: заменить шаблон, который также расширяет себя (т.е. шаблон в каталоге, расположенном дальше в списке)?
Предположим, что ваши шаблоны загружаются как из .../templates/mysite, так и из .../templates/default в указанном порядке. Шаблон page.twig, хранящийся в .../templates/default, выглядит следующим образом:
{# page.twig #}
{% extends "layout.twig" %}
{% block content %}
{% endblock %}
Вы можете заменить этот шаблон, поместив файл с тем же именем в .../templates/mysite. А если вы хотите расширить исходный шаблон, то, возможно, захотите написать следующее:
{# page.twig in .../templates/mysite #}
{% extends "page.twig" %}{# from .../templates/default #}
Однако это не сработает, так как Twig всегда загрузит шаблон из .../templates/mysite.
Оказывается, это возможно, добавив каталог в конец каталогов шаблонов, который является родительским по отношению ко всем остальным каталогам: .../templates в нашем случае. Это позволит адресовать каждый файл шаблона в нашей системе уникальным образом. В большинстве случаев вы будете использовать «обычные» пути, но в особом случае, когда необходимо расширить шаблон с помощью переопределяющей версии самого себя, мы можем указать полный, однозначный путь родительского шаблона в теге extends:
{# page.twig in .../templates/mysite #}
{% extends "default/page.twig" %}{# from .../templates #}
Примечание
Этот рецепт был вдохновлен следующей страницей вики-проекта Django: https://code.djangoproject.com/wiki/ExtendingTemplates
Настройка синтаксиса
Twig позволяет настроить синтаксис блоков. Не рекомендуется использовать эту функцию, так как шаблоны будут связаны с вашим пользовательским синтаксисом. Но для определённых проектов это может быть целесообразно.
Чтобы изменить разделители блоков, необходимо создать свой собственный объект лексера:
$twig = new \Twig\Environment();
$lexer = new \Twig\Lexer($twig, [
'tag_comment' => ['{#', '#}'],
'tag_block' => ['{%', '%}'],
'tag_variable' => ['{{', '}}'],
'interpolation' => ['#{', '}'],
]);
$twig->setLexer($lexer);
Вот несколько примеров конфигурации, которые имитируют синтаксис других движков шаблонов:
// Ruby erb syntax
$lexer = new \Twig\Lexer($twig, [
'tag_comment' => ['<%#', '%>'],
'tag_block' => ['<%', '%>'],
'tag_variable' => ['<%=', '%>'],
]);
// SGML Comment Syntax
$lexer = new \Twig\Lexer($twig, [
'tag_comment' => ['<!--#', '-->'],
'tag_block' => ['<!--', '-->'],
'tag_variable' => ['${', '}'],
]);
// Smarty like
$lexer = new \Twig\Lexer($twig, [
'tag_comment' => ['{*', '*}'],
'tag_block' => ['{', '}'],
'tag_variable' => ['{$', '}'],
]);
Использование динамических свойств объекта
Когда Twig встречает переменную, такую как article.title, он пытается найти общедоступное свойство title в объекте article.
Это также работает, если свойства не существует, а определяется динамически благодаря магическому методу __get(); вам также нужно реализовать магический метод __isset(), как показано в следующем фрагменте кода:
class Article
{
public function __get($name)
{
if ('title' == $name) {
return 'The title';
}
// throw some kind of error
}
public function __isset($name)
{
if ('title' == $name) {
return true;
}
return false;
}
}
Доступ к родительскому контексту в вложенных циклах
Иногда при работе с вложенными циклами вам нужно получить доступ к родительскому контексту. Родительский контекст всегда доступен через переменную loop.parent. Например, если у вас есть данные шаблона:
$data = [
'topics' => [
'topic1' => ['Message 1 of topic 1', 'Message 2 of topic 1'],
'topic2' => ['Message 1 of topic 2', 'Message 2 of topic 2'],
],
];
И следующий шаблон для отображения всех сообщений во всех темах:
{% for topic, messages in topics %}
* {{ loop.index }}: {{ topic }}
{% for message in messages %}
- {{ loop.parent.loop.index }}.{{ loop.index }}: {{ message }}
{% endfor %}
{% endfor %}
Вывод будет примерно таким:
* 1: topic1
- 1.1: The message 1 of topic 1
- 1.2: The message 2 of topic 1
* 2: topic2
- 2.1: The message 1 of topic 2
- 2.2: The message 2 of topic 2
Во внутреннем цикле переменная loop.parent используется для доступа к внешнему контексту. Таким образом, индекс текущего topic, определённого во внешнем цикле for, доступен через переменную loop.parent.loop.index.
Определение неопределённых функций и фильтров на лету
Если функция (или фильтр) не определена, Twig по умолчанию выбрасывает исключение \Twig\Error\SyntaxError. Однако он также может вызвать обратный вызов (любой допустимый обратный вызов PHP), который должен вернуть функцию (или фильтр).
Для фильтров регистрируйте обратные вызовы с помощью registerUndefinedFilterCallback(). Для функций используйте registerUndefinedFunctionCallback():
// auto-register all native PHP functions as Twig functions
// don't try this at home as it's not secure at all!
$twig->registerUndefinedFunctionCallback(function ($name) {
if (function_exists($name)) {
return new \Twig\TwigFunction($name, $name);
}
return false;
});
Если обратный вызов не может вернуть действительную функцию (или фильтр), он должен вернуть false.
Если вы регистрируете более одного обратного вызова, Twig будет вызывать их по очереди, пока один не вернёт false.
Подсказка
Поскольку разрешение функций и фильтров выполняется во время компиляции, при регистрации этих обратных вызовов нет накладных расходов.
Проверка синтаксиса шаблона
Когда код шаблона предоставляется сторонним лицом (например, через веб-интерфейс), может быть полезно проверить синтаксис шаблона перед сохранением. Если код шаблона хранится в переменной $template, вот как это можно сделать:
try {
$twig->parse($twig->tokenize(new \Twig\Source($template)));
// the $template is valid
} catch (\Twig\Error\SyntaxError $e) {
// $template contains one or more syntax errors
}
Если вы перебираете набор файлов, вы можете передать имя файла методу tokenize() для получения имени файла в сообщении об ошибке:
foreach ($files as $file) {
try {
$twig->parse($twig->tokenize(new \Twig\Source($template, $file->getFilename(), $file)));
// the $template is valid
} catch (\Twig\Error\SyntaxError $e) {
// $template contains one or more syntax errors
}
}
Введено в версии 1.27: \Twig\Source был введён в версии 1.27, на предыдущих версиях передавались исходный код и идентификатор напрямую.
Примечание
Этот метод не перехватит никакие нарушения политики sandbox, так как политика применяется во время рендеринга шаблона (Twig нуждается в контексте для некоторых проверок, таких как разрешённые методы объектов).
Обновление изменённых шаблонов при включенном OPcache или APC
При использовании OPcache с opcache.validate_timestamps установленным на 0 или APC с apc.stat установленным на 0 и включенным кэшем Twig, очистка кэша шаблонов не обновит кэш.
Чтобы обойти эту проблему, заставьте Twig аннулировать кэш байткода:
$twig = new \Twig\Environment($loader, [
'cache' => new \Twig\Cache\FilesystemCache('/some/cache/path', \Twig\Cache\FilesystemCache::FORCE_BYTECODE_INVALIDATION),
// ...
]);
Примечание
Перед Twig 1.22 вам следует расширить \Twig\Environment вместо этого:
class OpCacheAwareTwigEnvironment extends \Twig\Environment
{
protected function writeCacheFile($file, $content)
{
parent::writeCacheFile($file, $content);
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN)) {
opcache_invalidate($file, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($file);
}
}
}
Использование состояния посетителя узла
При добавлении посетителя к экземпляру \Twig\Environment Twig использует его для посещения всех шаблонов, которые он компилирует. Если вам нужно сохранить какую-либо информацию о состоянии, вам, вероятно, нужно будет сбросить её при посещении нового шаблона.
Это можно сделать следующим кодом:
protected $someTemplateState = [];
public function enterNode(Twig_NodeInterface $node, \Twig\Environment $env)
{
if ($node instanceof \Twig\Node\ModuleNode) {
// reset the state as we are entering a new template
$this->someTemplateState = [];
}
// ...
return $node;
}
Использование базы данных для хранения шаблонов
Если вы разрабатываете CMS, шаблоны обычно хранятся в базе данных. Этот рецепт предоставляет вам простой загрузчик шаблонов PDO, который вы можете использовать в качестве отправной точки для собственного.
Сначала создадим временную базу данных SQLite3 в памяти для работы:
$dbh = new PDO('sqlite::memory:');
$dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)');
$base = '{% block content %}{% endblock %}';
$index = '
{% extends "base.twig" %}
{% block content %}Hello {{ name }}{% endblock %}
';
$now = time();
$dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.twig', $base, $now]);
$dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.twig', $index, $now]);
Мы создали простую таблицу templates, которая содержит два шаблона: base.twig и index.twig.
Теперь давайте определим загрузчик, способный использовать эту базу данных:
class DatabaseTwigLoader implements \Twig\Loader\LoaderInterface, \Twig\Loader\ExistsLoaderInterface, \Twig\Loader\SourceContextLoaderInterface
{
protected $dbh;
public function __construct(PDO $dbh)
{
$this->dbh = $dbh;
}
public function getSource($name)
{
if (false === $source = $this->getValue('source', $name)) {
throw new \Twig\Error\LoaderError(sprintf('Template "%s" does not exist.', $name));
}
return $source;
}
// \Twig\Loader\SourceContextLoaderInterface as of Twig 1.27
public function getSourceContext($name)
{
if (false === $source = $this->getValue('source', $name)) {
throw new \Twig\Error\LoaderError(sprintf('Template "%s" does not exist.', $name));
}
return new \Twig\Source($source, $name);
}
// \Twig\Loader\ExistsLoaderInterface as of Twig 1.11
public function exists($name)
{
return $name === $this->getValue('name', $name);
}
public function getCacheKey($name)
{
return $name;
}
public function isFresh($name, $time)
{
if (false === $lastModified = $this->getValue('last_modified', $name)) {
return false;
}
return $lastModified <= $time;
}
protected function getValue($column, $name)
{
$sth = $this->dbh->prepare('SELECT '.$column.' FROM templates WHERE name = :name');
$sth->execute([':name' => (string) $name]);
return $sth->fetchColumn();
}
}
И наконец, пример его использования:
$loader = new DatabaseTwigLoader($dbh);
$twig = new \Twig\Environment($loader);
echo $twig->render('index.twig', ['name' => 'Fabien']);
Использование различных источников шаблонов
Этот рецепт является продолжением предыдущего. Даже если вы храните шаблоны в базе данных, вы можете захотеть сохранить исходные/базовые шаблоны на файловой системе. Когда шаблоны можно загружать из разных источников, вам нужно использовать загрузчик \Twig\Loader\ChainLoader.
Как вы можете видеть в предыдущем рецепте, мы ссылаемся на шаблон точно так же, как и при использовании обычного загрузчика файловой системы. В этом и заключается ключ к возможности смешивать и сопоставлять шаблоны, полученные из базы данных, файловой системы или любого другого загрузчика: имя шаблона должно быть логическим именем, а не путем из файловой системы:
$loader1 = new DatabaseTwigLoader($dbh);
$loader2 = new \Twig\Loader\ArrayLoader([
'base.twig' => '{% block content %}{% endblock %}',
]);
$loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]);
$twig = new \Twig\Environment($loader);
echo $twig->render('index.twig', ['name' => 'Fabien']);
Теперь, когда шаблоны base.twig определены в загрузчике массивов, вы можете удалить их из базы данных, и всё остальное будет работать так же, как и раньше.
Загрузка шаблона из строки
Из шаблона вы можете загрузить шаблон, хранящийся в строке, с помощью функции template_from_string (доступна начиная с Twig 1.11 через расширение \Twig\Extension\StringLoaderExtension):
{{ include(template_from_string("Hello {{ name }}")) }}
Из PHP также возможно загрузить шаблон, хранящийся в строке, с помощью \Twig\Environment::createTemplate() (доступно начиная с Twig 1.18):
$template = $twig->createTemplate('hello {{ name }}');
echo $template->render(['name' => 'Fabien']);
Примечание
Никогда не используйте загрузчик Twig_Loader_String, который имеет серьёзные ограничения.
Использование Twig и AngularJS в одних и тех же шаблонах
Смешивание различных синтаксисов шаблонов в одном файле не рекомендуется, так как как AngularJS, так и Twig используют одинаковые разделители в своём синтаксисе: {{ и }}.
Тем не менее, если вы хотите использовать AngularJS и Twig в одном шаблоне, есть два способа сделать это в зависимости от объёма AngularJS, который вам необходимо включить в ваши шаблоны:
-
Обработка разделителей AngularJS путём обертывания разделов AngularJS тегом
{% verbatim %}или путём экранирования каждого разделителя с помощью{{ '{{' }}и{{ '}}' }}; -
Изменение разделителей одного из движков шаблонов (в зависимости от того, какой движок вы добавили последним):
-
Для AngularJS измените теги интерполяции, используя сервис
interpolateProvider, например, во время инициализации модуля:angular.module('myApp', []).config(function($interpolateProvider) { $interpolateProvider.startSymbol('{[').endSymbol(']}'); });
-
Для Twig измените разделители с помощью опции Lexer
tag_variable:$env->setLexer(new \Twig\Lexer($env, [ 'tag_variable' => ['{[', ']}'], ]));
-
© 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/1.x/recipes.html