Spec-Zone.ru › Twig 3

Рецепты

Отображение уведомлений о устаревании

Устаревшие функции генерируют уведомления об устаревании (через вызов функции 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(string $name): Source
    {
        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(string $name)
    {
        return $name === $this->getValue('name', $name);
    }

    public function getCacheKey(string $name): string
    {
        return $name;
    }

    public function isFresh(string $name, int $time): bool
    {
        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 (через расширение %%%CODE_BLOCK_67%%):

{{ include(template_from_string("Hello {{ name }}")) }}

Из PHP также можно загрузить шаблон, хранящийся в строке, с помощью %%%CODE_BLOCK_69%%:

$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/3.x/recipes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API