Рецепты
Отображение уведомлений о устаревших функциях
Устаревшие функции генерируют уведомления об устаревании (через вызов функции trigger_error() PHP). По умолчанию они отключены и никогда не отображаются и не регистрируются.
Чтобы удалить все устаревшие использования функций из ваших шаблонов, напишите и запустите скрипт по следующему образцу:
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
}
}
Примечание
Этот метод не поймает никаких нарушений политики песочницы, поскольку политика применяется во время рендеринга шаблона (так как 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\Environment, Twig использует его для посещения всех шаблонов, которые он компилирует. Если вам нужно сохранить некоторую информацию о состоянии, вы, вероятно, захотите сбросить ее при посещении нового шаблона.
Это можно сделать с помощью следующего кода:
protected $someTemplateState = [];
public function enterNode(\Twig\Node\Node $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
{
protected $dbh;
public function __construct(PDO $dbh)
{
$this->dbh = $dbh;
}
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);
}
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\Extension\StringLoaderExtension) :
{{ include(template_from_string("Hello {{ name }}")) }}
Из PHP также можно загрузить шаблон, хранящийся в строке, с помощью \Twig\Environment::createTemplate()) :
$template = $twig->createTemplate('hello {{ name }}');
echo $template->render(['name' => 'Fabien']);
Использование 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/2.x/recipes.html