Руководство по шаблонам Twig в Drupal

Данное руководство получилось довольно объемным, но разбивать его на несколько частей я не стал, как минимум по двум причинам: Это не статья для разового прочтения, а своего рода список готовых примеров, доступный в один клик, в котором можно быстро найти нужный пример, и использовать его в вашей задаче.

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

Содержание

Основы

В Drupal версии 8 и выше используется обработчик шаблонов Twig.

Шаблоны должны располагаться в папке templates вашей темы оформления, чтобы они могли быть автоматически обнаружены механизмом discovery.

Drupal имеет шаблоны по умолчанию, но они распределены по разным модулям. На эту "сырую" разметку можно посмотреть, включив тему оформления Stark.

Файлы шаблонов, непосредственно готовые к темизации, собраны в теме Stable 9. Эту тему (как и любую другую) можно указать как "базовую" для вашей темы оформления, и тогда все шаблоны этой темы унаследуются вашей темой, и будут автоматически обнаруживаться механизмом discovery, как если бы располагались в вашей теме.

Чтобы переопределить любой шаблон из базовой темы или шаблон по умолчанию из Drupal, нужно просто создать файл шаблона с таким же именем и поместить его в папку templates вашей темы. Шаблон из вашей темы будут иметь наивысший приоритет, затем шаблоны базовой темы, затем шаблоны ее базовой темы, и так далее вплоть до шаблона по умолчанию.

Имена файлов шаблонов

Файлы шаблонов имеют имя вида ELEMENT.html.twig.

Для некоторых элементов, помимо общего шаблона, могут быть определены и более специфичные, в зависимости каких-то параметров элемента. Например, для материала (node) могут быть определены разные шаблоны в зависимости от типа материала, режима отображения или идентификатора.

Формат имени файла будет вида ELEMENT--SOMETHING.html.twig. Разделителем параметров и слов в имени должны быть двойное и одинарное тире, соответственно.

Например, для материалов определены следующие имена: node--NODE-TYPE.html.twig, node--VIEW-MODE.html.twig и node--ID.html.twig (список не полный).

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

Отображение отладочной информации о шаблонах

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

Для этого в файл sites/development.services.yml, в раздел parameters нужно добавить следующее:

twig.config:
  debug: true
  auto_reload: true
  cache: false

Полное содержимое файла:

# Local development services.
#
# To activate this feature, follow the instructions at the top of the
# 'example.settings.local.php' file, which sits next to this file.
parameters:
  http.response.debug_cacheability_headers: true
  twig.config:
    debug: true
    auto_reload: true
    cache: false
services:
  cache.backend.null:
    class: Drupal\Core\Cache\NullBackendFactory

Также нужно убедиться что development.services.yml подключается в файле sites/SITENAME/settings.local.php а тот, в свою очередь, подключается в файле sites/SITENAME/settings.php (классический вариант подключения сервисов для разработки).

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

Отладочная информация о шаблонах

Документация.

Предложения для имен шаблонов по умолчанию

Формат имени файла для шаблонов может быть определен как на уровне ядра, так и расширен в модулях и темах оформления с помощью специальных функций — "хуков".

Например, для материалов, на уровне ядра определены следующие форматы имен шаблонов (template suggestions), в порядке убывания специфичности:

  • node--ID--VIEW-MODE.html.twig
  • node--ID.html.twig
  • node--NODE-TYPE--VIEW-MODE.html.twig
  • node--NODE-TYPE.html.twig
  • node--VIEW-MODE.html.twig
  • node.html.twig

Документация по именованию шаблонов.

Предложение собственных имен шаблонов

Для объявления собственного формата имени шаблона, нужно создать функцию в файле THEMENAME.theme вашей темы оформления, имплементирующую "хук" hook_theme_suggestions_HOOK_alter.

Например, для материалов, функция может быть вида:

/**
 * Implements hook_theme_suggestions_node_alter().
 */
function THEMENAME_theme_suggestions_node_alter(array &$suggestions, array $variables) {
  if (SOME-CONDITION) {
    $suggestions[] = 'node__' . 'SOMETHING';
  }
}

Разделителем параметров и слов в имени должны быть двойное и одинарное нижнее подчеркивание, соответственно.

В результате работы функции, к списку форматов имен шаблонов для элемента, удовлетворяющего условию, будет добавлен ваш вариант формата имени.

Документация.

Определение шаблона в модуле

Если нужно создать или переопределить шаблон в модуле, то просто файла шаблона в папке templates вашего модуля будет недостаточно. Необходимо также создать функцию, имплементирующую "хук" hook_theme в файле MODULENAME.module вашего модуля.

Например, функция для переопределения шаблона материала может быть вида:

/**
 * Implements hook_theme().
 */
function MODULENAME_theme() {
  return [
    'node__SOMETHING' => [
      'template' => 'node--SOMETHING',
      'base hook' => 'node',
    ],
  ];
}

Значения переменных в шаблоне

Для работы с шаблонами, очень часто бывает нужно узнать значения переменных в шаблоне до "отрисовки" (render), что получится после отрисовки мы увидим уже непосредственно в разметке элемента.

Для Drupal 9.4 и ниже это можно сделать с помощью функции dd. Предварительно нужно установить и включить модуль Twig Tweak — абсолютный "маст хэв" для работы с шаблонами.

Пример использования (в файле шаблона):

{{ dd(VARIABLE_NAME) }}

Начиная с Drupal 9.5 аналогичная функция присутствует в ядре:

{{ dump(VARIABLE_NAME) }}
Дамп значения переменной

Документация.

Список переменных доступных в шаблоне

Иногда бывает полезно узнать какие вообще переменные доступны в шаблоне. В комментарии вверху шаблона, как правило, приведены не все доступные переменные.

Два варианта вывода всех переменных шаблона:

{{ dd() }}
{{ dd(_context) }}

Для Drupal 9.5 и выше нужно соответственно использовать dump вместо dd.

Типы переменных в шаблонах

Переменные в шаблоне могут быть "отрисовываемые" (Renderable) и "не отрисовываемые" (Non-renderable).

Отрисовываемые переменные могут быть выведены на странице, либо в виде текста, либо в виде разметки.

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

{{ VARIABLE_NAME }}

К отрисовываемым переменным относятся:

  • логическое значение true, будет отображено как единица;
  • число;
  • строка;
  • объекты, имплементирующие MarkupInterface, например Markup и TranslatableMarkup;
  • рендер-массивы (render array или renderable array), их можно определить по наличию элемента с ключом #type или #theme;
  • массивы, которые содержат рендер-массивы.

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

Обработчик Twig толерантен к различного рода пустым значениям, и не выдает ошибки при попытке их вывести. К ним относятся:

  • пустые строки;
  • логическое значение false;
  • значение null;
  • пустые массивы;
  • массивы, не содержащие рендер-массивов (могут выдавать предупреждения).

К неотрисовываемым переменным относятся:

  • объекты не поддерживающие отрисовку (не имеющие метода __toString()).

Попытка вывести такой объект выдаст ошибку на белом экране.

Но некоторые свойства объектов вывести все же можно — c помощью специально определенных для них методов или метода value. Такие свойства, как правило, будут перечислены в массиве с ключом #values. Для множественных значений, массив #values будет определен для каждого элемента в массиве с ключом #list.

Абстрактные примеры:

{{ OBJECT.id }}
{{ OBJECT.PROPERTY.value }}
{{ OBJECT.PROPERTY.0.value }}

Передача собственных переменных в шаблон

Чтобы передать переменную в шаблон, нужно создать функцию в файле THEMENAME.theme вашей темы оформления, имплементирующую "хук" hook_preprocess_HOOK().

Например, для шаблона материала, функция может быть вида:

/**
 * Implements hook_preprocess_HOOK().
 */
function THEMENAME_preprocess_node(&$variables) {
  $variables['VARIABLE_NAME'] = VARIABLE_VALUE;
}

После чего переменная станет доступна в шаблоне node.html.twig.

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

Название элемента в отладочной информации

Атрибуты

Если переменная предназначена для вывода в разметке, например имя CSS класса или значение какого-то другого HTML атрибута, то в Drupal предусмотрен специальный механизм для работы с ними.

На уровне PHP кода существует класс Drupal\Core\Template\Attribute, который позволяет инициализировать объект атрибутов:

use Drupal\Core\Template\Attribute;

$attributes = new Attribute([
  'class' => ['class-1', 'class-2'],
]);

Если объект уже существует, с ним можно взаимодействовать как с массивом:

$attributes['class'][] = 'class-3';
$attributes['id'] = 'some-id';

Также в классе определено несколько методов для работы с атрибутами.

На уровне Twig шаблона объект атрибутов выводится как обычная переменная, но внутри тега:

<div{{ attributes }}>Some value</div>

Пример результата вывода разметки:

<div id="some-id" class="class-1 class-2 class-3">Some value</div>

Но самое главное, что в шаблоне доступны все методы для работы с атрибутами из PHP класса:

  • attributes.addClass('class-4', 'class-5') — добавляет одно или несколько значений классов;
  • attributes.removeClass('class-1', 'class-2') — удаляет одно или несколько значений классов;
  • attributes.hasClass('class-1') — проверяет наличие значения класса;
  • attributes.setAttribute('id', 'some-another-id') — добавляет либо заменяет значение одного аттрибута;
  • attributes.removeAttribute('id', 'class') — удаляет один или несколько атрибутов.

Методы можно объединять в цепочки:

<div{{ attributes.addClass('class-4', 'class-5').removeClass('class-1') }}>Some value</div>

Можно вывести только определенный атрибут:

