Drupal 8: Рендер массивы и их рендеринг

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

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

Первое знакомство с ними поднимает очевидные вопросы: «Что это?», «Зачем это?», «Как этим пользоваться?», «Как это работает под капотом и что оно умеет?» и конечно «Почему бы просто не написать HTML?». Этим материалом, я постараюсь ответить на данные вопросы.

Рендер массивы

Рендер массивы (Render arrays или Renderable arrays) — специально отформатированные ассоциативные PHP массивы, которые описывают представление данных. Они описывают структуру, значения и прочие требования к Drupal, которые в дальнейшем преобразуются в HTML разметку.

Я не уверен, можно ли обозначить рендер массивы как Abstract Syntax Tree (AST) для HTML (в контексте Drupal), но это самое близкое техническое определение того, чем по сути являются рендер массивы.

Как выглядят рендер массивы?

Отличить рендер массивы от обычных просто — у рендер массивов есть ключи, которые начинаются с шарпа #.

Пример простого рендер массива:

$element = [
  '#markup' => 'Hello World!',
];

Пример выше, это самый минимальный и простой рендер массив, результатом которого будет строка «Hello World!».

Рассмотрим другой пример:

$element = [
  '#type' => 'html_tag',
  '#tag' => 'p',
  '#value' => 'Hello World!',
  '#attributes' => [
    'class' => ['hello-world'],
  ],
];

Пример оброс новыми значениями и стал больше. Результатом данного массива станет HTML разметка: <p class="hello-world">Hello World!</p>.

Хоть он и выглядит немного «пугающе», у вас не должно вызвать затруднений провести соответствия между «исходником» и результатом.

Зачем нужны рендер массивы?

Теперь, когда вы уже посмотрели как они выглядят и примерно поняли какой у них результат, поговорим об их назначении. Конечно, когда впервые сталкиваешься с рендер массивами, возникает вопрос «зачем всё это?».

Если вы пришли из другой CMS или фреймворка, с огромной вероятностью для вас это будет «диковинной» особенностью. По крайней мере, я не знаю где ещё используется подобный подход в CMS.

Ответить на вопрос «Зачем нужны рендер массивы?» проще, сравнивая их с HTML разметкой. Если забегать наперёд — гибкость, простота изменения, расширяемость и стандартизация.

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

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

Мы не будем вдаваться в тонкости валидации, отправки, оформления — нам просто нужна HTML разметка формы.

В классическом примере это выглядело бы примерно так:

<form method="POST" action="/search">
  <label for="keys">Поисковый запрос</label>
  <input id="keys" name="keys" type="text" value="<?php echo $default_value; ?>" placeholder="Введите поисковый запрос" required>
  <p>Введите ключевые слова для поиска.</p>
  <input type="submit" name="submit" value="Поиск">
</form>

А так, эта же форма будет выглядеть в рендер массиве:

$form['keys'] = [
  '#type' => 'textfield',
  '#title' => 'Поисковый запрос',
  '#description' => 'Введите ключевые слова для поиска.',
  '#placeholder' => 'Введите поисковый запрос',
  '#required' => TRUE,
  '#default_value' => $default_value,
];

$form['submit'] = [
  '#type' => 'submit',
  '#value' => 'Поиск',
];

Чем больше и гибче будет форма, тем монструознее, «страшнее» и сложнее для поддержки становится классический вариант.

Теперь допустим что такая форма уже существует на сайте. Она предоставляется модулем или ядром, не важно, ключевое здесь — она сторонняя. Затем ставится задача:

  • Нужно убрать описание у поля поиска.
  • Нужно добавить чекбокс для персональных данных.

Как тут поступают в «классическом варианте»? Форма не ваша, она где-то объявлена. Если это файл шаблона, обычно копируют файл целиком и правят под задачу, поддерживая новую версию темплейта (новую версию формы). Если это результат какой-то функции, то это, как правило, строковое значение, которое либо придётся целиком перезаписывать, либо обрабатывать регулярками, либо как-то выкручиваться с DOMElement. В итоге — задача уже может привести к костылям на проекте и сложно поддерживаемому коду.

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

function hook_form_FORM_ID_alter(array &$form, FormStateInterface $form_state) {
  // Убираем описание у строки поиска.
  unset($form['keys']['#description']);

  $form['privacy_policy'] = [
    '#type' => 'checkbox',
    '#title' => 'Я согласен с условиями обработки персональных данных',
    '#required' => TRUE,
  ];
}

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

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

С рендер массивами это совершенно не проблема, так как все разработчики работают с идентичной структурой и «форматом». Все знают что нужно отдавать, и что ожидать в качестве данных. В таком случае, решение выше просто обрастает парочкой проверок в более общем хуке hook_form_alter().

И всё же «Зачем нужны рендер массивы?». Рендер массивы унифицируют всю работу с HTML внутри PHP. У вас не мешается два разных языка — в PHP вы работаете с PHP, с HTML вы работаете в шаблонизаторе (по умолчанию Twig). Не возникает вопросов как вы можете внести изменения в HTML разметку стороннего модуля или ядра не поломав при этом всё, и не сломав возможность обновлять систему. Все делают через рендер массивы, Drupal API в подавляющем большинстве случаев в конечном итоге отдаёт рендер массив или ожидает его получить в качестве результата. Благодаря этому уровню «унификации» и стандартизации, а также простоте PHP массивов, мы получаем невероятно гибкий инструмент.

Структура рендер массива

Допустим, примеры и польза понятны. Но что если нужно сделать вложенную структуру? Например, вы хотите получить следующую HTML разметку используя только рендер массивы:

<div class="card">
  <h2 class="card__title">Hello World!</h2>
  <div class="card_content">
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  </div>
</div>

В данном примере уже появляется вложенность, как же поступить сейчас? И тут всё очень просто:

$element = [];

// <div class="card"></div>
$element['card'] = [
  '#type' => 'container',
  '#attributes' => [
    'class' => ['card'],
  ],
];

// <div class="card">
//   <h2 class="card__title">Hello World!</h2>
// </div>
$element['card']['title'] = [
  '#type' => 'html_tag',
  '#tag' => 'h2',
  '#attributes' => [
    'class' => ['card__title'],
  ],
  '#value' => 'Hello World!',
];

// <div class="card">
//   <h2 class="card__title">Hello World!</h2>
//   <div class="card__content"></div>
// </div>
$element['card']['content'] = [
  '#type' => 'container',
  '#attributes' => [
    'class' => ['card__content'],
  ],
];

// <div class="card">
//   <h2 class="card__title">Hello World!</h2>
//   <div class="card__content">
//     <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
//   </div>
// </div>
$element['card']['content'][] = [
  '#type' => 'html_tag',
  '#tag' => 'p',
  '#value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
];

Пример выше демонстрирует следующие особенности рендер массивов:

  • Элементы можно вкладывать друг в друга, причем неограниченное кол-во раз. При этом, названия ключей (если это не Form API) лишь для вашего удобства. Таким способом вы строите «HTML дерево» при помощи PHP массивов.
  • С ними можно работать «кусочками», а не пытаться описать огроменный массив. По сути, из таких вот маленьких кусочков и собирается вся страница.