<div class="{{ attributes.class }}">Some value</div>

Можно исключить определенные атрибуты с помощью фильтра without:

<div{{ attributes|without('class', 'id') }}>Some value</div>

Можно создать объект атрибутов прямо в Twig шаблоне:

{% set attributes = create_attribute() %}

Также возможно создать объект атрибутов сразу с начальными значениями:

{% set attributes = create_attribute({
  'id': 'some-id',
  'class': ['class-1', 'class-2'],
}) %}

Документация.

Шаблоны сущностей содержимого

В Drupal много различных шаблонов: глобальные шаблоны, шаблоны сущностей, шаблоны элементов и другие.

В этом разделе рассмотрим шаблоны для сущностей содержимого (content entities). К ним относятся:

  • материалы (node);
  • пользовательские блоки (custom block);
  • термины таксономи (taxonomy term);
  • медиа (media);
  • комментарии (comment);
  • профили пользователей (user);
  • параграфы (paragraph);
  • и другие сущности, которые поддерживают управление полями.

Основные файлы шаблонов для этих сущностей в теме Stable 9:

  • core/themes/stable9/templates/content/node.html.twig;
  • core/themes/stable9/templates/block/block.html.twig;
  • core/themes/stable9/templates/content/taxonomy-term.html.twig;
  • core/themes/stable9/templates/content/media.html.twig;
  • core/themes/stable9/templates/content/comment.html.twig;
  • core/themes/stable9/templates/user/user.html.twig;
  • PARAGRAPHS-MODULE/templates/paragraph.html.twig (определен в модуле Paragraphs).

Шаблоны этих сущностей очень похожи по своей структуре. Пример шаблона блока:

<div{{ attributes }}>
  {{ title_prefix }}
  {% if label %}
    <h2{{ title_attributes }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}
  {% block content %}
    {{ content }}
  {% endblock %}
</div>

Для шаблона определены ряд переменных (как правило, список основных переменных приводится в комментарии вверху шаблона), которые могут быть выведены или использованы в управляющих конструкциях. В шаблон сущности содержимого могут передаваться следующие виды переменных:

  • служебные переменные (например, attributes, view_mode, title_prefix, title_suffix);
  • значения базовых полей (label, date, и т. д.);
  • переменная content, которая представляет из себя массив с полями, созданными пользователем;
  • объект самой сущности (как правило, переменная есть, но она может никак не использоваться в шаблоне).

Не стоит путать переменную content с названием блока в конструкции {% block content %}, это разные вещи, подробнее про конструкцию block смотрите в разделе Шаблоны блоков.

Документация по функциям и возможностям Twig.

Переменная "content"

content — это самая главная переменная в шаблоне сущности, некоторые шаблоны вообще выводят только ее. content представляет из себя массив рендер-массивов полей сущности, готовых для отрисовки самым простым способом:

{{ content }}

Содержимое этого массива определяется полями, приведенными в списке на вкладке "Управление отображением" (Manage display), для текущего режима отображения (View mode). Порядок полей в списке определяет порядок вывода.

Соответственно, формат отображения каждого поля определяется "виждетом" (widget) и его настройкам для каждого поля, в том числе для отображения заголовка поля.

Вкладка "Управление отображением"

Если переместить поле в группу выключенных полей (Disabled), то для него не будет сформирован рендер-массив, и соответствующий элемент не будет присутствовать в массиве content.

Вывод полей сущности

Поля из переменной content можно вывести не только все разом, но и по одному:

{{ content.FIELD_NAME }}

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

<div class="field field--name-FIELD_NAME field--type-string field--label-above">
  <div class="field__label">Field title</div>
  <div class="field__item">Field value</div>
</div>

Также можно исключить какие-то поля из вывода с помощью фильтра without (чтобы, например, потом вывести их отдельно в другой части шаблона):

{{ content|without('FIELD1_NAME', 'FIELD2_NAME') }}

Это может быть полезно при создании нестандартных раскладок (custom layout), например:

<div class="cols">
  <div class="col-1">
    {{ content.FIELD1_NAME }}
    {{ content.FIELD2_NAME }}
  </div>
  <div class="col-2">
    {{ content.FIELD3_NAME }}
    {{ content|without('FIELD1_NAME', 'FIELD2_NAME', 'FIELD3_NAME') }}
  </div>
</div>

При этом, для каждого поля будет произведена полноценная отрисовка с использованием шаблона этого поля. Информация о том, какой шаблон использовать присутствует в самом рендер-массиве поля — элемент с ключом #theme, а непосредственно значение — в элементе с ключом 0 (либо другим числовым индексом, если поле множественное).

Если вам необходимо вывести только значение поля, без использования шаблона, то это можно сделать с помощью фильтра without:

<div class="field-wrapper">
  {{ content.FIELD_NAME|without('#theme') }}
</div>

Данный способ является основным и оптимальным, с точки зрения производительности и безопасности, способом вывода значения поля в произвольной разметке. С его помощью можно выводить значения полей любых типов, как простых, так и ссылающихся на другие сущности (изображения, медиа, термины таксономии, параграфы и др.). Пример результата вывода:

<div class="field-wrapper">
  Field value
</div>

Если поле множественное, то значения нужно проитерировать. Конструкция не самая элегантная, выглядит так:

<div class="field-wrapper">
  {% for key, item in content.FIELD_NAME if key|first != '#' %}
    <div class="field-item-wrapper">
      {{ item }}
    </div>
  {% endfor %}
</div>

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

В модуле Twig Tweak есть фильтр children, упрощающий приведенную выше запись:

<div class="field-wrapper">
  {% for item in content.FIELD_NAME|children %}
    <div class="field-item-wrapper">
      {{ item }}
    </div>
  {% endfor %}
</div>

Пример результата вывода:

<div class="field-wrapper">
  <div class="field-item-wrapper">
    Field 1 value
  </div>
  <div class="field-item-wrapper">
    Field 2 value
  </div>
</div>

Если в разметке нужно отразить порядковый номер элемента и/или их количество, то запись будет вида:

<div class="field-wrapper items--{{ content.FIELD_NAME['#items']|length }}">
  {% for key, item in content.FIELD_NAME if key|first != '#' %}
    <div class="field-item-wrapper item--{{ key + 1 }}">
      {{ item }}
    </div>
  {% endfor %}
</div>

При выводе значений поля без шаблона, настройки виджета, по-прежнему, будут учтены, а вот настройки заголовка — нет. Пример вывода заголовка:

<div class="field-title">
  {{ content.FIELD_NAME['#title'] }}
</div>

Тут важно отметить, что значения полей в переменной content, даже, если они выводятся без шаблона поля, все равно остаются в формате рендер-массива, соответственно проходили всю необходимую обработку и отрисовываюся обработчиком Twig без каких либо дальнейших преобразований. В частности, если в значении поля присутствует HTML разметка, Twig не будет ее экранировать.

Саму переменную content можно использовать в управляющих конструкциях (так как это обычный массив), а вот использование в управляющих конструкциях рендер-массивов полей может привести к неожиданным результатам. Даже если для поля не заданно значение, рендер-массив для него все равно создается и содержит служебные данные. Так, например, следующие условия будут всегда истинными, независимо от наличия значения:

{% if content.FIELD_NAME %}

{% if not content.FIELD_NAME is empty %}

В случае с использованием шаблонов полей, о необходимости вывода разметки для пустых полей заботится шаблон поля, но при выводе непосредственно значений, проверка наличия значения бывает очень полезна. Правильный способ это сделать приведен в разделе Методы объекта сущности.

Переменная объекта сущности

Еще одна очень важная переменная в шаблоне сущности — переменная содержащая непосредственно объект сущности.

Такая переменная присутствует практически в каждом шаблоне сущности, но называется по-разному, в зависимости от типа сущности:

  • node.html.twignode;
  • taxonomy-term.html.twigterm;
  • media.html.twigmedia;
  • comment.html.twigcomment;
  • user.html.twiguser;
  • paragraph.html.twigparagraph.

С блоками ситуация чуть сложнее. Пользовательский блок (Custom block), а именно такие блоки можно создавать через интерфейс администратора, это всего лишь один из возможных "плагинов" системы блоков. Помимо них в Drupal существуют и другие "плагины", например, блок основного содержимого страницы (Main page content), блок заголовка страницы (Page title), блоки меню и т. д. Такие блоки не являются сущностями содержимого, а пользовательский блок — является. И несмотря на то, что основной шаблон у всех блоков общий — block.html.twig, объект сущности будет доступен только в шаблоне пользовательского блока, он будет представлен элементом #block_content в массиве content.

Для удобства использования объекта сущности блока, можно присвоить ему более короткое имя прямо в шаблоне конкретного пользовательского блока:

{% set block_content = content['#block_content'] %}

Или определить переменную объекта сущности для всех блоков на стадии препроцесса:

/**
 * Implements hook_preprocess_block().
 */
function THEMENAME_preprocess_block(&$variables) {
  // Pass block object shortcut to template.
  if (isset($variables['content']['#block_content'])) {
    $variables['block_content'] = $variables['content']['#block_content'];
  }
}

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

Методы объекта сущности

Объект сущности в шаблоне поддерживает некоторые методы, определенные для него в PHP классе. А конкретно, это методы начинающиеся с get, has или is, а также следующие "базовые" методы:

  • bundle() — возвращает машинное имя "бандла" или, по простому, типа содержимого (для терминов таксономии — это машинное имя словаря, для пользователей — всегда user);
  • id() — возвращает идентификатор содержимого;
  • label() — возвращает заголовок или название содержимого.

Пример использования метода в шаблоне (здесь и далее все примеры работы с объектом сущности будут приведены для материала):

node.bundle()

В большинстве случаев, скобки после "базовых" методов можно опускать, но бывают и исключения, поэтому для надежности я все же рекомендую указывать.

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

  • isPublished() — возвращает статус публикации;
  • hasField('FIELD_NAME') — проверяет определено ли поле (не значение) для данного типа содержимого;
  • getCreatedTime() возвращает Timestamp даты и времени создания;
  • getOwnerId() — возвращает идентификатор пользователя создавшего сущность.

Также объект сущности поддерживает обращение к объекту поля с помощью метода get и напрямую по имени.

node.get('FIELD_NAME')
node.FIELD_NAME

Проверить наличие значение поля можно с помощью метода isEmpty объекта поля. Пример использования:

{% if not node.FIELD_NAME.isEmpty %}
  <div class="field-wrapper">
    {{ content.FIELD_NAME|without('#theme') }}
  </div>
{% endif %}

Значения полей в объекте сущности

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

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

Поэтому правильнее, в первую очередь, всегда стараться использовать значение из переменной content, и только если это по каким-то причинам невозможно — обращаться к объекту сущности.

Кстати, способ вывести сырое значение с сохранением разметки все же есть, он приведен в примере вывода значения для форматированного текста.

Случаи, когда использование сырых значений может иметь смысл:

  • когда переменная content не доступна;
  • когда требуется самостоятельно сформировать разметку (например, для ссылки или изображения);
  • в управляющих конструкциях, именах CSS классов и разметке;
  • когда нужно вывести значение поля несколько раз, по-разному;
  • когда нужно вывести перевод или значение на нескольких языках;
  • когда необходимо узнать значение поля дочерней сущности в шаблоне родительской сущности.

Если сырое значение само по себе вам не нужно, а нужно просто вывести поле с какими-то особыми настройками виджета, то можно воспользоваться фильтром view (параметры не обязательны) или функцией drupal_field из модуля Twig Tweak. Поле будет выведено с использованием шаблона поля, как если бы оно выводилось из переменной content, но фактически рендер-массив будет сформирован заново из объекта сущности.

{{ node.FIELD_NAME|view('VIEW_MODE', 'LANGCODE') }}

{{ drupal_field('FIELD_NAME', 'node', node.id(), 'VIEW_MODE') }}

Руководство по возможностям модуля Twig Tweak.

Поля сущности можно условно разделить на два вида:

  • Простое поле (текст, число, список, дата, ссылка) — содержит непосредственно значение.
  • Ссылка на сущность (файл, изображение, медиа, материал, термин таксономии и др.) — указывает на сущность, поля которой содержат значения (или другие ссылки на сущности).

Значения простых полей

Получение и вывод сырого значения для большинства простых полей:

<div class="field-wrapper">
  {{ node.FIELD_NAME.value }}
</div>

Вывод сырого значения множественного поля:

<div class="field-wrapper">
  {% for item in node.FIELD_NAME %}
    <div class="field-item-wrapper">
      {{ item.value }}
    </div>
  {% endfor %}
</div>

Вариант, с использованием порядкового номера значения:

<div class="field-wrapper items--{{ node.FIELD_NAME|length }}">
  {% for key, item in node.FIELD_NAME %}
    <div class="field-item-wrapper item--{{ key + 1 }}">
      {{ node.FIELD_NAME[key].value }}
    </div>
  {% endfor %}
</div>

Далее будут приведены особенности вывода простых полей.

Простой текст, Email, Номер телефона

Text (plain), Email, Telephone number

Особенностей нет.

Примеры вывода email и телефона в виде ссылки:

<a href="mailto:{{ node.field_email.value }}">{{ node.field_email.value }}</a>

<a href="tel:{{ node.field_telephone_number.value }}">{{ node.field_telephone_number.value }}</a>

Длинный простой текст

Text (plain, long)

Длинный текст поддерживает переносы строк, которые при попытке вывода обычным способом будут удалены обработчиком Twig. Чтобы этого избежать нужно пропустить значение через обработчик текста, для чего нужно подготовить значение в виде рендер-массива:

{% set rendered_text = {
  '#type': 'processed_text',
  '#text':  node.field_text_plain_long.value,
  '#format': 'plain_text',
} %}

{{ rendered_text }}

Форматированный текст, Длинный форматированный текст

Text (formatted), Text (formatted, long)

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

{% set rendered_text = {
  '#type': 'processed_text',
  '#text': node.field_text_formatted.value,
  '#format': node.field_text_formatted.format,
} %}

{{ rendered_text }}

Длинный форматированный текст с анонсом

Text (formatted, long, with summary)

Форматированный текст с анонсом имеет два текстовых поля — непосредственно текст и анонс в формате длинного простого текста. Пример вывода:

{% set rendered_summary = {
  '#type': 'processed_text',
  '#text':  node.field_text_formatted_summary.summary,
  '#format': 'plain_text',
} %}
{% set rendered_text = {
  '#type': 'processed_text',
  '#text': node.field_text_formatted_summary.value,
  '#format': node.field_text_formatted_summary.format,
} %}

{{ rendered_summary }}
{{ rendered_text }}

Целое число, Десятичное число, Вещественное число

Number (integer), Number (decimal), Number (float)

Пример вывода значения с префиксом и суффиксом:

{% set number_prefix = node.field_number.getSetting('prefix') %}
{% set number_suffix = node.field_number.getSetting('suffix') %}

{{ number_prefix }}{{ node.field_number.value }}{{ number_suffix }}

Drupal по-разному хранит значения разных типов числовых полей (что может быть важно при использовании в управляющих конструкциях):

  • Целое хранится в виде числа со знаком, минимальное значение -2147483647, максимальное — 2147483647;
  • Десятичное хранится в виде числа с двумя знаками после точки, минимальное значение -4194303.99, максимальное — 4194303.99. Примеры: 3.14, 24.00, -256.00;
  • Вещественное число хранится в виде числа c плавающей точкой. Значение, которое в итоге будет сохранено зависит от точности, но в общем случае правило такое: значение должно быть не длиннее семи знаков включая точку. Если значение длиннее семи знаков, и точки нет вообще, то все знаки после шестого должны быть нулями. Примеры: 3.14159, 2.71828, 234.544, 4894860, 489489, -5555.56.

Teкстовый список, Список целых чисел, Список вещественных чисел

List (text), List (integer), List (float)

В качестве значения, которое Drupal сохраняет в базу данных используется ключ. Пример вывода ключа и метки:

{% set list_item_key = node.field_list.value %}
{% set list_item_label = node.field_list.getSetting('allowed_values')[list_item_key] %}

{{ list_item_key }}
{{ list_item_label }}

В случае с множественным списком есть небольшая особенность — метки лучше получать до итерации значений:

{% set list_labels = node.field_list.getSetting('allowed_values') %}
{% for list_item in node.field_list %}
  {% set list_item_key = list_item.value %}
  {% set list_item_label = list_labels[list_item_key] %}

  {{ list_item_key }}
  {{ list_item_label }}
{% endfor %}

При использовании числовых списков, ключи должны соответствовать типу числового поля, иначе значение поля будет скорректировано при сохранении и не будет соответствовать ключу, определенному в настройках поля. Соответственно, значение не будет проставлено по умолчанию при следующем открытии страницы редактирования сущности, и получить по нему метку не получится.

Логическое значение

Boolean

По структуре логическое поле является частным случаем поля типа "список", с двумя фиксированными ключами (0 и 1) и двумя определяемыми для них метками.

{% set boolean_value = node.field_boolean.value %}
{% set boolean_label = boolean_value ? node.field_boolean.getSetting('on_label') : node.field_boolean.getSetting('off_label') %}

{{ boolean_value }}
{{ boolean_label }}

Множественное логическое поле лишь позволяет выбрать сразу оба значения. Множественные значения выводятся аналогично списку.

Дата

Date

Значения поля типа "дата" хранится в формате "HTML Datetime" (имеет вид 0000-00-00T00:00:00) или "HTML Date" (0000-00-00), в зависимости от выбранного типа хранилища.

Пример вывода значения в таком формате:

{{ node.field_date.value }}

Правильный способ вывести дату в другом формате — использовать переменную content и настройки виджета поля, но если этот вариант недоступен, то способ тоже есть, даже несколько.

Вывод с помощью Twig фильтра date:

{{ node.field_date.value|date('d.m.Y H:i:s') }}

Фильтр принимает различные форматы на вход и отдает строку в соответствии с заданным шаблоном (в формате даты PHP), аналогично тому, как задаются форматы дат в административном интерфейсе Drupal. Документация по фильтру, документация по форматам даты PHP.

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

{{ node.field_date.value|date('d.m.Y H:i:s', 'Europe/Copenhagen') }}

Недостатком фильтра можно назвать вывод названий месяцев на языке, определенном в PHP по умолчанию, а не на языке страницы.

Вывод с помощью формата даты определенного в Drupal:

{{ node.field_date.value|date('U')|format_date('FORMAT_NAME') }}

Так как фильтр format_date принимает на вход дату в формате "Timestamp", значение нужно сначала пропустить через фильтр date, который вернет Timestamp.

Альтернативный вариант — получить Timestamp из объекта DrupalDateTime:

{{ node.field_date.date.getTimestamp()|format_date('FORMAT_NAME') }}

В случае с использованием формата даты Drupal, названия месяцев будут выведены на языке страницы.

Диапазон дат

Date range

Этот тип поля представляет из себя расширенный вариант поля дата, основное отличие — значений тут всегда по два: value и end_value. Также поле поддерживает еще один тип хранилища — "весь день" (All day), в котором даты хранятся в формате "HTML Datetime".