Если внимательно посмотреть на массив, то вырисовывается два основных правила:

  1. Ключи конкретного массива начинающиеся с шарпа (#) — используются как свойства для передачи каких-либо данных.
  2. Все прочие ключи трактуются как дочерние рендер массивы.

Компонентный подход

Пример выше, хоть и небольшой, но уже потребовал создания приличного массива, чтобы сформировать его разметку. В частном случае — это нормально. Но что если разметка ещё больше, сложнее и используется в более чем одном месте? Копипастить рендер массивы с одинаковой структурой — плохая затея, так как нарушает принципы DRY. Более того, если в будущем потребуется изменить данную разметку везде где она используется, придётся искать все подобные вызовы.

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

Drupal предоставляет данную возможность из коробки. Причем в нескольких вариантах, вам только нужно выбрать что лучше подходит в вашей конкретной ситуации. Более того, все примеры выше, кроме самого первого с #markup и есть «компоненты», которым мы передаем аргументы через их свойства.

В Drupal у вас на выбор следующие варианты:

  1. hook_theme() (хуки тем) — позволяет регистрировать шаблон, какие ему нужны аргументы, его обработчики и использовать сколько угодно раз. Он используется для формирования HTML разметки на основе полученных данных из рендер массива.
  2. @RenderElement — плагины, которые, чаще всего используются в связке с hook_theme(), являясь дополнительной обёрткой. Как правильно, в данном случае hook_theme() отвечает только за HTML разметку и всё что с ней связано. Рендер элемент, в свою очередь, берёт на себя обработку входных данных, предоставляет значения по умолчанию, подключает библиотеки, проводит дополнительные обработки, формирует кэш метаданные и т.д. Рендер элементы также умеют собирать итоговый результат из других рендер массивов прямо в себе, или сразу отдавать готовый HTML результат.
  3. @FormElement — разновидность @RenderElement. Используется для создания рендер элементов для Form API. Иными словами для элементов, которые могут запрашивать данные у пользователя.

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

// <div class="card">
//   <h2 class="card__title">Hello World!</h2>
//   <div class="card__content">
//     <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
//   </div>
// </div>
$element = [
  '#theme' => 'card',
  '#title' => 'Hello World!',
  '#content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
];

Таким образом, мы создаём «компонент», который затем можно использовать где угодно, кому угодно и как угодно. И если внезапно потребуется добавить к нему гибкости или изменить разметку, то это делается просто, и повлияет на все его реализации. А также не забываем про theme hook suggestions от hook_theme(), что позволяет нам создавать разные варианты одного и того же элемента, с абсолютно разной разметкой, но одними и теми же аргументами и обработчиками.

Это позволяет не только сократить размеры массива и сохранить ваше время, но и делает данные решения переносимыми. Также, это решает вопрос «хаоса». Регистрируя свой «компонент» в Drupal, вы стандартизуете его. Он будет работать только так, как вы этого хотите и никак иначе. Именно поэтому в рендер массивах не выйдет написать что попало. Там ожидаются только заранее известные структуры.

Рендеринг

Рендеринг (рендер) — процесс преобразования рендер массива в HTML разметку.

Рендерер (Renderer) — стандартный сервис (renderer), отвечающий за рендер в Drupal.

Рендерер определяет как трактовать рендер массив, что с ним делать, что он будет поддерживать, какие значения будут по умолчанию и как его преобразовать в HTML результат. Именно здесь происходит «магия» превращения массивов в HTML.

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

Процесс основного рендера

Сервис имеет множество различных ситуативных методов, каждый из которых в конечном итоге произведёт рендер при помощи \Drupal\Core\Render\Renderer::doRender. С ним мы и будем разбираться.

Данный метод ожидает всего два аргумента:

  • &$elements — рендер массив, который необходимо преобразовать в HTML.
  • $is_root_call = FALSE — индикатор того, что рендер должен предоставить завершённый HTML. Например, сразу заменяя плейсхолдеры для ленивого билдера на значения. Это также означает, что это «последний рендер», а не рекурсивный.

В качестве результата, он возвращает строку с HTML разметкой. При отсутствии результата или экстренном завершении выполнения он вернёт пустую строку ('').

Обработка рендер массива идёт рекурсивно. Каждый уровень массива воспринимается как «рендер элемент», у которого могут быть как свои свойства, так и опционально — дочерние элементы. Иными словами, за один вызов doRender() обрабатывается один рендер элемент определённого уровня, начиная с самого верхнего (первого уровня), при необходимости заходя глубже (по дереву) и повторяя операцию для дочерних массивов.

Этап №1. Проверка $elements

Текущий этап проверяет аргумент $elements. Если это пустой массив, то сразу будет возвращён пустой результат ('').

Этап №2. Проверка доступа через функцию обратного вызова

Текущий этап производит проверку доступа к элементу на основе свойств #access и #access_callback.

Если для свойства #access не установлено значение, но задано для #access_callback, то:

  1. Проверяет, является ли значение строкой. Если является строкой, и при этом в значении имеется :: (вызов статического метода), то подобная ситуация трактуется как вызов контроллера. Рендерер попытается найти ответственный контроллер и вернет его функцию обратного вызова (callback, callable). Затем, данное значение запишется обратно в свойство #access_callback. Если контроллер не был найден, то преобразование пропускается.
  2. Независимо от предыдущего шага, вызывается функция обратного вызова из #access_callback, где в качестве аргумента передается массив $elements, а результат вызова записывается в свойство #access.

Следующим шагом обрабатывается свойство #access, если оно установлено.

  1. Если значение в #access является экземпляром \Drupal\Core\Access\AccessResultInterface, то его кэш-данные добавляются к результату, независимо от его результа. Если данный объект запрещает доступ, то сразу возвращается пустой результат рендера.
  2. Если значение не является объектом AccessResultInterface, то производится строгая проверка (===) на FALSE. Если там FALSE, возвращается пустой результат рендера, иначе, продолжается.

Обратите внимание на то, что #access_callback вызывается только в том случае, если не задан #access.

Этап №3. Защита от повторного рендера

Текущий этап проверяет свойство #printed. Если данное значение не пустое или же равно TRUE, то возвращается пустой результат. Это необходимо для защиты от рендера одного и того же элемента дважды в пределах одного рендер массива.

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

Этап №4. Получение рендер контекста

Текущий этап производит получение рендер контекста. Рендер контекст — это экземпляр объекта \Drupal\Core\Render\RenderContext, который коллекционирует в себе всю информацию о кэшировании в пределах текущего рендера. В него сразу же передаётся пустой экземпляр объекта \Drupal\Core\Render\BubbleableMetadata, что означает — по умолчанию никаких настроек кэша, на данном этапе, дополнительно не применяется. Данный объект будет использоваться как отправная точка для объединения собранных в дальнейшем кэш метаданных.

Этап №5. Добавление стандартных кэш контекстов

Текущий этап подготавливает стандартные кэш контексты для всех элементов. Текущая операция производится только если $is_root_call установлен в TRUE или задано значение ['#cache']['keys'] для свойства элемента.

Процесс установки кэш контекстов по умолчанию состоит из следующих этапов:

  1. Сначала получаются кэш контексты, которые должны применяться ко всем рендер элементам по умолчанию. Данные значения хранятся в параметр renderer.config:required_cache_contexts core.services.yml файла, которые вы также можете переопределять для проекта. По умолчанию добавляются следующие контексты: languages:language_interface, theme, user.permissions — это значит, что результаты рендера будут иметь различные варианты в зависимости от текущего языка системы, активной темы оформления и прав доступа пользователя (для каждой роли).
  2. Затем производится проверка, существует ли значение в ['#cache']['contexts'], если да, то значения объединяются, если нет, то просто задаются.

Этап №6. Попытка получить результат из кэша

Текущий этап попытается получить готовый результат из кэша рендера.

Если значение ['#cache']['keys'] задано, то производится попытка получения готового результата из кэша. Если не задано, то весь этап пропускается.

Если результат не найден, рендер продолжается, если найден:

  1. Результат кэша записывается в $elements (заменяя оригинальное значение).
  2. Производится проверка на $is_root_call. Если TRUE — плейсхолдеры заменяются на значения.
  3. Если значение $elements['#markup'] является строкой, то оно по умолчанию считается «безопасной» и заворачивается в объект Markup::create($elements['#markup']) для того чтобы к ней не применялись никакие процессы санитизиации.
  4. Затем, к рендер массиву из кэша применяются данные кэширования собранные на данный момент в RenderContext.
  5. Далее все собранные BubbleableMetadata окончательно собираются в один конечный результат.
  6. Рендер прекращается, в качестве результата возвращается $elements['#markup'].

Этап №7. Сохранение оригинальных данных для кэширования

Текущий этап сохраняет значения свойств #cache, #lazy_builder и #create_placeholder в переменную $pre_bubbling_elements.

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

Этап №8. Обработка @RenderElement и @FormElement

Текущий этап обрабатывает плагины @RenderElement и @FormElement.

Для запуска обработчика данных плагинов у рендер элемента должно быть задано свойство #type, но не задано #defaults_loaded или установлено в FALSE.

Данные, предоставляемые плагином из метода getInfo(), добавляются к основному рендер массиву $elements путём конкатенации массивов: $elements += $this->elementInfo->getInfo($elements['#type']). При этом, внутри, происходит установка значения #defaults_loaded в TRUE, чтобы в будущем данная обработка ни при каких обстоятельствах не повторилась.

Этап №9. Обработка #lazy_builder

Текущий этап обрабатывает свойство #lazy_builder.

Валидация

Если для элемента задано свойство #lazy_builder, то начинается его проверка.

  1. Первым делом проверяется, является ли значение свойства массивом. Если нет, то вызывается исключение.
  2. Если это массив, то проверяется количество значений. Их должно быть строго два: первый — функция обратного вызова, второй — массив аргументов. Если их больше или меньше, то вызывается исключение.
  3. Затем проводится проверка массива с аргументами. В качестве значений аргументов допускаются только скалярные типы (integer, float, string и boolean) и NULL. Если один из аргументов не соответствует допустимым типам данных, то вызывается исключение.
  4. Далее, производится попытка получить дочерние рендер элементы для текущего. Если такие есть, вызывается исключение, так как ленивый билдер не может иметь вложенные элементы.
  5. Последней проверкой является сравнение допустимых свойств и тех что переданы с рендер элементом. Для ленивого билдера допустима передача следующих свойств: #lazy_builder, #cache и #create_placeholder. Так как в процессе рендера к текущему моменту уже могут появиться свойства #printed и #weight, то они также попадают в список допустимых. Наличие любого другого элемента приведет к вызову исключения.
Определение необходимости плейсхолдеров

Далее, производится проверка, необходимо ли добавить плейсхолдеры для ленивого билдера (#create_placeholder). Для этого производится две проверки:

  1. Сначала проверяется, может ли вообще быть создан плейсхолдер. Если задан #lazy_builder и не задан #create_placeholder, или он не равен FALSE, тогда решается что можно добавлять плейсхолдер.
  2. Второй проверкой производится попытка автоматического определения, нужно ли создавать плейсхолдер. Для этого получается параметр render.config:auto_placeholder_conditions core.services.yml файла, которое вы можете переопределять. Для того чтобы данная проверка вернула TRUE, достаточно чтобы по одному из пунктов произошло совпадение:
    • В первую очередь проверяется ['#cache']['max-age']. Если он задан и не является перманентным (Cache::PERMANENT), а также его значение меньше или равно настройке max-age из конфигурации (по умолчанию 0), то возвращается TRUE.
    • Затем проводится проверка по ['#cache']['contexts']. Для того чтобы она вернула TRUE, достаточно чтобы один из переданных контекстов с рендер элементом был перечислен в настройке contexts (по умолчанию session и user).
    • Последней производится проверка по ['#cache']['tags'], которая полностью повторяет логику контекстов. По умолчанию в ядре условия по тегам не заданы.

Если обе проверки вернули TRUE, то для текущего рендер элемента принудительно задаётся значение #create_placeholder в TRUE, в остальных случаях вы должны контролировать данное поведение руками.

Создание плейсхолдеров

Если после всех операций выше, элемент имеет значение #create_placeholder равное TRUE, то запускается процесс создания плейсхолдеров. Если при заданном #create_placeholder окажется, что отсутствует свойство #lazy_builder, то будет вызвано исключение.

Процесс создания сводится к тому, что на основе данных рендер элемента генерируется «плейсхолдер» вида <drupal-render-placeholder callback="' . Html::escape($callback) . '" arguments="' . Html::escape($arguments) . '" token="' . Html::escape($token) . '"></drupal-render-placeholder>. Данный результат записывается в переменную $placeholder_markup.

После чего создаётся новый рендер элемент для плейсхолдера. Значение $placeholder_markup записывается в #markup (оно будет результатом рендера), а также используется в качестве ключа для аттачментов $placeholder_element['#attached']['placeholders'][$placeholder_markup], где в качестве значения элемента массива будет оригинальный рендер массив. Затем, получившийся элемент возвращается на замену оригинальному в основной поток рендера.

Хочу обратить ваше внимание на то, что пример плейсхолдера представлен от стандартной «стратегии» замены. Модули могут объявлять свои собственные стратегии замен и плейсхолдер, соответственно, может выглядить совершенно иначе. Например, этим занимается BigPipe поставляемый с Drupal. Его плейслхолдер выглядит несколько иначе.

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

«Отключение» ленивого билдера для элемента

Если рендер элемент с ленивым билдером прошёл этап валидации, но при этом не имел #create_placeholder равным TRUE, а также не получил данное значение при автоматической проверке (не заменен на плейсхолдер), то данный элемент незамедлительно будет заменён на реальный рендер элемент (не лениво). Это означает, что будет вызвана функция обратного вызова, а её результат станет текущим рендер элементом не дожидаясь окончания запроса. При этом, если возникла данная ситуация, то для данного элемента добавляется свойство #lazy_builder_built равное TRUE, которое никак и нигде больше не используется. Но если потребуется, будете знать что искать!

Таким образом, ленивый билдер не гарантирует свою «ленивость» без явного указания #create_placeholder равным TRUE. Если вы хотите чтобы какой-то элемент гарантированно проходил через данный процесс, не забывайте установить значение для данного свойства.

Этап №10. Вызов #pre_render

Текущий этап производит вызов всех функций обратного вызова что указаны в свойстве #pre_render.

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

Если функция обратного вызова является строкой и содержит ::, то Drupal попытается найти контроллер (см. этап №2).

Этап №11. Санитизация #markup и #plain_text

Текущий этап проводит санитизацию значений для свойств #plain_text и #markup.

Первым делом проверяется на наличие #plain_text. Если он имеется, он преобразует специальные символы в HTML сущности при помощи Html::secape(). Затем, результат заворачивается в Markup::create() и записывается в свойство #markup рендер элемента. Санитизация #markup пропускается.

Если #plain_text не задан, то проверяется #markup. Санитизация содержимого в свойстве #markup пропускается если значение является экземпляром объекта \Drupal\Component\Render\MarkupInterface. Это означает, что результат будет возвращён в неизменном виде.

Если #markup не является доверенным объектом, то получается список допустимых тегов, которые могут присутствовать в разметке. Допустимые теги можно передать через свойство #allowed_tags, если их там не обнаружено, то используется стандартный набор Xss::getAdminTagList(). Затем значение свойства фильтруется с разрешенными тегами Xss::filter(). Результат заворачивается в Markup::create() и записывается обратно в #markup.

Этап №12. Добавление стандартных значений для кэша и библиотек

Текущий этап задаёт значения по умолчанию для следующих значений:

  • ['#cache']['tags'] = []
  • ['#cache']['max-age'] = Cache::PERMANENT (время жизни кэша не ограничено)
  • ['#attached'] = []

Каждое из этих значений будет применено только в случае если на данный момент у рендер элемента их до сих пор не появилось. Они необходимы для BubbleableMetadata.

Этап №13. Экстренное прерывание через #printed

Текущий этап позволяет экстренно прервать дальнейший рендер и вернуть пустое значение в качестве результата. Для этого у элемента должно быть значение в свойстве #printed. Данный этап фактически повторяет этап №3, но также обновляет кэш данные что были собранны на данный момент.

Может возникнуть недопонимание, как же мы до сюда добрались, если подобное должно было сработать на этапе №3. Это вызвано той особенностью, что на этапе №8, 9 и 10 происходят преобразования, которые могут добавить данное свойство, при этом, его могло не быть на 3 этапе.

Эта особенность позволяет экстренно приостановить рендер конкретного элемента. Например, вы можете останавливать таким образом рендер на 10 этапе в #pre_render, когда проверки #access и #access_callback уже пройдены, но вы решили перекрыть доступ. Например задав элементу '#access' => FALSE в #pre_render, это не окажет никакого эффекта и элемент будет отрендерен.

Этап №14. Обработка и подключение States API

Текущий этап обрабатывает свойство #states от State API.

Если значение в данном свойстве присутствует, то к элементу добавляется подключение библиотеки core/drupal.states. Также, значение из #states в json формате (конвертируется автоматически из массива) записывается в аттрибут элемента data-drupal-states. Для рендер элемента '#type' => 'item' значение задаётся в #wrapper_attributes, для всех остальных в #attributes.

Этап №15. Сбор дочерних элементов

Текущий этап собирает и сортирует все дочерние элементы, результат записывает в новую переменную $children.

Затем, если у рендер элемента отсутствует свойство #children, то задаётся значение по умолчанию ''.

Этап №16. Санитизация #description, #field_prefix, #field_suffix

Текущий этап производит санитизацию значений для свойств #description, #field_prefix, #field_suffix.

Если значение имеется и оно скалярного типа, то производится сантизиация. Значение будет обработано Xss::filterAdmin() и затем завернуто в объект Markup::create() в качестве результата.

Этап №17. Рендер #theme

Текущий этап обрабатывает свойство #theme.

Прежде всего, рендерер записывает состояние, есть ли свойство #theme у элемента в переменную $theme_is_implemented, чтобы не проводить проверки повторно. На самом деле, данная операция проводится после 15 и перед 16 этапом. Почему оно находится там? Возможно недоглядели, но оно ни на что не влияет на том моменте.

Первым делом производится попытка обработки рендер элемента с #theme если при этом у него отсутствует значение для #render_children.

В таком случае #theme передается на рендер \Drupal\Core\Theme\ThemeManagerInterface::render, где текущий рендер элемент и выступает в качестве знакомой переменной $variables из хуков hook_preprocess_HOOK() (но ещё с сырыми значениями). Результат выполнения записывается в свойство #children текущего рендер элемента.

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

Более подробно о рендере #theme написано ниже. Про #theme и hook_theme() вы можете прочитать в отдельном материале.

Этап №18. Рендер дочерних рендер элементов

Текущий этап запускает рендер всех дочерних элементов текущего рендер элемента.

Данный этап будет выполнен только при следующих условиях:

  1. $theme_is_implemented равен FALSE (#theme не задан или не удалось найти подходящий тем хук) или задано свойство #render_children равное TRUE.
  2. #children не имеет значение.

Оба требования должны быть удовлетворены для рендера дочерних элементов.

Если условие успешно пройдено, то каждый дочерний элемент из переменной $children (этап №15) передается на рендер (рекурисвный вызов всего текущего процесса начиная с этапа №1), а результат при помощи конкатенации записывается в свойство #children текущего рендер элемента.

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

На текущем этапе происходит рендер «дерева» рендер массивов.

Этап №19. Соединение #markup с #children

Текущий этап соединяет результат из #markup c результатом из #children.

Обработка запустится только в том случае, если $theme_is_implemented равен FALSE и #markup имеет значение.

В процессе обработки, значение #markup будет объединено со значением #children в соответствующем порядке ($elements['#markup'] . $elements['#children']). Дополнительно, получившийся результат будет завёрнут в Markup::create(), а его результат — объект, будет записан в свойство #children.

Этап №20. Обработка #theme_wrappers

Текущий этап обрабатывает свойство #theme_wrappers, которое позволяет добавить дополнительную обёртку для рендер элемента. Данный процесс не запускается при наличии #render_children, для того чтобы избежать рекурсии.

Данное свойство имеет два разных поведения:

  1. Если значение #theme_wrappers является одномерным массивом (['container']), то значения трактуются как тем хуки (#theme).
  2. Если значение #theme_wrappers является многомерным массивом (['container' => ['#attributes' => ['class' => ['example']]]]), то ключи трактуются как тем хуки (#theme), а значения как свойства данного тем хука.

Каждое значение обрабатывается индивидуально в цикле и передается на рендер тем хукам. Текущий рендер элемент в данном случае выступает в качестве аргумента ($variables). При этом, при многоуровневом массиве свойства затирают оригинальные от текущего рендер элемента. Но вас это уже не должно никак смутить, так как текущий элемент фактически уже отрендерен и хранится в #children.

Так как процесс идёт в цикле, то заворачиваться они будут в соответствующем порядке. Если будет две обёртки — ['container', 'fieldset'], то результат сначала завернется в container, а затем в fieldset.

Каждая итерация записывает свой результат в #children свойство.

Забегая наперёд и беря во внимание то, как происходит рендер тем хуков, можно сделать вывод, что в качестве обёрток могут выступать только тем хуки с render element. Фактически, запрета нет, но учитывая то как ведёт себя рендер для тем хука с variables, это фактически не применимо и лишено смысла.

Этап №21. Вызов #post_render

Текущий этап вызывает функции обратного вызова указанные в свойстве #post_render. Аналогично тому, как это делается на этапе №10 с #pre_render.

В данном случае в функцию передаётся два аргумента:

  • $elements['#children'] — текущая HTML разметка элемента.
  • $elements — текущее состояние рендер элемента.

Результат работы функции записывается в $elements['#children']. То есть, на данном этапе производится работа уже с готовой HTML разметкой, если это по каким-то причинам необходимо. Но на основе элемента, вы можете фактически запустить его «новый рендер» и вернуть в качестве результата.

Этап №22. Формирование финального #markup. Обработка #prefix и #suffix

Текущий этап формирует финальное значение для свойства #markup, а также, обрабатывает свойства #prefix и #suffix.

Первым делом проверяется, задано ли значение для #render_children. Если оно задано, то значение из свойства #children записывается в #markup, дополнительно оборачивая его в Markup::create().

Если значение для #render_children не задано, то производится добавление префикса и суфикса. Значения будут получены из соответствующих свойств рендер элемента #prefix и #suffix. Если они не заданы, то по умолчанию будет присвоена пустая строка (''). Если значение(я) задано, то оно дополнительно проходит санитизацию чезез Xss::filterAdmin().

Результаты для префикса и суфикса, какими бы они ни были, оборачивают значение #children и дополнительно оборачиваются в Markup::create(). Результат присваивается свойству #markup.

Фактически, данный этап заканчивает рендер. HTML разметка будет находиться в #markup.

Этап №23. Обновление кэш метаданных контекста.

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

Это значит, что все кэш метаданные «дерева» будут учтены на самом высоком уровне у родителя.

Этап №24. Запись результата рендера в кэш рендера

Текущий этап записывает результат в кэш рендера.

Прежде всего, производится проверка, задано ли значение для ['#cache']['keys'] текущего элемента и переменной $pre_bubbling_elements (этап №7, оригинальные значения). Если в одной из переменных значение отсутствует — кэш не будет создан.

Если значения заданы, то они сравниваются. В случае если их значения расходятся, вызывается исключение. Это значит, что ['#cache']['keys'] нельзя менять в процессе рендера, но можно удалить для отключения кэширования.

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

Этап №25. Замена плейсхолдеров на значения

Текущий этап заменяет плейсхолдеры (этап №9) на значения, если это до сих пор не сделано. Данная замена будет произведена только если обрабатывается корневой элемент ($is_root_call).

Этап №26. Результат

Текущий этап завершает процесс рендеринга.

В качестве финальных операций, производится объединение кэш метаданных для рендер контекста. Рендер элементу задается свойство #printed равное TRUE, а в качестве результата возвращается значение из свойства #markup.

TL;DR основной рендер

  • Свойства (в порядке обработки) #access, #access_callback, #printed, #cache, #weight, #type, #defaults_loaded, #lazy_builder, #create_placeholder, #lazy_builder_built, #pre_render, #markup, #plain_text, #allowed_tags, #printed, #states, #children, #weight (используется объектом Element при сортировке дочерних элементов) #description, #field_prefix, #field_suffix, #theme, #render_children, #theme_wrappers, #prefix, #suffix — являются частью рендера и «зарезервированы» системой. Строгого запрета на их использование нет, но лучше не использовать их в качестве переменных для своих рендер элементов и тем хуков. Это не все подобные свойства, а только те что используются и обрабатываются в процессе основного рендера!
  • #access и #access_callback (в порядке приоритета) позволяют остановить рендер конкретного элемента и всего его дерева на самых ранних этапах. Подобная особенность часто используется при описании форм, чтобы не городить кучу сложных условий. Например '#access' => !$entity->isNew(). Для более комплексных случаев используйте #access_callback в связке с объектом \Drupal\Core\Access\AccessResultInterface, что позволит не только управлять доступом, но и корректно кэшировать и инвалидировать результаты для различных состояний.
  • Если нужно скрыть какой-то элемент, не основываясь на правах доступа, можно задействовать #printed. Для данного свойства в ядре имеются соответствующие функции hide() и show(), которые меняют значение. Чаще всего данная особенность используется в формах. Например hide($form['username']), вместо $form['username']['#access'] = FALSE;.
  • BubbleableMetadata фактически предназначен для внутренного использования рендерером. Он собирает все кэш данные с рендер элементов, а также #attached значения. Его название говорит про его особенность работы — «пузырьковые метаданные». Данные конкретного элемента объединяются с ранее полученными в процессе текущего рендера, таким образом делая «пузырь» больше. За пределами рендера используйте CacheableMetadata, который и расширяется BubbleableMetadata.
  • Для рендер элементов вы можете полностью пропустить обработку установив #defaults_loaded в TRUE.
  • Приоритет обработки основных свойств: #type, #theme, #plain_text, #markup. Это означает, что если вы укажите два и более данных свойства для одного элемента, то обрабатываться будет самый приоритетный.
  • #type имеет больший приоритет над #theme, при этом #type может превратиться в #theme, но не наоборот. Они могут использоваться вместе, в таком случае, первым будет обработан #type затем #theme. Хоть они и дополняют друг друга, в одном рендер элементе используйте только один из них, для того чтобы избежать проблем и путаницы.
  • #plain_text имеет больший приоритет над #markup. Задав их одному рендер элементу, значением станет то что передано в #plain_text.
  • Значения #plain_text и #markup подлежат санитизации. Значение #plain_text подлежит принудительной сантизации. В случае с #markup санитизация контролируема, как на уровне свойства #allowed_tags, так и его значения.
  • Если вы не желаете чтобы значение в #markup проходило санитизацию, вы можете создать «доверенное» значение, которое позволит пропустить данный процесс $element['#markup'] = Markup::create('<strong>All HTML allowed</strong>'). Такое значение также проигнорирует свойство #allowed_tags, если оно задано. Используйте данное поведение с умом, так как это может стать брешью в безопасности. Ни в коем случае не используйте данную особенность в связке с данными, которые могут вводить третьи лица или получаются динамически.
  • Вы можете экстренно отключить рендер элемента задав для #printed значение TRUE, на тех этапах, когда проверка #access и #access_callback уже пройдена. Например, через #pre_render.
  • При помощи сервис-параметра render.config:auto_placeholder_conditions вы можете влиять, для каких элементов будут автоматически создаваться плейсхолдеры при использовании ленивого билдера.
  • При помощи сервис-параметра renderer.config:required_cache_contexts вы можете влиять на то, какие кэш контексты будут устанавливаться на все элементы проходящие через рендер.
  • В качестве #theme_wrappers могут выступать только тем хуки (hook_theme()). Учитывая особенность рендера и назначение #theme_wrappers, тем хуки должны быть объявлены с render element.
  • Результат рендер массива кэшируется только на том уровне, где явно заданы кэш ключи. Если их не задано, значение не кэшируется. В кэш попадает результат текущего рендер элемента целиком ,со всеми его дочерними элементами, даже если у них заданы кэш ключи (в таком случае и у них будет персональный кэш). Не стоит злоупотреблять кэш ключами на рендер элементах без понимания что вы делаете. Как правило, этого делать не требуется, а бездумная расстановка приведёт к серьезному разбуханию кэша.

Рендеринг тем хуков

На 17 и 20 этапах основного рендера производится вызов рендера тем хуков (#theme, hook_theme()). Его мы рассмотрим отдельно, так как обрабатываются они своим собственным сервисом.

theme.manager (\Drupal\Core\Theme\ThemeManagerInterface) — сервис, ответственный за работу с активной темой оформления. Также данный сервис отвечает за непосредственный рендер #theme элементов.

Процесс рендера

За рендер тем хуков отвечает метод \Drupal\Core\Theme\ThemeManagerInterface::render. Он принимает два аргумента:

  • $hook — название тем хука для рендера. Может принимать в качестве значения как строку, так и массив из тем хуков.
  • $variables — массив с переменными для тем хука. Сырой вариант $variables из hook_preprocess_HOOK().

В качестве результата работы метода возвращается либо строка, либо инстанс \Drupal\Component\Render\MarkupInterface с HTML разметкой. В случае проблем — FALSE.

Этап №1. Подготовка

Текущий этап резервирует статическую переменную $default_attributes и получает название текущей активной темы в $active_theme.

Этап №2. Проверка модулей и режима обслуживания

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

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

Этап №3. Получение реестра темы

Текущий этап получает реестр активной темы (\Drupal\Core\Utility\ThemeRegistry) в переменную $theme_registry.

Реестр темы — это хранилище информации о конкретной теме, а также всех тем хуков (hook_theme()). Информация хранится в уже готовом для использования виде и на момент обращения полностью собрана. Это значит, что все параметры hook_theme() уже распарсены и приведены к общему значению, все хуки hook_preprocess_HOOK() — уже найдены и сохранены в preprocess functions.

Более того, там уже собрана вся информация о theme hook suggestions на основе темплейтов конкретной темы. Это означает, что если в вашей теме есть шаблон page--front.html.twig, то для него в реестре будет зарегистрирован тем хук page__front на основе его родительского page, с автоматически заданным названием темплейта page--front а также пути до его расположения.

Реестр получается из bootstrap кэша, который имеет неограниченное время жизни. Поэтому, если вы создали новый шаблон, объявили хук темы или реализовали hook_preprocess_HOOK() — вам необходимо сбросить кэш, чтобы вся эта информация собралась в новом реестре. Вы можете влиять на эти данные при помощи hook_theme_registry_alter().

Этап №4. Определение кандидата и сохранение оригинального хука

Текущий этап определяет кандидата на рендер и сохраняет оригинальное значение.

Первым делом будет произведена попытка определить кандидата на рендер. Это произойдёт если $hook является массивом. В таком случае каждое значение массива будет искаться в реестре темы. Первое найденное значение прервёт цикл. Если в реестре не будет найдено ни одного значения из переданного массива, то последний элемент цикла станет текущим $hook.

Затем значение $hook записывается в переменную $original_hook для будущих этапов.

Этап №5. Комплексный поиск тем хука

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

Раз мы попали на текущий этап и реестр темы актуален, с большой вероятностью мы имеем дело с тем хуком вида node__page для которого нет информации в реестре (нет тем хука или реестр не актуален). Поиск производится путём срезания последней части после __ и последующей проверкой в реестре, есть ли там текущий тем хук. Например для тем хука node__page__teaser сначала будет произведён поиск по целому значению (на этапе №4), затем по node__page, а затем, если не найден, поиск по node. Если в процессе будет найден результат, то цикл просто будет завершен, если нет, то он остановится когда в названии тем хука не останется частей после __. Значение постоянно заменяет собой $hook.

Затем, в очередной раз производится проверка текущего тем хука из $hook в реестре. Если на данный момент он до сих пор не определён, то рендер вернет FALSE в качестве результата. Беря пример выше, это произойдёт если не будет найден тем хук даже для node. Дополнительно, если $hook изначально не был массивом, будет создан лог о том что тем хук не найден.

Этап №6. Получение информации о тем хуке

Текущий этап получает информацию о тем хуке из реестра и записывает значение в $info.

Этап №7. Подготовка переменных

Текущий этап подготавливает переменные для тем хука.

Данный этап будет выполнен только при условии что переменная $variables, переданная в качестве второго аргумента, является рендер массивом. Для того чтобы массив был признан рендер массивом, должно быть задано свойство #theme или #theme_wrappers. Если этих свойств нет, то переменная $variables считается подготовленной и этап пропускается.

Если свойство найдено – то происходит переопределение переменной $variables. Оригинальное значение записывается в новую переменную $element, а для $variables задаётся пустой массив. Этап имеет два поведения.

Если тем хук ($info) содержит ключ variables, в таком случае, получаются ключи массива $info['variables'] (те самые из hook_theme()) и по ним запускается цикл. Если в оригинальном массиве с переменными (который уже $element) задано значение в формате # + [ключ] или такой ключ просто существует, то его значение записывается в $variables под тем же ключом, но уже без шарпа #. Это тот процесс, когда значение переменных передается из свойств рендер массива #key => key. Таким образом, тем хуки с variables получают только значения объявленных переменных. Если их не указать, то они просто не будут переданы.

Если тем хук не содержит ключ variables, в таком случае он обрабатывается как тем хук с render element. В данном случае всё намного проще. В переменную $variables добавляется ключ из render element тем хука, а значением становится $element (оригинальное значение). Также данному ключу задаётся свойство #render_children равное TRUE. Фактически тем хуки с render element используются в качестве обёрток над другими рендер массивами и элементами.

Этап №8. Добавление значений по умолчанию

Текущий этап добавляет значения по умолчанию в переменные.

Если тем хук с variables, то к текущему массиву переменных $variables путём конкатенации добавляются значения из $info['variables']. Так, значения переменных что не были переданы при вызове будут добавлены из хука.

Если тем хук с render element и его значение ($element) пустое, то добавится значение по умолчанию. Так как с render element нельзя передать значение по умолчанию, оно автоматически устанавливается в качестве пустого массива.

Этап №9. Запись названия оригинального тем хука

Текущий этап добавляет оригинальный тем хук ($original_hook, этап №4) в переменные ($variables) под ключом theme_hook_original.

Этап №10. Запись базового хука

Текущий этап записывает название базового хука в новую переменную $base_theme_hook.

Если у тем хука указано значение base hook, то будет записано оно, если оно не указано, то базовым хуком будет текущее значение из $hook (которое трансформируется на этапе №5).

Данное значение будет использовано для будущих вызовов хуков и theme hook suggestions.

Этап №11. Вызов hook_theme_suggestions_HOOK()

Текущий этап производит вызов всех реализаций hook_theme_suggestions_HOOK().

Хук вызывается на основе $base_theme_hook переменной, значение которого будет подставлено вместо HOOK ('hook_theme_suggestions_' . $base_theme_hook). В качестве аргумента передается текущий массив $variables.

Результатом будет массив со всеми theme hook suggestions, а значение записывается в переменную $suggestions для последующего использования.

Данный хук вызывается только для модулей.

Этап №12. Добавление суджешена при наличии базового хука

Текущий этап добавляет в $suggestions значение $hook если для тем хука задано значение base hook.

Например, если был вызван тем хук с явным указанием суджешена ('#theme' => 'node__page'), то он также будет добавлен в суджешены.

Добавление происходит в конец массива, это значит, что он будет иметь самый высокий приоритет.

Этап №13. Вызов hook_theme_suggestions_alter() и hook_theme_suggestions_HOOK_alter()

Текущий этап производит вызов двух хуков: hook_theme_suggestions_alter() и hook_theme_suggestions_HOOK_alter(), в соответствующем порядке. В случае hook_theme_suggestions_HOOK_alter(), значение HOOK, как и на этапе №11, берется из $base_theme_hook.

Хук позволяет сторонним модулям и темам изменить значение $suggestions переменной.

Данный хук вызывается сначала для модулей, затем для тем оформления.

Этап №14. Поиск подходящего тем хука для суджешена

Текущий этап, на основе значения $suggestions, пытается заменить информацию о текущем тем хуке ($info), если это требуется.

Это тот этап, который и вносит немного путаницы при работе с хуками из этапа №13. Суджешены задаются в порядке от самого общего, до более точного (['node', 'node__page', 'node__page__teaser']), но на текущем этапе значение $suggestions «переворачивается» и обрабатывается в обратном порядке (['node__page__teaser', 'node__page', 'node']).

Обработка идёт по циклу из суджешенов и проверяется каждый из суджешенов на наличие объявленного тем хука. Если он найден, то информация о нём запрашивается из реестра темы и записывается в $info, а цикл сразу прерывается. Если в цикле такой информации не было найдено, то значение $info останется неизменным.

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

Этап №15. Подключение необходимых файлов

Текущий этап подключает все требуемые php файлы для текущего тем хука.

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

Этап №16. Вызов препроцессоров и сбор bubbleable метаданных

Текущий этап вызывает все функции препроцессинга.

В первую очередь проверяется, есть ли у текущего хук темы ($info) значение base hook. Если оно задано, до вызываются дополнительные обработчики. Для этого, получается вся информация о базовом тем хуке из реестра темы, и загружаются все его includes, если такие имеются. Если у него задано значение preprocess functions, то текущее значение $hook записывается в переменную $theme_hook_suggestion для более позднего этапа.

После чего, независимо от действий выше, проверяется, если для текущего тем хука заданы preprocess functions, то они вызываются поочередно. В них передаются аргументы $variables, $hook и $info. Тут происходит вызов всех hook_preprocess_HOOK().

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

После того как все препроцесс функции были вызваны и обработаны, текущая переменная $variables проверяется на свойства #attached и #cache. Затем, удаляется значение для кэш-ключей (['#cache']['keys']). Это значит, что препроцесс функции не могут задавать и влиять на кэш-ключи.

Если bubbleable метаданные были собраны, то этот массив состоящий из #attached и #cache отдаётся в основной рендер (renderer). Так, эти данные «всплывут» в основном рендере, откуда был вызван рендер текущего тем хука.

Этап №17. Генерация результата

Текущий этап ответственен за генерацию HTML разметки на основе собранных данных.

Процесс получения разметки делится на два разных варианта. Если у тем хука ($info) задано значение для function, то запускается один вариант, если нет, второй.

Но прежде чем перейти на любой из вариантов, задаётся новая переменная $output с пустой строкой, куда и будет добавлен результат.

Если задан function

Если у тем хук задано значение для function, то производится проверка, существует ли данная функция. Если функция существует, то она вызывается, с аргументом $variables, её результат заворачивается в Markup::create() и записывается в $output.

Таким образом, за получение HTML разметки тем хука целиком и полностью отвечает функция.

Если не задан function

Если для тем хука не задано значение function, а это практически все кейсы, то запускается текущий процесс.

Первым делом задаются две новых переменные:

  1. $render_function со значением twig_render_template — функция, ответственная за рендер шаблона.
  2. $extension со значением .html.twig — расширение файлов шаблонов.

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

Если значение задано и при этом тем хуку не присвоено значение type равное module, обработка продолжается. Если нет, то за рендер будет отвечать Twig. Важно отметить, что по умолчанию всем тем хукам задаётся значение base_theme_engine для type.

Далее проверяется на наличие функция $theme_engine . '_render_template', если она существует, то записывается в $render_function.

После чего проверяется на наличие функция $theme_engine . '_extension', если она существует, то она вызывается, а в качестве результата должна возвращать расширение движка которое запишется в $extension.

Затем, если на данный момент в $variables не задано значение для directory то задаётся новая переменная $default_template_variables с пустым массивом в качестве значения. После чего вызывается функция template_preprocess(), которая добавляет переменные по умолчанию в $default_template_variables. Далее вызывается функция _template_preprocess_default_variables() а также хук hook_template_preprocess_default_variables_alter(). В процессе подготовки переменных также задаётся значение directory которая будет хранить путь до активной темы, а также предотвратит повторный вызов данной операции. После чего, полученные переменные по умолчанию с их значениями путём конкатенации добавляются к $variables.

Если не задано значение для статичной переменной $default_attributes, ей присваивается значение new Attribute().

Далее происходит обработка трёх переменных: attributes, title_attributes и content_attributes. Если для данных переменных имеются значения и они не являются экземпляром Attribute то они конвертируются в данный объект. Если значения нет, то создаётся копия $default_attributes (пустой Attribute).

После чего готовится путь до файла шаблона. Первым делом создаётся переменная $template_file из комбинации $info['template'] . $extension. Если в текущем тем хуке указано значение path, оно добавляется к данному значению $info['path'] . '/' . $template_file и записывается в $template_file.

Затем к переменным добавляется новое значение theme_hook_suggestions в которое записывается значение $suggestions.

Если задано значение для переменной $theme_hook_suggestion то оно записывается в переменные под ключём theme_hook_suggestion. Это необходимо для случаев когда происходит вызов с прямым указанием суджешена ('#theme' => 'node__page').

В конечном итоге полученная функция в $render_function вызывается с аргументом $template_file и текущими переменными в $variables. Данная функция должна вернуть HTML разметку в виде строки или Markup. В общем случаем, это уйдет на рендер Twig.

Этап №18. HTML разметка

Текущий этап возвращает получившеюся разметку.

Если $output является экземпляром MarkupInterface, то отдаётся в неизменном виде, если это что-то иное, то производится конвертация в строковое значение и возвращается как результат.

TL;DR рендер тем хуков

  • Рендер тем хуков не может происходить до полной загрузки всех модулей. Это значит что для рендера тем хуков обязательно должно быть запущено ядро, а именно, завершиться \Drupal\Core\DrupalKernel::preHandle.
  • Вся информация о тем хуках, реализованных theme hook suggestions и hook_preprocess_HOOK() собирается один раз и кэшируется. Для хранения информации используется реестр темы (ThemeRegistry), при этом для каждой темы оформления он свой собственный. Если вы что-то добавили или изменили, необходимо сбросить кэш чтобы он перестроился.
  • Все реализации hook_preprocess_HOOK() собираются при построении реестра темы и кэшируются. Эти хуки не вызываются динамически.
  • theme hook suggestions собираются в момент построения реестра темы на основе шаблонов. Все найденные темплейты для суджешенов регистрируются как тем хуки внутри реестра. Это значит, создав в активной теме шаблон node--page--teaser.html.twig он будет зарегистрирован в реестре темы как тем хук node__page__teaser.
  • theme hook suggestions ищется только в темах оформления. Для того чтобы сделать это в модуле, нужно явно зарегистрировать тем хук для него, тогда он перекроет автоматический. Например, если вы хотите задать значение для шаблона paragraph--image.html.twig, то вам необходимо в модуле явно зарегистрировать тем хук paragraph__image, тогда он будет использоваться из модуля.
  • Вы можете корректировать реестр темы при помощи hook_theme_registry_alter().
  • Тем хуки могут вызываться с точным указанием хука ('#theme' => 'node'), с желаемым суджешеном который явно не объявлен ('#theme' => 'node__page'), а также массивом из тем хуков в порядке их приоритета для вас ('#theme' => ['node__page', 'node']).
  • Рендер тем хуков может вызываться программно с названием тем хука и массивом из переменных. Фактически, он может работать в обход рендер массивов.
  • Тем хуки с variables обрабатывают только те переменные, что явно указаны в определении тем хука. Все остальные срезаются.
  • Тем хуки с render element являются обёртками для других рендер элементов и массивов, хотя их можно использовать и как обычные, что позволит передавать любые переменные не объявляя. Но для таких задач лучше использовать @RenderElement.
  • Тем хук может быть объявлен либо с variables, либо с render element ключом, но не с двумя.
  • Тем хук с variables имеет больший приоритет над render element.
  • Хуки препроцессинга запускаются только для базового тем хука и активного, все остальные игнорируются.
  • Переменные по умолчанию attributes, title_attributes, content_attributes, title_prefix, title_suffix, db_is_active, is_admin, logged_in и directory присваиваются всем тем хукам в function _template_preprocess_default_variables(). Вы также можете менять их (кроме directory) и добавлять новые при помощи hook_template_preprocess_default_variables_alter(). Но данный процесс вызывается только при условии использования какого-либо движка для рендера темплейта.
  • Переменные attributes, title_attributes и content_attributes автоматически конвертируются из массивов в объект Attribute до того как будут переданы в шаблон, но после препроцесс хуков.
  • Хоть стандартные переменные и регистрируются автоматически, вы не можете использовать их для передачи данных без объявления в variables. Например, если в variables вашего тем хука нет переменной attributes, то вы не сможете передать значение через свойство #attributes, хотя оно будет в дальнейшем добавлено. Это вызвано тем, что стандартные переменные добавляются после чистки основных и влиять на них становится возможным только через препроцессинг хуки.
  • Только модули могут объявлять hook_theme_suggestions_HOOK(), темы могут влиять на них только при помощи hook_theme_suggestions_alter() и hook_theme_suggestions_HOOK_alter().

Кэширование рендера

Пройдясь по двум рендерам Drupal, стоит также отметить и кэш рендера, так как он имеет свои особенности. Работа с кэшем производится в основном рендере на этапах №6 (получение) и 24 (запись).

Первое на что стоит обратить внимание, то что кэш рендера реализует \Drupal\Core\Render\RenderCacheInterface вместо \Drupal\Core\Cache\CacheBackendInterface. Получается что кэш рендера является обёрткой над более общим кэш хранилищем.

Полностью разбирать данный сервис (render_cache) мы не будем, но пройдёмся по его методу set() который отвечает за сохранение кэша.

Данный метод получает два аргумента:

  1. $elements — массив с рендер элементом который необходимо закэшировать.
  2. $pre_bubbling_elements — массив с кэш данными до момента обработки (этап №7 основного рендера).

Этап №1. Проверка на кэширование и создание Cache ID

Текущий этап проверяет, может ли текущий запрос быть закэширован в принципе, а также создаётся Cache ID.

Cache ID создаётся только при наличии кэш-ключей (['#cache']['keys']) и max-age больше 0. Для создания ID кэша создаётся массив. Первым делом в него добавляются кэш-ключи.

После того как в массив для склейки добавлены кэш-ключи, проверяется значение кэш-контекстов (['#cache']['contexts']). Если значение задано, то все контексты что там указаны конвертируются в ключи. Например кэш-контексты ['languages:language_interface', 'url.site'] превратятся в ['[languages:language_interface]=en', '[url.site]=http://drupal8.localhost'].

В конце, значения массива склеиваются при помощи :, а результат и будет Cache ID. Например ['#cache']['keys'] = ['hello', 'World'] и ничего более, сформирует Cache ID равный hello:World. Если там будут контексты, они также будут склеены, например: hello:World:[languages:language_interface]=en:[url.site]=http://drupal8.localhost']. Обратите внимание что регистр, и порядок кэш ключей имеет значение. Кэш контексты при конвертации сортируются, так что для них порядок не важен, а за постоянство значения отвечает непосредственно сам контекст провайдер.

Этап №2. Конвертация рендер элемента

Текущий этап конвертирует полученный рендер элемент в новый.

Здесь происходит приведение текущего рендер элемента к единому виду для всех кэшей. А именно, создаётся совершенно новый рендер массив. Рендер массив для кэша имеет всегда три свойства: #markup, #attached и #cache (только contexts, tags, max-age). Все три принимают значения по умолчанию от оригинального рендер элемента.

Далее проводится проверка свойства #cache_properties, если оно задано и является массивом, то запускается дополнительная обработка. Значение данного свойства записывается в новый рендер массив без изменений.

Затем происходит поиск свойств в текущем рендер элементе на основе значения из #cache_properties, а результат записывается в переменную $cacheable_items. Далее, полученные значения проверяются на наличие дочерних рендер элементов. Если дочерние элементы найдены, их результаты (#markup) записываются под соответствующими ключами #cache_properties.

После чего эти данные добавляются к новому рендер элементу, значение #markup нового рендер элемента оборачивается в Markup и возвращается новый рендер элемент.

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

Обратите внимание что на этапе №6 основного рендера, идёт получение из кэша, а результатом становится значение #markup. Это означает, что закэшированные свойства при помощи #cache_properties надо получать напрямую из рендер кэша. Таким образом, эта особенность фактически предназначена для очень низкоуровневых операций вроде упомянутого ранее рендера основного содержимого. Никакой автоматизации для неё в основном рендере не предусмотрено.

Этап №3. Определение и получение кэш «корзины»

Текущий этап определяет и загружает необходимое кэш хранилище («корзину», bin).

По умолчанию кэш рендера сохраняет в кэш корзину render, но вы можете передавать значение для конкретного рендер элемента при помощи ['#cache']['bin'].

После того как определена кэш корзина, производится её загрузка.

Этап №4. Редирект кэша

Текущий этап отвечает за создание кэш редиректов.

Прежде всего, на основе $pre_bubbling_elements создаётся Cache ID. Затем, если он создался, то он сравнивается с основным CID ($cid, этап №1). Если их значения одинаковы, то весь текущий этап пропускается.

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

Например на рендер приходит элемент с ['#cache']['keys'] = ['foo'], он имеет дочерний элемент который задаёт ['#cache']['contexts'] = ['b']. На момент, когда основной элемент попытается получить свой результат из кэша (этап №6 основного рендера), он ничего не знает о своих дочерних элементах, а следовательно, он не знает про кэш-контекст b. И когда он попытается получить данные из кэша, он получит Cache ID равный foo и его результат будет некорректный. Но когда мы записываем кэш, мы знаем о всех дочерних элементах и всех их метаданных которые они задают.

Зная, что кэш-метаданные в начале рендера ($pre_bubbling_elements) расходятся с фактическими (при записи на этапе №26), мы можем указать кэшу что его нужно искать дальше. На элемент foo, в таком случае, добавляется свойство #cache_redirect равное TRUE, а также все #cache данные из дочерних элементов объединяются с текущими. Затем, такой рендер массив кэшируется.

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

Таким образом выходит, что некоторые элементы (родительские) не хранят в себе фактический кэш с результатом, а хранят редиректы на необходимые Cache ID где уже и лежат фактические результаты.

Если вы хотите разобраться в этом более детально и узнать как это решает другие кейсы, можете обратиться к исходнику \Drupal\Core\Render\RenderCache:109 — там очень подробное объяснение что происходит и какие проблемы решаются.

Этап №5. Запись в хранилище

Текущий этап записывает полученный новый рендер массив с кэш данными непосредственно в хранилище.

При сохранении кэша дополнительно всем элементам докидывает кэш тег rendered.

TL;DR кэш рендера

  • Кэш создаётся только для элементов где заданы ['#cache']['keys'] и ['#cache']['max-age'] больше 0.
  • ID кэша генерируется из кэш ключей, а также кэш контекстов если они заданы.
  • Кэш ключи - чувствительны к регистру и порядку. Если вы формируете одинаковые рендер массивы с кэш метаданными в разных местах, убедитесь что они задаются в одинаковом порядке, иначе у вас будут множиться кэш данные с одним и тем же результатом. Например ['#cache']['keys'] = ['foo', 'bar'] и ['#cache']['keys'] = ['bar', 'foo'] — это два разных Cache ID, даже если у них одинаковый результат.
  • Кэш рендера не всегда хранит готовый результат, иногда там хранятся кэш редиректы. Это особенность данного типа кэша. Если по каким-то причинам вам необходимо получить данные из кэш рендера напрямую, используйте сервис render_cache вместо cache.render.
  • Кэш рендера может хранить свой результат не только в корзине render (cache.render). Вы можете указывать конечное хранилище при помощи ['#cache']['bin'].
Drupal Drupal 8 Render API

Добавить комментарий

Содержимое данного поля является приватным и не предназначено для показа.