Пример получения значений:

{{ node.field_date_range.value }}
{{ node.field_date_range.end_value }}

Пример получения Timestamp из объектов DrupalDateTime:

{{ node.field_date_range.start_date.getTimestamp() }}
{{ node.field_date_range.end_date.getTimestamp() }}

Значения в нужном формате выводятся аналогично простому полю типа "дата".

Timestamp

Этот тип поля является частным случаем поля дата, который хранит значения в формате Timestamp. Вывести значение в нужном формате даже проще:

{{ node.field_timestamp.value|date('d.m.Y H:i:s') }}

{{ node.field_timestamp.value|format_date('FORMAT_NAME') }}

Кстати, системные даты и время (дата создания сущности, дата обновления сущности и т. д.) хранятся именно в таком формате. Два способа получения этих значений:

{{ node.created.value }}
{{ node.changed.value }}

{{ node.getCreatedTime() }}
{{ node.getChangedTime() }}

Link

Поле типа "ссылка" поддерживает различные виды ссылок и соответственно различные форматы их хранения (вернее формат один — URI, но он имеет разные виды):

  • ссылка на сущность, хранится в виде entity:ENTITY_TYPE/ENTITY_ID;
  • ссылка по пути или синониму (может быть с якорем и/или параметрами), хранится в виде internal:PATH;
  • ссылка по "роуту" (route), хранится в виде route:ROUTE_NAME;
  • внешняя ссылка, хранится в виде обычного URL вида https://example.com/test.

Пример получения этого значения:

{{ node.field_link.uri }}

Также поле предусматривает значение для метки, пример получения:

{{ node.field_link.title }}

Правильный способ вывести разметку ссылки — использовать функцию link, она как раз принимает параметр в формате URI, а также метку и необязательный объект с атрибутами. Примеры:

{{ link(node.field_link.title, node.field_link.uri, { 'class': ['CLASS-NAME'] }) }}
{{ link(node.field_link.title, node.field_link.uri, { 'target': '_blank' }) }}

Помимо URI, функция link принимает также объект Drupal\Core\Url, в этом случае будут учтены и параметры объекта. Пример:

{{ link(node.field_link.title, node.field_link.0.url) }}

Функция link корректно обрабатывает служебные пути <nolink> и <button>, и генерирует разметку, используя теги <span> и <button> соответственно.

Если метка для ссылки не является обязательной, то лучше добавить соответствующую проверку и предоставить альтернативное значение, иначе, при пустом значении метки, результат вывода не будет виден на странице. Примеры:

{{ link('LINK LABEL'|t, node.field_link.uri) }}

{% set link_url = node.field_link.0.url %}
{% set link_label = node.field_link.title ? node.field_link.title : link_url.toString() %}
{{ link(link_label, link_url) }}

В качестве метки функция принимает только строки, что делает этот способ неподходящим, когда вам нужно добавить какую-то разметку внутрь тега <a>, например изображение или иконку. В этом случае, разметку можно прописать прямо в шаблоне, но позаботится о тегах придется самостоятельно.

Для формирования разметки понадобится значение ссылки в виде строки:

{{ node.field_link.0.url.toString() }}

Также его можно получить с помощью фильтра file_url, из модуля Twig Tweak. Пусть вас не смущает его название, в данном случае фильтр будет корректно преобразовывать любые URI.

{{ node.field_link|file_url }}

Простой пример использования:

<a href="{{ node.field_link.0.url.toString() }}">{{ node.field_link.title }}<a>

Продвинутый пример с проверками:

{% set link_route = not node.field_link.0.url.external ? node.field_link.0.url.routeName : null %}
{% if link_route == '<nolink>' %}
  {% set link_alt_tag = 'span' %}
{% elseif link_route == '<button>' %}
  {% set link_alt_tag = 'button' %}
{% else %}
  {% set link_alt_tag = null %}
{% endif %}
{% set link_url = link_alt_tag ? null : node.field_link.0.url.toString() %}
{% set link_alt_title = link_url ? link_url : 'Link'|t %}
{% set link_title = node.field_link.title ? node.field_link.title : link_alt_title %}
{% set link_attributes = create_attribute() %}
{% if link_url %}
  {% set link_attributes = link_attributes.setAttribute('href', link_url) %}
{% endif %}
{% set link_tag = link_alt_tag ? link_alt_tag : 'a' %}

<{{link_tag}}{{ link_attributes }}>{{ link_title }}</{{link_tag}}>

Если изменить нужно не разметку а именно URL ссылки (например, добавить GET параметр) то можно использовать функции path или url. Обе принимают в качестве аргументов имя "роута" (Route), параметры роута и объект свойств ссылки (соответственно, работают только с внутренними путями). Функция path выводит относительный URL, а url — абсолютный.

Пример добавления параметра destination:

<a href="{{ path(node.field_link.0.url.routeName, node.field_link.0.url.routeParameters, { 'query': { 'destination': path('<current>') } }) }}">{{ node.field_link.title }}</a>

Пример вывода абсолютного URL:

<a href="{{ url(node.field_link.0.url.routeName, node.field_link.0.url.routeParameters) }}">{{ node.field_link.title }}</a>

Документация по функциям Drupal, доступным в Twig шаблонах.

Значения "ссылающихся" полей (reference field)

Значением ссылающегося поля является идентификатор "целевой" сущности. Пример получения и вывода этого значения для всех ссылающихся полей:

{{ node.REFERENCE_FIELD_NAME.target_id }}

Пример вывода значения множественного поля:

<div class="field-wrapper">
  {% for item in node.REFERENCE_FIELD_NAME %}
    <div class="field-item-wrapper">
      {{ item.target_id }}
    </div>
  {% endfor %}
</div>

Альтернативный вариант, с использованием порядкового номера значения:

<div class="field-wrapper items--{{ node.REFERENCE_FIELD_NAME|length }}">
  {% for key, item in node.REFERENCE_FIELD_NAME %}
    <div class="field-item-wrapper item--{{ key + 1 }}">
      {{ node.node.REFERENCE_FIELD_NAME[key].target_id }}
    </div>
  {% endfor %}
</div>

Но как правило, вывести нужно не идентификатор, а саму целевую сущность или значение какого-то ее поля. Для этого нужно сначала получить объект целевой сущности с помощью метода entity.

Из коробки, в Drupal нет возможности по-простому вывести сущность целиком, но это можно сделать с помощью фильтра view из модуля Twig Tweak (параметры не обязательны):

{{ node.REFERENCE_FIELD_NAME.entity|view('VIEW_MODE', 'LANGCODE') }}

Если вы заранее знаете тип целевой сущности, то ее можно вывести с помощью функции drupal_entity (из модуля Twig Tweak) даже не загружая объект сущности:

{{ drupal_entity('ENTITY_TYPE', node.REFERENCE_FIELD_NAME.target_id, 'VIEW_MODE') }}

Для целевой сущности поддерживаются все те же методы, что и для родительской, включая базовые label(), bundle() и id() (значение последнего проще получить через target_id, не загружая целевую сущность). Пример:

{{ node.REFERENCE_FIELD_NAME.entity.label() }}

Если целевая сущность имеет какое-либо представление в виде страницы (выражаясь технически — имплементирует метод toUrl()), то с помощью фильтров entity_url и entity_link из модуля Twig Tweak, можно быстро получить URL этой страницы или сразу готовую ссылку:

{{ node.REFERENCE_FIELD_NAME.entity|entity_url }}

{{ node.REFERENCE_FIELD_NAME.entity|entity_link }}

Значения полей целевой сущности выводятся точно так же, как это делается для родительской сущности:

{{ node.REFERENCE_FIELD_NAME.entity.FIELD_NAME.value }}

Соответственно, все примеры работы с полями родительской сущности справедливы и для целевых.

В том числе, можно использовать фильтр view (параметры не обязательны) и функцию drupal_field из модуля Twig Tweak, которые в случае с полями целевых сущностей, становятся гораздо полезней, так как для целевых сущностей переменная content недоступна.

{{ node.REFERENCE_FIELD_NAME.entity.FIELD_NAME|view('VIEW_MODE', 'LANGCODE') }}

{{ drupal_field('FIELD_NAME', 'TARGET_ENTITY_TYPE', node.REFERENCE_FIELD_NAME.target_id, 'VIEW_MODE') }}

Далее будут приведены особенности вывода данных для целевых сущностей разного типа.

Ссылка на файл, изображение

File, Image references

Ссылка на Файл и Изображение — это особые поля. Метаданные хранятся в самом поле, а значения относящиеся непосредственно к файлу — в объекте сущности.

Для поля типа "ссылка на файл" могут быть определены флаг необходимости отображения файла и описание.

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

Пример получения этих значений:

{{ node.field_file.display }}
{{ node.field_file.description }}

{{ node.field_image.alt }}
{{ node.field_image.title }}
{{ node.field_image.width }}
{{ node.field_image.height }}

Оба типа поля ссылаются на тип сущности Файл. Пример получения информации о имени, размере и Mime-типе файла:

{{ node.field_file.entity.filename.value }}
{{ node.field_file.entity.filesize.value }}
{{ node.field_file.entity.filemime.value }}

Несколько способов получить URI файла (последний из Twig Tweak):

{{ node.field_file.entity.uri.value }}
{{ node.field_file.entity.fileuri }}
{{ node.field_file|file_uri }}

Несколько способов получить URL файла (последние два из Twig Tweak):

{{ file_url(node.field_file.entity.fileuri) }}
{{ node.field_file.entity.fileuri|file_url }}
{{ node.field_file|file_url }}

Получить URL изображения определенного стиля (Image style) можно с помощью фильтра image_style из модуля Twig Tweak, в качестве аргумента фильтр принимает URI:

{{ node.field_image.entity.uri.value|image_style('IMAGE_STYLE') }}
{{ node.field_image|file_uri|image_style('IMAGE_STYLE') }}

Чтобы просто вывести изображение в виде разметки (тега <img>), проще и правильнее было бы воспользоваться значением из переменной content или возможностями модуля Twig Tweak: фильтром view или функцией drupal_field. Для этого даже не нужно обращаться к целевой сущности. Drupal сгенерирует разметку со всеми необходимыми атрибутами в соответствии с настройками виджета поля.

{{ content.field_image|without('#theme') }}
{{ node.field_image|view('VIEW_MODE')|without('#theme') }}
{{ drupal_field('field_image', 'node', node.id, 'VIEW_MODE')|without('#theme') }}

Чтобы внести изменения в разметку самого тега <img> или значения атрибутов, придется сгенерировать разметку самостоятельно. Простой пример без проверок значений:

{% set image_attributes = create_attribute({
  'src': file_url(node.field_image.entity.fileuri),
  'alt': node.field_image.alt,
  'title': node.field_image.title,
  'width': node.field_image.width,
  'height': node.field_image.height,
  'loading': 'lazy'
}) %}

<img{{ image_attributes }}>

Пример вывода изображения определенного стиля:

{% set image_attributes = create_attribute({
  'src': node.field_image|file_uri|image_style('IMAGE_STYLE'),
  'alt': node.field_image.alt,
  'title': node.field_image.title,
  'width': 480,
  'height': 320,
  'loading': 'lazy'
}) %}

<img{{ image_attributes }}>

Также вывести изображение в виде разметки можно с помощью функции drupal_image из модуля Twig Tweak. Функция может работать с разными типами параметров для определения изображения и позволяет, при необходимости, передавать атрибуты для тега <img>, (документация):

{{ drupal_image(node.field_image|file_uri, '', {
  alt: node.field_image.alt,
  title: node.field_image.title,
  width: node.field_image.width,
  height: node.field_image.height,
  loading: 'lazy'
}) }}

{{ drupal_image(node.field_image|file_uri, 'IMAGE_STYLE', {
  alt: node.field_image.alt,
  title: node.field_image.title,
  width: 480,
  height: 320,
  loading: 'lazy'
}) }}

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

Еще одна возможность функции drupal_image — вывести адаптивное стилизованное изображение (Responsive image style). Сформировать для него разметку самостоятельно было бы не просто:

{{ drupal_image(node.field_image.entity.fileuri, 'RESPONSIVE_IMAGE_STYLE', responsive=true) }}

Пример использования URL изображения в качестве значения CSS свойства background-image:

{% if not node.field_image.isEmpty %}
  {% set image_url = file_url(node.field_image.entity.fileuri) %}
{% endif %}

{% set image_attributes = create_attribute() %}
{% if image_url %}
  {% set image_attributes = image_attributes.setAttribute('style', 'background-image:url(' ~ image_url ~ ');') %}
{% endif %}

<div{{ image_attributes.addClass('wrapper-class') }}></div>

Для стилизованного изображения URL будет, соответственно:

{% set image_url = node.field_image|file_uri|image_style('IMAGE_STYLE') %}

Для разметки значения поля типа "ссылка на файл" может пригодится самая разная информация, пример использования:

{% set file_url = file_url(node.field_file.entity.fileuri) %}
{% set file_name = node.field_file.entity.filename.value %}
{% set file_extention = file_name|split('.')|last %}
{% set file_description = node.field_file.description %}
{% set file_size = node.field_file.entity.filesize.value %}
{% set file_size_formatted = '@size KB'|t({ '@size': (file_size / 1024)|round(2) }) %}

<div><a href="{{ file_url }}" download="{{ file_name }}">{{ file_description }}</a> ({{ file_extention }}, {{ file_size_formatted }})</div>

Форматированное значение размера файла можно получить с помощью Фильтра format_size из модуля Twig Tweak (размерность подбирается автоматически):

{% set file_size_formatted = file_size|format_size %}

Ссылка на Медиа

Media references

В Медиа типов "Аудио", "Видео", "Документ" и "Изображение" сами ресурсы хранятся с помощью стандартных полей типа "Ссылка на файл" и "Ссылка на изображение", работа с которыми описана выше. Медиа является своего рода оберткой для этих полей, унифицирующей работу с ресурсами. В Медиа есть дополнительные данные о типе ресурса (он же "плагин"), благодаря которым Drupal предоставляет соответствующие виджеты для отображения поля, но с точки зрения сырых значений, ничего нового там нет.

Получить тип самой сущности типа "медиа" можно стандартным способом:

{{ node.field_media_multiple.entity.bundle() }}

Получить тип непосредственно ресурса можно из объекта Source:

{{ node.field_media_multiple.entity.getSource().getPluginId() }}

URL ресурса, независимо от его типа, можно получить с помощью фильтра file_url из модуля Twig Tweak:

{{ node.field_media_multiple.entity|file_url }}

В отличие от типов медиа упомянутых выше, в медиа типа "Внешнее видео" (Remote video) ресурсы хранятся с помощью поля типа "простой текст". Значение обрабатывается с помощью плагина "oEmbed", который также часто используется в типах медиа предоставленных "контриб" (Contrib) модулями. В этом случае, данные из медиа могут быть очень полезны.

Пример получения метаданных медиа из шаблона материала (в случае использования в шаблоне сущности типа "медиа", переменная media уже будет содержать объект сущности):

{% set media = node.field_media.entity %}
{% set media_source = media.getSource() %}

{{ media_source.getMetadata(media, 'ATTRIBUTE_NAME') }}

Получение списка всех доступных атрибутов:

{{ media_source.getMetadataAttributes() }}

Наиболее полезные атрибуты:

{{ media_source.getMetadata(media, 'title') }}
{{ media_source.getMetadata(media, 'author_name') }}
{{ media_source.getMetadata(media, 'author_url') }}
{{ media_source.getMetadata(media, 'provider_name') }}
{{ media_source.getMetadata(media, 'thumbnail_uri') }}
{{ media_source.getMetadata(media, 'thumbnail_width') }}
{{ media_source.getMetadata(media, 'thumbnail_height') }}
{{ media_source.getMetadata(media, 'html') }}

В случае с внешним видео часто бывает так, что обложка для видео либо плохого качества, либо отсутствует. Соответственно возникает необходимость использовать в качестве обложки изображение из пользовательского поля. Сложность может возникнуть с запуском видео по клику по обложке видео, так как "айфрейм" (iframe) ресурса будет внутри айфрейма oEmbed, и контролировать айфрейм ресурса с помощью JavaScript не получится.

Простой пример разметки, работающий с медиа обоих типов: "внешнее видео" и "видео" (файл):

{% if not node.filed_media.isEmpty %}
  {% set media = node.field_media.entity %}
  {% set media_source = media.getSource() %}
  {% set video_plugin = media_source.getPluginId() %}
  {% set video_provider = media_source.getMetadata(media, 'provider_name') ? media_source.getMetadata(media, 'provider_name') : 'file' %}
  {% set video_classess = [
    'video__video',
    'video__video--plugin-' ~ video_plugin|clean_class,
    'video__video--provider-' ~ video_provider|clean_class,
  ] %}

  <div class="video">
    <div{{ create_attribute().addClass(video_classess) }}>
      {% if video_plugin == 'oembed:video' %}
        {{ media_source.getMetadata(media, 'html')|raw }}
      {% else %}
        {{ media.field_media_video_file|view|without('#theme') }}
      {% endif %}
    </div>

    {% if not node.field_image.isEmpty %}
      <div class="video__poster">
        {{ content.field_image|without('#theme') }}
        <button class="video__play-button">{{ 'Play'|t }}</button>
      </div>
    {% endif %}
  </div>
{% endif %}

Для отображения разметки поля типа "ссылка на файл" из медиа типа "видео" в виде тега <video>, был использован фильтр view из модуля Twig Tweak, но аналогичного результата можно добиться и без него. Можно оставить отрисовку поля field_media_video_file в шаблоне сущности медиа:

{{ content.field_media_video_file|without('#theme') }}

А в шаблоне материала вывести значение поля field_media из переменной content:

{{ content.field_media|without('#theme') }}

Простой вариант JavaScript обработчика клика по кнопке "Play" для приведенной выше разметки, без использования API видеохостингов:

(function ($, Drupal, drupalSettings) {
  'use strict';

  Drupal.behaviors.LIBRARYNAMEFunctions = {
    attach: function(context, settings) {

      // Handling media video playback starting when custom poster is used.
      $(context).find('.video__play-button').click(function() {
        var videoWrapper = $(this).closest('.video').find('.video__video');

        // Getting plugin type and video element.
        if (videoWrapper.hasClass('video__video--plugin-video-file')) {
          var plugin = 'file';
          var video = videoWrapper.find('video')[0];
        }
        else if (videoWrapper.hasClass('video__video--plugin-oembedvideo')) {
          var plugin = 'oembed';
          var video = videoWrapper.find('iframe')[0];
        }

        // Removing poster.
        $(this).closest('.video__poster').remove();

        // Starting playback.
        if (plugin && plugin === 'file') {
          video.play();
        }
        else if (plugin && plugin === 'oembed') {
          video.src += "&autoplay=1";
        }
      });

    }
  };
}) (jQuery, Drupal, drupalSettings);

Ссылка на Материал, Термин таксономии, Пользователя

Node (Content), Taxonomy term, User references

Поля, ссылающиеся на различные типы сущностей (включая медиа), фактически используют общий тип поля — "Ссылка на сущность".

Целевая сущность ничем не отличается от родительской с точки зрения работы с сырыми значениями полей. А вот получить доступ к обработанным значениям полей целевой сущности (как в переменной content) из шаблона родительской не получится.

Если значения полей целевой сущности не участвуют в логике работы шаблона родительской сущности, а должны быть просто выведены, то правильней было бы оставить отрисовку целевой сущности шаблону самой сущности (то есть вывести значение ссылающегося поля из переменной content родительской сущности).

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

Простой пример использования обоих подходов — вывод целевых сущностей во вкладках. Для меток вкладок и идентификаторов используются значения из объекта сущности, а для содержимого вкладок — значения полей, отрисованные с использованием шаблона целевой сущности:

<div class="tabs">
  <div class="tabs__list">
    {% for object_item in node.REFERENCE_FIELD_NAME %}
      <div class="tabs__list-item" data-target="tabs__content-item--{{ object_item.target_id }}">
        {{ object_item.entity.label() }}
      </div>
    {% endfor %}
  </div>

  <div class="tabs__content">
    {% for content_item_key, content_item in content.REFERENCE_FIELD_NAME if content_item_key|first != '#' %}
      <div class="tabs__content-item tabs__content-item--{{ node.REFERENCE_FIELD_NAME[content_item_key].target_id }}">
        {{ content_item }}
      </div>
    {% endfor %}
  </div>
</div>

Еще один пример — генерация разметки для слайдера или карусели, где слайды — это содержимое сущности, а элементы управления слайдером должны быть представлены каким-либо значением этой сущности (изображением или другим параметром).

Ссылка на Параграф

Paragraph reference

Поле типа "Ссылка на ревизию сущности", примером использования которого является ссылка на параграф, помимо идентификатора целевой сущности, хранит также идентификатор ревизии. Новая ревизия для параграфов создается каждый раз, когда изменяется родительская сущность. Ревизии параграфов нужны чтобы предыдущие ревизии родительской сущности указывали на соответствующие им ревизии встроенных дочерних сущностей.

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

{{ node.field_paragraph.target_revision_id }}

Значения переводов полей

Вывести переведенное значение поля можно с помощью метода translation:

{{ node.translation('LANGCODE').label() }}
{{ node.translation('LANGCODE').FIELD_NAME.value }}

Помимо очевидного применения для вывода значения поля на разных языках, может возникнуть ситуация, когда вам нужно в родительской сущности вывести перевод значения переводимого поля из "непереводимой" целевой сущности. Например, перевод термина таксономии (переводимая сущность), на который ссылается сущность типа "пользователь" (непереводимая сущность), в шаблоне материала (переводимая сущность).

В этом случае, значение термина будет выведено на языке сущности пользователя (то есть, языке по умолчанию), а не языке материала. Пример вывода перевода значения:

{% set langcode = node.langcode.langcode %}
{% set user = node.field_user.entity %}
{% set term = user.field_taxonomy_term.entity %}
{% set term_label = term.hasTranslation(langcode) ? term.translation(langcode).label() : term.label() %}

{{ term_label }}

Если вам нужно вывести значение на языке отличном от текущего языка сущности, или вывести значение в шаблоне, в котором недоступен объект langcode, то идентификатор языка можно передать в шаблон с помощью функции имплементирующей "хук" hook_preprocess_HOOK() в файле THEMENAME.theme вашей темы оформления. Пример передачи идентификатора текущего языка в шаблон абстрактного элемента:

/**
 * Implements hook_preprocess_HOOK().
 */
function THEMENAME_preprocess_ELEMENT(&$variables) {
  $variables['langcode'] = \Drupal::languageManager()->getCurrentLanguage()->getId();
}

Шаблоны представлений (Views)

Шаблоны представлений бывают нескольких видов, или, правильнее было бы сказать, нескольких уровней. Для темы Stable 9 определены следующие файлы шаблонов.

Уровень представления:

  • core/themes/stable9/templates/views/views-view.html.twig — определяет разметку контейнеров для заголовка представления, шапки, раскрытых фильтров, вложений, непосредственно строк, "пейджера" и подвала.

Уровень формата представления (View format):

  • core/themes/stable9/templates/views/views-view-grid.html.twig — определяет разметку для формата "Сетка" (Grid);
  • core/themes/stable9/templates/views/views-view-list.html.twig — определяет разметку для формата "HTML список" (HTML List);
  • core/themes/stable9/templates/views/views-view-table.html.twig — определяет разметку для формата "Таблица" (Table);
  • core/themes/stable9/templates/views/views-view-unformatted.html.twig — определяет разметку для формата "Неформатированный список" (Unformatted list).

Уровень стиля строки (Row style):

Шаблоны этого уровня доступны для всех стандартных форматов представлений, кроме формата "Таблица" (в котором обработка строк происходит в шаблоне формата представления).

Из коробки, доступны два стиля строк: "содержимое" (Content) и "поля" (Fields).

Для стиля строк "содержимое" будут использоваться уже знакомые нам шаблоны сущностей содержимого, а для стиля строк "поля" — шаблон core/themes/stable9/templates/views/views-view-fields.html.twig.

Забегая вперед, скажу, что наибольший интерес, с точки зрения работы с разметкой, представляют шаблоны views-view.html.twig, views-view-unformatted.html.twig и views-view-fields.html.twig.

Также в модуле "представления" определено несколько шаблонов для вспомогательной разметки, но работа с ними не должна вызвать сложностей.

Предложения для имен шаблонов

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

  • BASE-TEMPLATE--VIEW-ID--DISPLAY-ID.html.twig
  • BASE-TEMPLATE--DISPLAY-ID.html.twig
  • BASE-TEMPLATE--VIEW-ID--DISPLAY-TYPE.html.twig
  • BASE-TEMPLATE--DISPLAY-TYPE.html.twig
  • BASE-TEMPLATE--VIEW-ID.html.twig
  • BASE-TEMPLATE.html.twig

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

  • node--view--VIEW-ID--DISPLAY-ID.html.twig
  • node--view--VIEW-ID.html.twig

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

Шаблон представления

views-view.html.twig — это основной шаблон представления, определяющий общую разметку. В него передается множество разных переменных: содержимое блоков представления (переменные header, footer и др.), непосредственно результаты (переменная rows), а также объект представления (переменная view).

Объект представления доступен во всех шаблонах представлений, на всех уровнях, это своего рода глобальная переменная.

Из него можно получить, например, такие полезные значения:

  • view.getTitle() — общий заголовок представления, а не текущего отображения (Display);
  • view.exposed_raw_input — массив значений всех раскрытых фильтров;
  • view.getExposedInput.FILTER_NAME — значение раскрытого фильтра с именем FILTER_NAME;
  • view.args — массив знчений всех аргументов;
  • view.args.0 — значение первого аргумента (остальные можно получить аналогичным образом);
  • view.result|length — количество результатов в текущем отображении.

Получить общее количество результатов, можно только с "пейджерами" (Pager) типа "Показать все результаты" (Display all items) и "Постраничный, полный" (Paged output, full pager):

{{ view.total_rows }}

Как правило, в общем шаблоне представления бывает нужно отобразить сводку по результатам какого-то запроса, приведенных выше значений для этого должно быть вполне достаточно. Простой пример вывода сводки для поиска:

{% set key = view.getExposedInput.KEY_NAME %}
{% set count = view.result|length %}
{% set total = view.total_rows %}

{{ 'Search results for <strong>"@key"</strong>'|t({ '@key': key }) }}
{{ 'Displayed @count of @total results'|t({ '@count': count, '@total': total }) }}

Практически все тоже самое можно сделать в шапке (Header), через интерфейс представлений, но правильным местом для этого является шаблон.

Кстати, поля шапки (Header), подвала (Footer) и поведения при отсутствии результатов (No results behavior) можно выводить по отдельности, и исключать из вывода. Примеры:

{{ header.FIELD_NAME }}
{{ header|without('FIELD_NAME') }}

Шаблон формата представления

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

Шаблоны формата представления, как правило, используются если нужно изменить структуру вывода. Например: изменить порядок результатов, убрать или добавить результаты, вывести результаты с использованием нестандартной раскладки и пр.

Шаблоны для стиля строк "содержимое"

По моим личным ощущениям, стиль строк "содержимое" незаслуженно недооценен в сообществе Drupal-разработчиков. Мне не попадались какие-либо упоминания о нем в статьях или видео о темизации. И тем не менее, я считаю его наиболее удобным вариантом, с точки зрения работы с разметкой.

Для использования в представлении можно создать отдельный режим отображения (View mode), добавить все необходимые поля и настроить виджеты привычным способом на вкладке управления отображением (Manage display). При необходимости, можно создать отдельный файл шаблона в соответствии с предложениями имен.

Да, интерфейс представлений также позволяет это сделать, но не самым оптимальными способом.

В угоду гибкости, интерфейс представлений имеет ряд неочевидных решений и особенностей:

  • перегруженный интерфейс управления разметкой (обертками, тегами, классами и другими атрибутами):
    • значения некоторых опций не очевидны;
    • элементы управления разнесены по разным местам;
    • одинаковые настройки выглядят по-разному для разных элементов;
    • управление логикой перемешано с управлением разметкой;
  • необходимость добавлять поля и скрывать их из вывода, чтобы затем использовать их значение в другом поле;
  • необходимость добавлять отдельное поле с разметкой, чтобы иметь определенное значение на нужном уровне вложенности;
  • генерация разметки происходит программно, на основе параметров, которые хранятся в базе данных;
  • неудобный процесс работы с переводами.

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

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

Еще одним доводом в пользу использования стиля строк "содержимое" и шаблонов сущностей может стать отсутствие необходимости добавлять связанные сущности (Relationships) в представление. Получить значения из связанных сущностей можно прямо в шаблоне, или передать их в шаблон на этапе препроцесса.

Шаблон для стиля строк "поля"

Несмотря на все преимущества стиля строк "содержимое" для большинства обычных задач, для стиля строк "поля" тоже есть свое применение — когда значение поля или его формат предоставляется представлением. Например: поля с перезаписанным значением, поля с агрегацией значений, поля связанных сущностей, получаемые через плагин представления и др.

Хорошая новость в том, что есть возможность использовать всю мощь представлений и функциональность Twig вместе — в шаблоне для стиля строк "поля".

Интерфейс представлений тоже позволяет использовать Twig в некоторых местах, но там его возможности сильно ограничены.

В свою очередь, шаблон views-view-fields.html.twig один из самых удобных, в него передаются все значения и переменные, какие только могут понадобится.

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

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

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

Получить же непосредственно значения можно довольно просто. Пример вывода значения обработанного плагином представления (то есть просто значения, без оберток):

{{ fields.FIELD_NAME.content }}

Пример с проверкой на наличие значения перед выводом:

{% if not fields.FIELD_NAME.isEmpty %}
  {{ fields.FIELD_NAME.content }}
{% else %}

Если значение поля множественное, и включено отображение значений в одной строке (Display all values in the same row), то значения будут объединены в одно обработчиком представления, в этом случае будут использоваться настройки формата отображения (Display type) и/или разделителя (Separator) из интерфейса представлений.

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

Для некоторых полей бывает доступно также сырое значение:

{{ fields.FIELD_NAME.raw }}

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

Также в шаблоне присутствует объект с результатом запроса — переменная row. В нем есть несколько полезных значений.

Идентификатор сущности, по которой формируется запрос. В случае с материалами свойство будет иметь название nid:

{{ row.nid }}

Объект сущности, по которой формируется запрос. Если вы собираетесь его использовать, то имеет смысл сразу создать переменную с привычным именем. Пример для материала:

{% set node = row._entity %}

По сути, объект сущности позволяет использовать шаблон стиля строк "поля" аналогично шаблону сущности содержимого: получать, использовать в логике и выводить любые значения полей самой сущности и связанных с ней дочерних сущностей.

Шаблоны страниц, регионов и блоков

Разметка страницы определяется шаблонами нескольких уровней — страницы, регионов и блоков. Соответствующие файлы шаблонов в теме Stable 9:

  • core/themes/stable9/templates/layout/html.html.twig — определяет разметку страницы на уровне тегов <html>, <head> и <body>;
  • core/themes/stable9/templates/layout/page.html.twig — определяет разметку страницы внутри тега <body>, в частности, разметку регионов;
  • core/themes/stable9/templates/layout/region.html.twig — определяет разметку на уровне региона страницы;
  • core/themes/stable9/templates/block/block.html.twig — определяет разметку на уровне блока.

Шаблоны сущностей и представлений включаются внутри блока основного содержимого страницы (Main page content), расположенного, как правило, внутри региона "Содержимое" (Content).

Шаблоны страницы

Глобальная разметка страницы определяется двумя шаблонами — html.html.twig и page.html.twig.

Для обоих шаблонов доступен ряд предложений имен, в зависимости от модуля, ответственного за обработку страницы. Примеры:

  • html--node--NODE-ID.html.twig
  • html--node.html.twig
  • html--VIEW-NAME.html.twig
  • page--node--NODE-ID.html.twig
  • page--node.html.twig
  • page--VIEW-NAME.html.twig

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

Полезные переменные в шаблоне html.html.twig:

  • attributes — аттрибуты тега <body>;
  • html_attributes — аттрибуты тега <html>;
  • head_title — содержит заголовок страницы и сайта;
  • root_path — базовый компонент пути страницы;
  • node_type — тип материала (для страниц материалов).

Полезные переменные в шаблоне page.html.twig:

  • front_page — путь к главной странице;
  • language — объект типа Drupal\Core\Language\Language для текущего языка страницы;
  • node — объект сущности материала (для страниц материалов).

Как правило, редактирование шаблона html.html.twig имеет смысл в следующих случаях:

  • управление классами тега <body>;
  • добавить встроенный JS или CSS код (часто требуется для интеграции с системами сбора метрик и аналитики).

Альтернативный способ управления классами тега <body>, сделать это на этапе проепроцесса, определив функцию имплементирующую "хук" hook_preprocess_html() в файле THEMENAME.theme вашей темы оформления:

/**
 * Implements hook_preprocess_HOOK().
 */
function THEMENAME_preprocess_html(array &$variables) {
  if (SOME-CONDITION) {
    $variables['attributes']['class'][] = 'SOME-CLASS';
  }
}

Для добавления элементов внутрь тега <head> лучше воспользоваться специальными модулями или сделать это программно, это позволит Drupal оптимизировать загрузку страницы. Пример программного добавления элемента:

/**
 * Implements hook_preprocess_HOOK().
 */
function THEMENAME_preprocess_html(array &$variables) {
  $preload_font_roboto = [
    '#tag' => 'link',
    '#attributes' => [
      'rel' => 'preload',
      'href' => 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap',
      'as' => 'style',
    ],
  ];

  $variables['page']['#attached']['html_head'][] = [$preload_font_roboto, 'preload_font_roboto'];
}

Редактирование шаблона page.html.twig, как правило, требуется при создании любой уникальной темы оформления. Это основной шаблон, определяющий разметку элементов страницы, структура которого во многом зависит от дизайна.

Если разметка страницы зависит от какого-то параметра, в том числе определенного в настройках темы оформления, то значения этих параметров можно передать в шаблон создав функцию имплементирующую "хук" hook_preprocess_page() в файле THEMENAME.theme вашей темы оформления:

/**
 * Implements hook_preprocess_HOOK().
 */
function THEMENAME_preprocess_page(array &$variables) {
  if (SOME-CONDITION) {
    $variables['VARIABLE1'] = 'SOME-VALUE';
  }
  $variables['VARIABLE2'] = theme_get_setting('SETTING_NAME');
}

Шаблоны регионов

По умолчанию, для вех регионов используется один общий шаблон — region.html.twig.

Для шаблона доступно предложение имени включающее название региона, и обычно этого вполне достаточно:

  • region--REGION-NAME.html.twig

Полезные переменные в шаблоне region.html.twig:

  • region — название региона;
  • elements — содержит рендер массивы всех блоков региона;
  • content — основная переменная шаблона, содержит отрисованную разметку всех блоков региона.

Состав, порядок и условия видимости блоков для региона определяются страницей "Схема блоков" (Block layout) интерфейса администрирования.

Но можно вывести блоки и вручную, по отдельности:

{{ elements.BLOCK_NAME }}

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

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

Шаблоны блоков

Основной шаблон у всех блоков общий — block.html.twig.

Стандартные предложения имен:

  • block--PLUGIN-NAME--BLOCK-ID.html.twig
  • block--PLUGIN-NAME.html.twig

На момент публикации, список не включает в себя предложения, которые содержат "бандл" и режим отображения для пользовательских блоков. Их можно добавить применив патч из задачи #3062376 вручную.

  • block--block-content--BLOCK-ID--VIEW-MODE
  • block--block-content--BLOCK-ID
  • block--block-content--BUNDLE--VIEW-MODE
  • block--block-content--BUNDLE
  • block--block-content--VIEW-MODE

В базовых темах ядра уже имеются переопределенные шаблоны для некоторых типов (плагинов) блоков (блоков меню, блока сообщений статуса, блоков со вкладками действий и др.).

Блоки управляются разными плагинами, и для них часто требуется разная разметка. Например, нестандартная разметка оберток, либо обертка не требуется вовсе. В шаблоне блока (и многих других шаблонах) это реализовано с помощью функционала обработчика Twig — конструкции block.

Twig позволяет поместить разметку внутрь конструкции block, выделяя тем самым части шаблона, предназначенные для переопределения. Другой шаблон может использовать шаблон с конструкцией block в качестве основы (расширяет его), переопределяя только разметку внутри конструкции block. Остальные части шаблона будут унаследованы без изменений. Абстрактный пример:

{# base-template.html.twig #}

<div class="globabl-wrapper">
  {% block content %}
    <div class="content-wrapper">
      {{ content }}
    </div>
  {% endblock %}
</div>
{# extended-template.html.twig #}

{% extends "base-template.html.twig" %}

{% block content %}
  <div class="content-wrapper">
    <div>Before content</div>
    {{ content }}
    <div>After content</div>
  </div>
{% endblock %}

Документация Twig по использованию конструкции block и extends.

При создании пользовательского шаблона для блока, можно использовать как "расширение" (конструкция extends), так и полное переопределение, в зависимости от того, нужны ли вам стандартные обертки блока в вашем шаблоне или нет. Примеры использования обоих подходов есть в теме Stable 9 (core/themes/stable9/templates/block).

Так как блоки управляются разными плагинами, то для них нет общего набора полезных переменных, кроме разве что base_plugin_id. Но в том или ином виде, в шаблоне обязательно будут присутствовать значения параметров конфигурации, определенных на странице Схема блоков (переменная configuration) и непосредственно содержимое блока, предоставляемое плагином (переменная content).

Создание собственного плагина блока

Иногда может понадобится сделать определенную разметку (как статичную, так и определяемую программно) доступной в виде блока, например, для управления видимостью с помощью конфигурации блока.

Чтобы создать собственный программный тип блока, нужно в вашем модуле объявить PHP класс-плагин блока.

Любую мало-мальски сложную разметку удобнее задавать в шаблоне. Существует несколько способов объявить шаблон для блока. Простой вариант — определить в вашем плагине блока необходимые переменные, объявить шаблон в функции, имплементирующей "хук" hook_theme, и передать сами переменные в шаблон. Особенностью этого варианта является то, что так можно задать шаблон только для содержимого блока, но не для самого элемента block, соответственно переопределить обертку блока в таком шаблоне не получится. Впрочем, для простой вставки разметки на страничку, обычно, этого достаточно.

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

Для примера давайте сделаем блок, выводящий ссылку на предыдущую страничку, если в URL текущей страницы определен параметр destination.

Чтобы затронуть все возможности системы блоков, будем использовать в плагине блока "dependency injection", добавим параметр на форму конфигурации, дополнительно передадим в шаблон сырые значения, и создадим пользовательский шаблон, в котором будем их использовать вместо вывода по умолчанию.

Назовем наш модуль example, и предположим, что все стандартные файлы для модуля уже созданы.

Нужно создать файл для класса-плагина блока modules/custom/example/src/Plugin/Block/BackLinkBlock.php со следующим содержимым:

<?php

namespace Drupal\example\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a backlink block.
 *
 * @Block(
 *   id = "example_backlink_block",
 *   admin_label = @Translation("Back link"),
 *   category = @Translation("Example")
 * )
 */
class BackLinkBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * Current request object.
   *
   * @var \Symfony\Component\HttpFoundation\Request|null
   */
  protected $currentRequest;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = new static($configuration, $plugin_id, $plugin_definition);
    $instance->currentRequest = $container->get('request_stack')->getCurrentRequest();

    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'show_home' => TRUE,
      'label_display' => FALSE,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    // Adding "Show home" checkbox to block configuration form.
    $form['block_backlink'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Configure backlink'),
    ];
    $form['block_backlink']['show_home'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show a link to the Homepage as a fallback'),
      '#description' => $this->t("If the previous page can't be determined, show the link to the home page instead."),
      '#default_value' => $this->configuration['show_home'],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    // Handling submmit of block configuration form.
    $block_backlink = $form_state->getValue('block_backlink');
    $this->configuration['show_home'] = $block_backlink['show_home'];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    // Preventing link from being cached.
    return 0;
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    // Determining the previous page by destination GET parameter.
    $destination = $this->currentRequest->query->get('destination');
    // Hiding the whole block if there is no link to show.
    if (empty($destination) && !$this->configuration['show_home']) {
      return [];
    }

    // Preparing block output and collecting raw values as well.
    $options = [
      'attributes' => [
        'class' => ['backlink'],
      ],
    ];

    $is_backlink = !empty($destination);
    if ($is_backlink) {
      $url = Url::fromUserInput($destination, $options);
      $title = $this->t('Back');
    }
    else {
      $url = Url::fromRoute('<front>', [], $options);
      $title = $this->t('Home');
    }

    $link = Link::fromTextAndUrl($title, $url);

    return [
      // Passing raw data for using in custom templates. 
      '#is_backlink' => $is_backlink,
      '#link_url' => $url,
      '#link_title' => $title,
      // The only renderable element, it is an actual block output.
      'link' => $link->toRenderable(),
    ];
  }

}

Также нужно создать файл modules/custom/example/example.module со следующим содержимым:

<?php

/**
 * Implements hook_theme().
 */
function example_theme() {
  return [
    'block__example_backlink_block' => [
      'render element' => 'elements',
      'base hook' => 'block',
    ],
  ];
}

/**
 * Implements hook_preprocess_HOOK() for block templates.
 */
function example_preprocess_block(&$variables) {
  switch ($variables['base_plugin_id']) {
    case 'example_backlink_block':
      // Passing raw values to template as root level variables. 
      $variables['is_backlink'] = $variables['content']['#is_backlink'] ?? '';
      $variables['link_url'] = $variables['content']['#link_url'] ?? '';
      $variables['link_title'] = $variables['content']['#link_title'] ?? '';
      $variables['show_home'] = (bool) $variables['configuration']['show_home'];

      break;
  }
}

Шаблон по умолчанию, вывел бы разметку из рендер-массива content, в данном случае — HTML-элемент ссылки, созданный программно из объекта Link.

В пользовательском шаблоне рендер-массив content не используется, а вместо этого элемент определятся с помощью разметки непосредственно в шаблоне, что позволяет, например, вставить иконку внутрь тега <a>.

Создадим файл шаблона modules/custom/example/templates/block--example-backlink-block.html.twig со следующим содержимым:

{% set classes = [
  'block',
  'block-' ~ configuration.provider|clean_class,
  'block-' ~ plugin_id|clean_class,
  'block-backlink',
] %}

<nav role="navigation"{{ attributes.addClass(classes)|without('role') }}>
  {{ title_prefix }}
  {% if label %}
    <h2{{ title_attributes.addClass('block-backlink__block-title') }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}

  {% block content %}
    {% if link_url %}
      <a class="block-backlink__link backlink" href="{{ link_url.toString() }}">
        {% if is_backlink %}
          <svg class="backlink__icon icon icon--back" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20,11H7.83l5.59-5.59-1.42-1.41L4,12l8,8,1.41-1.41-5.58-5.59h12.17v-2Z"/></svg>
        {% else %}
          <svg class="backlink__icon icon icon--home" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12,3L1,11.4l1.21,1.59,1.79-1.37v9.38H20V11.62l1.79,1.36,1.21-1.58L12,3Zm6,16H6V10.1l6-4.58,6,4.58v8.9Z"/></svg>
        {% endif %}
        <span class="backlink__label">{{ link_title }}</span>
      </a>
    {% endif %}
  {% endblock %}
</nav>

Шаблоны форм

Чаще всего темизация форм осуществляется программно. Специально для этого в Drupal есть Form API, он довольно мощный и гибкий, о нем написано много статей и доступно много примеров.

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

По умолчанию, для всех форм используется один общий шаблон — form.html.twig. Этот шаблон очень простой, он выводит тег <form> и скомпилированную разметку всей формы из переменной children.

Для некоторых форм, помимо основного шаблона также используется шаблон содержимого формы. В нем отрисовывается то, что будет значением переменной children. В теме Stable 9 определено несколько таких шаблонов, например:

  • core/themes/stable9/templates/views/views-exposed-form.html.twig
  • core/themes/stable9/templates/content-edit/node-edit-form.html.twig

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

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

При этом, шаблон содержимого формы, для большинства форм тоже не определен. Дело в том, что для содержимого формы нет шаблона по умолчанию, а если вы определяете шаблон, то его нужно предоставить.

Чтобы определить шаблон содержимого формы, для формы у которой его еще нет, необходимо создать функцию, имплементирующую "хук" hook_theme в файле MODULENAME.module вашего модуля.

/**
 * Implements hook_theme().
 */
function MODULENAME_theme() {
  return [
    'FORM_THEME' => [
      'render element' => 'form',
    ]
  ];
}

В качестве значения FORM_THEME можно использовать существующее значение элемента $form['#theme'] массива $form, как правило, оно частично или полностью совпадает с идентификатором формы ($form_id). Это же значение будет и именем шаблона, только вместо нижних подчеркиваний нужно использовать тире.

Если существующее значение FORM_THEME вас не устраивает или не задано, то его можно задать с помощью функции, имплементирующей "хук" hook_form_alter в файле MODULENAME.module вашего модуля или в файле THEMENAME.theme вашей темы.

use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_from_alter().
 */
function MODULENAME_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  switch ($form_id) {
    case 'FORM_ID_VALUE':
      $form['#theme'] = 'FORM_THEME'

      break;
  }
}

Шаблон содержимого формы

Основной переменной в шаблоне содержимого формы является переменная form, содержащая рендер-массив элементов формы.

При выводе этой переменной обработчик обходит все элементы по порядку и отрисовывает их, если нужно, аналогично тому, как это делается при выводе переменной content в шаблонах сущностей содержимого.

Но элементами рендер-массива являются другие поля, это не объекты типа "поле", а поля — элементы HTML формы (и вспомогательная разметка), описанные в виде рендер-массивов.

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

Абстрактный пример разметки в шаблоне содержимого формы:

<div class="cols">
  <div class="col-1">
    {{ form.ELEMENT1_NAME }}
    {{ form.ELEMENT2_NAME }}
  </div>
  <div class="col-2">
    {{ form.ELEMENT3_NAME }}
    {{ form|without('ELEMENT1_NAME', 'ELEMENT2_NAME', 'ELEMENT3_NAME') }}
  </div>
</div>

Заключение

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

Я буду рад если руководство окажется вам полезным. Если у вас есть идеи по улучшению и дополнению руководства, делитесь ими в комментариях.

Комментарии

2 комментария

Очень круто! Интересно, а тема для блога обновляется? Попробовать бы ее под друпал10 в новой парадигме на default-сайте

Здравствуйте. Да, тема переодически обновляется, но так как этот сайт довольно простой, то ожидать, чего-то сверхъестественного, конечно же, не стоит. Залил архивом прямо сюда, качайте, пробуйте на здоровье.

Из заметных изменений в последнее время были пожалуй лишь переработанные комментарии, которые теперь работают на базе Друпала, вместо Disquss. Также совсем недавно я переписал весь JS код на чистый JS без использования JQuery. Про оба этих кейса было бы неплохо написать отдельные статьи, но пока руки не дошли, посмотрим...