Изучите основы работы с одним из ключевых хуков Drupal. Узнайте, как создавать и использовать тем хуки в Drupal 8.
Что-то решил рассмотреть hook_theme(), так как быстрый гугл ничего на русском
особо не выдал, в основном легкие заметки, а я про него еще со времен D7 хотел
написать и забыл, но раз уже 8-ка в самом разгаре, разумеется, гайд будет под
8-ку. Сразу скажу, в с 8-ке в целом осталось как в 7-ке, но с легкими
изменениями. Удален template_process_HOOK()
, удалена функция theme()
, theme
hook suggestion объявляются теперь через hook_theme_suggestions_HOOK()
(см.
пример 3).
Что за хук и где используется
hook_theme()
один из самых фундаментальных хуков в Drupal, как в 7-ке, так и в
8-ке. Всё что вы видите на страницах, так или иначе проходит через данный хук.
Все темплейты которые вы создаете или имеете в своей теме — это всё его работа.
Он намного глобальнее чем просто хук. Он за собой тащит кучу совершенно новых.
Например, hook_preprocess_HOOK()
напрямую связан с hook_theme()
и без него
не представляет никакой ценности. Вы наверняка использовали его множество раз,
например hook_preprocess_html()
, hook_preprocess_page()
, hook_preprocess_node()
и т.д., и если не копали глубже, возможно, даже не подозревали, что это всё
от hook_theme()
и без него их бы не было. Даже создавая любой темплейт в теме,
будь то page.html.twig
или node.html.twig
, или даже node--1.html.twig
—
всё это делает данный хук. Следовательно, поняв данный хук, вы откроете целый
пласт возможностей в Drupal касающийся темизации и вывода данных. Ведь данный
хук можно использовать под огромный спектр задач, он вас практически никак не
ограничивает.
Разбор хука
Данный хук работает как в модулях так и в темах. Но не забывайте, объявив данный
хук в теме, он будет доступен для использования только на тех страницах, где
используется данная тема. Больше никакой разницы нет. Соответственно объявляется
данный хук по старинке в виде функции в MYMODULE.module
— для модулей,
и THEMENAME.theme
— для тем.
Пример данного хука, пока что без практической ценности:
/**
* Implements hook_theme().
*/
function MYMODULE_theme($existing, $type, $theme, $path) {
return [
'my_template_first' => array(
'variables' => [
'example' => NULL,
],
),
'my_template_second' => array(
'render element' => 'element',
),
];
}
В примере выше мы зарегистрировали 2 "хука темы", по сути, это минимум, который достаточен. Эти два варианта кардинально отличаются друг от друга.
Данный хук должен возвращать массив, ключи в первом уровне которого будут
становиться "хуками темы" (theme hooks). Возвращаясь к упоминаниям выше, в
хук hook_preprocess_HOOK()
вместо слова HOOK
подставляется именно эти ключи,
данные которых вы хотите модифицировать.
Следовательно hook_preprocess_page()
— это работа с данными page
хука темы
который объявлен также как и выше. Ради интереса вы можете посмотреть как это
обьявлено в ядре, почти все самое ходовое находится в
файле core/includes/theme.inc
на строке 1719 в
функции drupal_common_theme()
. Это не прямой вызов hook_theme()
, а функция,
которая вызывается в нужный момент в hook_theme()
, но структура аналогичная.
С ключами, думаю, более-менее должно быть ясно. Далее, данный ключ должен
содержать ещё один массив, в котором находятся параметры, как например, из
примера выше variables
и render array
. И тут важно запомнить, обьявляя хук
темы — он может содержать только один из данных "параметров", то есть
либо variables
, либо render array
, так как они используются для совершенно
разных задач, хотя всё остальное работает идентично и остальные параметры и
поведение применимы к обоим. Теперь самое время рассмотреть все возможные
данные:
variables
: Используется совместно с#theme
(далее вы поймете, если не сталкивались). Грубо говоря, данный theme hook будет использоваться для собственного темплейта под какую-то задачу. Соответственно это массив, он может быть как пустым, так и содержать все возможные переменные, которые необходимы темплейту с их значениями по умолчанию. Допустим, вы хотите сделать темплейт файл, который принимает какие-то данные, так вот тут вы можете указать все данные которые нужны с их значениями по умолчанию, если же вы не знаете какие значения нужны по умолчанию или не нужны вообще, то принято использовать NULL. Более подробно об этом всем будет разжевано дальше.render element
: Если указан данный ключ, то его значение (строка) — это лишь название переменной где будет храниться render array с данными. Очень часто используется простоelement
, но, например, вpage
используется одноименный рендер элемент. Это как раз тот самый что вы используете вpage.html.twig
, например{{ page.content }}
. Вотpage
и есть тот самыйrender element
. Также он используется для объявления темплейтов форм, следовательно в данном случае значение ставитсяform
, так как в эту переменную будет передан render array формы. Данный вид theme hook можно вызывать только как render array ('#type' => 'theme_hook_name'
). Без необходимости не используйте, а предпочтитеvariables
, если вам не нужно работать с render array в данном theme hook. Когда он вам потребуется Drupal сам скажет, он будет ругаться что не указанrender element
, вот тогда вы и поймете что пора переводить theme hook с переменных на него. Резюмирую случаи где только он и нужен: темизации форм, темизация render array элементов, в том числе при объявлении своих.file
: Это название файла, который будет загружен если данный theme hook был вызван. Например вы туда можете положить всю необходимую логику для данного theme hook, чтобы он не грузился без причины. Порой данные хуки сопроваждаются очень приличным кол-вом кода, так как имеют очень большую гибкость, да банально — это организация кода.path
: По умолчанию все темплейты ищутся в папке/templates
модуля\темы, аfile
в корне папки модуля\темы, вы можете указать прямой путь до папки где искатьtemplate
иfile
. Путь должен быть от корня ядра Drupal, а не модуля. Для удобства вhook_theme()
передается аргумент$path
содержащий путь до текущего модуля\темы и вы можете использовать его в своих целях, например:$path . '/theme'
. Важно: указав данное значение,template
иfile
должны находиться по этому пути и нигде иначе, т.е. file в корне модуля\темы уже не будет работать.template
: Название темплейт файла, который будет использоваться для рендера. Все данные будут переданы в данный темплейт и его результат уже будет использован для вывода. Указывать расширение здесь не нужно, оно будет автоматически добавлено в соответствии с тем движком, который используется для рендера. Например из коробки Twig, и соответственно для темплейтаnode.html.twig
тут нужно указать простоnode
. Название не должно содержать нижних подчеркиваний, только тире. Если данное значение не установлено вами, то оно будет установлено автоматически по той же самой логике. Названиеtheme hook
будет установлено как значение с заменой нижних подчеркиваний на тире. Важно: темплейт файлы по умолчанию ищутся в/templates
, либо в той папке, которая указана вpath
, но в обоих случаях они должны находиться в корне данной папки, никаких вложений, но (!) если вы делаете переопределение темплейта theme hook в активной теме, там можно ложить в/templates
со вложениями.base hook
: Базовое название для theme hook если планируется использовать theme hook suggestions (это для подобных случаев: node, node--1, node--news, node--news--1 — здесь везде базово node). Не имеет смысла задавать если идентично основному theme hook.pattern
: Используется дляnode, node--1, node--news, node--news--1
— в данном значении указывается лишь начальный паттерн для будущих suggestions. Для определения динамической части используется__
(двойное нижнее подчеркивание). Например, чтобы добавитьmy_template_first
, из примера выше, поддержку паттернов, достаточно указатьmy_template_first__
. Все возможные комбинации и, непосредственно, сам выбор какой использовать производится дальше. На данном этапе указываются не все паттерны, а лишь откуда начинается динамическая часть.preprocess functions
: Здесь можно указать функции которые будут отвечать за препроцессинг данных. Использовать надо лишь когда реально требуется, а это, скорее всего, не случится, так как дефолтные значения покрывают все запросы. Для модулей этоtemplate_preprocess_HOOK()
. Не путайте сhook_preprocess_HOOK()
, работают они абсолютно идентично, ноtemplate_preprocess_HOOK()
разрешно использовать только в модуле который определил данный theme hook, и он вызывается раньшеhook_preprocess_HOOK()
. Об этом позже.override preprocess functions
: ЗначениеTRUE
в данном параметре полностью отключает все препроцессы кроме оригинального от модуля\темы который объявил данный theme hook. Это может пригодиться когда вы не хотите давать возможность внедриться в подготовку сторонним модулям. Как правило не нужно, только в экзотических случаях.type
: Здесь указывается кем объявлен данный theme hook. Может бытьmodule
,theme
илиtheme_engine
. Крайне не рекомендую использовать, ибо толку мало, устанавливается автоматически.theme path
: Путь до темы или модуля который регистрирует theme hook. Аналогично параметру выше, нет никакого практического примеренения заменять то что устанавливается автоматически.
Это все возможные параметры. Как вы видите, часть из них вообще можно забыть
моментально, такие как последние два, некоторые из них, скорее всего, никогда не
пригодятся, как preprocess functions
и override preprocess functions
, а
параметры как pattern
и base hook
— ситуативные. Остается всего лишь два
базовых которые определяют поведение — variables
и render element
, а
также file
, path
, template
— которые позволят поменять, в случае
необходимости местоположение темплейтов, добавить отдельный файл для препроцесса
и дать иное название темплейту.
Например, я, на проектах, если объявляю данный хук в теме, то file
указываю themename.theme.inc
, а также меняю path
на $path . '/templates/theme
чтобы все кастомные темплейты находились в
отдельной папке и было сразу видно что они кастомные. Так как я написал выше,
темплейты объявленные модулем\темой не могут иметь вложение если они их
собственные. Тут я не могу сказать что это best practice, просто пока лучше
вариант не нашел. Хранить все кучей в /templates
мне не нравится, я люблю
когда все лежит на своих местах, ибо при увеличении числа темплейтов начинается
бардак. Будет интересно послушать в комментариях кто где хранит свои кастомные
темплейты.
Примеры вызовов theme hooks
Сразу рассмотрим как они вызываются, а всё остальное уже разберем непосредственно на примерах.
// Вызов theme hook обьявленный через variables.
$result_array = [
'#theme' => 'my_template_first',
];
// В случае если вам нужно самому отрендерить — рендерим, зачастую достаточно
// просто вернуть массив и Drupal отрендерит самостоятельно. Тут всё зависит
// от того где вызывается код.
$result = \Drupal::service('renderer')->render($result_array);
// Аналогичный пример но с передачей переменной. В данном случае переменные
// передаются как часть render array, с добавлением # к названию переменной.
$result_array = [
'#theme' => 'my_template_first',
'#example' => 'Custom value',
];
// А теперь рассмотрим вызов для render element. Переменные передаются
// аналогично примеру выше, и аналогично, этот рендер массив либо
// возвращается, либо рендерится самостоятельно. Всё от ситуации.
// Но учтите, render element должен быть зарегистрирован в качестве элемента,
// просто так вызывать по названию theme hook нельзя. Он не самодостаточный.
$result = [
'#type' => 'my_template_second',
];
Теперь можно переходить к примерам и практике, так как объяснять данный хук проще всего именно на практике, в теории это полная каша.
Для того чтобы увидеть результаты примеров, их куда-то нужно выводить. Поэтому
для всех примеров я создам страницу /hook-theme
где и будут выводиться все
примеры. Вы можете написать сами или использовать свои заготовки, или же просто
уже скачать готовую заготовку
где только данный роут. Мне казалось что у меня даже есть материал про Routing
API, но даже нагуглить не смог, так что, пометил что надо расковырять и его ;)
Пример №1 — простое объявление theme hook
В данном примере мы объявим обычный theme hook который будет выводить дату. Не
совсем тот пример ради которого следует объявлять theme hook, но нам лишь для
понимания как оно работает. Далее по коду, подарузмевается что модуль имеет
название dummy
— учитывайте это если вы пишете в своем модуле.
Первым делом нам нужно зарегистрировать theme hook
. У нас нет вообще ничего,
поэтому нужно создать .module
файл, если его ещё нет, и написать
свой hook_theme
. В данном примере нам особо ничего не нужно, это будет просто
очень минимальный хук.
/**
* Implements hook_theme().
*/
function dummy_theme($existing, $type, $theme, $path) {
return [
'dummy_example_first' => [
'variables' => [],
],
];
}
Это очень минимальный набор для варианта с variables
. Из примера выше, theme
hook это dummy_example_first
. Переменных мы никаких не требуем, поэтому
значение оставляем пустым массивом, но никаких FALSE
или пустых строк тут не
должно быть.
Сбросив кэш, данный theme hook уже станет доступен. Но от него никакой пользы. Давайте сразу создадим для него препроцесс, и добавим туда текущую дату и время. Препроцессы позволяют нам работать с данными до тех пор, пока они не попадут на рендер движку для темизации, по умолчанию в 8-ке — Twig. Мы можем меняеть данные, добавлять, удалять, в общем делать все что хотим. Для этого существует два основных хука:
template_preprocess_HOOK()
— в случае если theme hook объявлен в модуле,THEMENAME_preprocess_HOOK()
— если объявлен в теме. Данный хук вызывается всегда самым первым, а уже затем отдается остальным модулям и темам на обработку. Здесь вы закладываете свои базовые переменные и данные, так как вы владелец данного theme hook. Обратите внимание, словоtemplate
в первом примере, не заменяется на название модуля, оно прямо так и пишется.hook_preprocess_HOOK()
— уже знакомый для многих хук. Он используется сторонними модулями и темами чтобы выполнять аналогичные действия что и хук выше. Аналогично как вы хукаете page или node, чтобы добавить своих классов, библиотек или что-то поправить или убрать ненужное.
Резюмируя и подводя короткий итог: 1-ый вариант используется если theme hook объявлен в этом же модуле\теме, 2-ой для всех остальных.
Слово HOOK
(именно то что большими буквами) в хуке заменяется на название
theme hook который нужно "обработать". В данные хуки передается всего 1
аргумент &$variables
, кто-то сокращенно пишет &$vars
. Лично я придерживаюсь
полного варианта как в ядре, но это дело вкуса. Самое важное тут, это то, что
данный аргумент всегда является переменной-ссылкой. Никаких return
данный
хук не должен вызывать, вы должны менять данные внутри &$variables
. Это
массив, а ключи первого уровня данной переменной будут становиться переменной в
темплейте. Следовательно, $variables['foo'] = 'bar'
создаст для темплейта
переменную foo со значением bar, а затем вы сможете использовать её в темплейте,
а можете и не использовать.
Переходим к созданию данного хука. Так как мы не указали
для dummy_example_first
значение file
то хук остается писать исключительно
в dummy.module
. Так как мы хотим выводить дату, то нам потребуется её
получать. В темплейте такого делать нельзя (upd, меня поправили что можно
через date
фильтр Twig), именно для этого есть препроцесс хуки, которые
позвляют всю логику вынести из темплейтов в соответствующие функции.
/**
* Implements template_preprocess_HOOK().
* Обработчик переменных для первого примера.
*/
function template_preprocess_dummy_example_first(&$variables) {
$date_formatter = \Drupal::service('date.formatter');
$variables['date'] = $date_formatter->format(time());
}
Выше мы задействовали сервис для форматирования времени и
передали в него текущее время на момент генерации темплейта. Затем мы положили
значение в $variables['date']
, из чего следует, что значение (дата) будет
находиться в переменной date
и мы сможем вызвать её в шаблоне сколько угодно
раз.
Теперь самое время подготовить шаблон. Так как мы не указали template
то
название theme hook автоматически трансфомрируется в темплейт, где нижнее
подчеркивание заменяется на тире. В нашем примере получается следующий
результат: dummy-example-first
. Из коробки движок для темизаций используется
Twig, его темплейты должны иметь расширение .html.twig
. Это значит что мы
должны создать файл dummy-example-first.html.twig
. Опять, мы не указали path
и данный файл будет искаться по дефолтному пути от корня модуля\темы в
папке /templates
. Собственно создаем данную папку, а в ней файл со следующим
содержимым:
<p>
Сейчас {{ date }}
</p>
Внутри темплейта вы ограничены только вашими потребностями. Единственное что стоит запомнить — никакой логики здесь быть не должно, она вся должна быть в препроцесс хуке. Но всякие циклы и условия тут, конечно же, разрешены. В D7 тут можно было в прямом смысле, если цензурно, гадить, и люди знатно гадили, но твиг это хорошенько режет и всю динамику и гибкие данные вам просто придется добавлять через preprocess. Поэтому особо тут нечего добавить.
Теперь нам нужно добавить вывод данного примера на страницу /theme-hook
. Нам
ведь нужно как-то посмотреть результат. Для этого заходим в контроллер для
данного пути и добавляем его в виде render array, и возвращаем результат.
/**
* {@inheritdoc}
*/
public function page() {
return [
'#theme' => 'dummy_example_first',
];
}
Сбрасываем кэш, заходим на страницу и вы должны увидеть свой результат:
Пример 2 — добавляем и используем переменные
В данном примере мы добавим возможность передавать значения в наш theme hook, а следовательно и темплейт. Допустим мы будем выводить список, а выбор, маркированный или нумерованный мы сделаем в переменной, также мы в переменной будем передавать массив с данными для списка, а каждый элемент может содержать заголовок и текст, при этом оба будут не обязательными. Мы также сделаем так, чтобы если значения не были переданы, мы не будем возвращать ничего. Поехали!
Первым делом нужно добавить наш theme hook в наш hook_theme()
. На этот раз, мы
препроцесс функцию вынесем за пределы .module
файла, а от первого примера там
и оставим, хотя и его можно туда будет перенести, но как пример я его трогать не
буду.
/**
* Implements hook_theme().
*/
function dummy_theme($existing, $type, $theme, $path) {
return [
'dummy_example_first' => [
'variables' => [],
],
// Данный пример будет использоваться для вывода содержимого в виде списка,
// а его препрцоесс мы венесем в отдельный файл.
'dummy_example_second' => [
'variables' => [
'list_type' => 'ul',
'items' => NULL,
],
'file' => 'dummy.theme.inc',
]
];
}
Как вы можете заметить, мы обьявили две переменные которые мы хотим
принимать: list_type
для указания какой тип списка будет использоваться, по
умолчанию установили в ul
, а также items
, в который можно передать массив с
данными. Данные будут массивом, каждый элемент также будет являться массивом и
может содержать два ключа title
и text
, которые мы будем использовать.
Вы должны понимать, что передавать то можно сколько угодно переменных, и какие
угодно, это не значит что раз мы объявили эти две переменные, другие не будут
приниматься, нет, будут, просто это значит, что у нас под них просто нет логики,
мы их не ожидаем там. И то что я выше написал, что items будет массивом
состоящим из других массивов с ключами title
и text
, это лишь мое
предпочтение и я исходя из этого дальше будут писать препроцесс и темплейт файл.
Это формальность которую вы сами задаете и пишете уже под это код.
Надеюсь с переменными всё понятно, поэтому нам нужно создать в корне модуля
файл dummy.theme.inc
, в котором будет наш препроцесс для темплейта. Файл, я
думаю создадите сами, а вот препроцесс я немного распишу. На самом деле, в такой
простой задачке препроцесс по сути не нужен, если будет соблюдаться четкая
структура переменных выше, то никакой обработки не нужно. Передадут верно —
выведется, нет — не выведется, это всё уже отрегулируется на уровне темплейта.
Но что-то же для примера в препроцесс засунуть нужно, и мы сделаем проверку
переменной list_type
, чтобы она могла быть только ul
или ol
, если же в неё
передадут что-то иное, мы сменим на значение по умолчанию ul
. Собственно, вот
такой препрцоесс:
/**
* Implements template_preprocess_HOOK().
*/
function template_preprocess_dummy_example_second(&$variables) {
// Для удобства записываю значение переменной list_type в локальную переменную
// в виде ссылки.
$list_type = &$variables['list_type'];
// Задаем список допустимых типов.
$allowed_list_types = ['ol', 'ul'];
// Если передано значение не из массива выше, мы устанавливаем значение по
// умолчанию.
if (!in_array($list_type, $allowed_list_types)) {
$list_type = 'ul';
}
}
Как вы видите, переменные, которые мы объявили, да и вообще, которые будут
передаваться, находятся по соответствующим ключам. Мы, по сути, передаем всегда
массив и с ним работаем. Просто в темплейте, ключи превратяться в переменные.
Например, мы не работали тут с items
, но она там есть, нам просто нет смысла с
ней работать, но она также дальше передастся в темплейт. В препроцессах мы
работаем с теми данными которые нам не нужны в сыром виде или же требуют
проверки как тип списка.
Далее создаем темплейт и выводим все нужные нам переменные.
{#
/**
* Тут мы проверяем, есть ли данные в переменной items. Если массив пустой или
* вовсе не был передан, то мы ничего рендерить не будем. Соответственно наш
* theme hook ничего не вернет и на странице ничего не будет.
*/
#}
{% if items is not empty %}
{# Делаем обертку для списка подставляя значение переменной в тег. #}
<{{ list_type }}>
{# Выводим наши данные из items. #}
{% for item in items %}
<li>
{% if item.title %}
<h3 class="title">{{ item.title }}</h3>
{% endif %}
{% if item.text %}
<p class="text">{{ item.text }}</p>
{% endif %}
</li>
{% endfor %}
</{{ list_type }}>
{% endif %}
Теперь, когда мы имеем все необходимое, осталось только вывести на нашей тестовой странице. Если вы не знаете, то render array могут быть как многоуровневые, так и быть в одном большом массиве, как у форм, поэтому мы теперь примеры будем помещать в массив с render array, а затем возвращать на рендер странице.
/**
* {@inheritdoc}
*/
public function page() {
$results = [];
// Пример №1.
$results[] = [
'#theme' => 'dummy_example_first',
];
// Пример №2.
$results[] = [
'#theme' => 'dummy_example_second',
'#list_type' => 'ol',
'#items' => [
[
'title' => 'Lorem ipsum dolor sit amet',
'text' => 'Phasellus in ipsum eros. Nunc tellus purus, cursus at tincidunt id, sagittis ut ligula. Ut metus tellus, laoreet ac feugiat vitae, sollicitudin ac quam.',
],
[
'title' => 'Suspendisse sodales in nulla in porttitor',
'text' => 'Praesent sapien ante, rhoncus ac lectus quis, tristique consequat purus. Nulla rhoncus tempus venenatis. Donec ultricies libero et ullamcorper blandit. Etiam dictum eros eu turpis lacinia, at ornare sem tincidunt.'
]
],
];
return $results;
}
Не забываем сбросить кэш, и обновляем нашу страницу с примерами.
Стоит упомнять что в препроцесс передаются не только ваши переменные, также туда добавляются некоторые системные, вы также можете их использовать. Но есть три переменных которые имеют особенное поведение.
Если вы для своего theme hook укажите что хотите получать
переменные attributes
, title_attributes
или content_attributes
учтите их
поведение. Они должны быть либо NULL, либо массивом, как пустым, так и с
аттрибутами, либо экземпляром объекта Attribute(). В случае если это экземпляр
объекта, ядро просто пройдет мимо, если это это не пустой массив, он сам создаст
из массива новый экземпляр объекта Attribute().
Например, ['class' => 'test test2', 'id' => 'my-id']
будет
равносильно: $atrriute->addClass('test test2');
, $attribute->setAttribute('id', 'my-id');
.
Проще всего передавать туда обычный массив, а всё остальное сделает ядро.
Данное поведение заработает только если вы укажете эти переменные в своём
theme_hook, в остальных случаях эти значения хоть и будут в $variables
, но они
всегда будут пустыми, даже если вы туда что-то передавали. Соответствено и
использоавть данные переменные для других целей не получится, ядро попробует
скормить ваши данные этому объекту, он, разумеется, не поймет что ему дали и
выдаст эксепшн.
Там есть другие переменные типа is_admin
, user
и т.д., можете сами их
посмотреть. Они могут вам немного помочь. Таких поведений как у трех выше там
нет.
Пример №3 — паттерны
Паттерны позволяют вам делать переопределение темплейтов под определенные
задачи. Самые очевидные примеры которые дадут понять о чем
я: block.html.twig
, block--region-left.html.twig
, block--block--1.html.twig
или node.html.twig
, node--news.html.twig
, node--1.html.twig
.
Данный пример будет именно про то, как создавать такую поддержку переопределения темплейта, как регистрируются такие варианты и как они определяются.
Всё это реализуется при помощи pattern
параметра для theme hook
и, как
правило, в связке с какими-то перемеными из variables
, можно и без них , но вы
будете очень сильно ограничены, так как данных для таких наборов будет мало.
Первым делом нам нужно создать пример, который бы можно было как-то вариативно менять. Я опять ничего хорошего не придумал, и поэтому будет следующее: мы объявим theme hook для цитат, самых обычных цитат. Он будет принимать непосредственно цитату, автора цитаты, год цитаты, название источника цитаты, а также ссылку на источник цитаты.
Мы сделаем возможность чтобы темплейты переопределялись по всем параметрам, так
будет лучше видно как это работает. Начнем с объявления theme hook
в hook_theme()
(старые примеры опущены, он просто добавляется ниже):
'dummy_example_quote' => [
'variables' => [
'quote' => NULL,
'author' => NULL,
'year' => NULL,
'source_title' => NULL,
'source_url' => NULL,
],
'file' => 'dummy.theme.inc',
'pattern' => 'dummy_example_quote__',
],
Здесь стоит остановиться немного лишь на pattern
, я просто напоминаю, что тут
указывается базовая "регулярка" для будущих паттернов для всех темплейтов.
Нижнее подчеркивание в названии темплейтов будет заменяться автоматически на
тире. Следовательно, он будет искать темплейты
с dummy-example-quote--SOMETHING.html.twig
, где SOMETHING мы будем указывать
сами, если же ни один темплейт по такому паттерну подходящий под конкретный
вызов не попадается, он применит стандартный dummy-example-quote.html.twig
.
Базовый темплейт в любом случае должен обязательно быть объявлен.
Пока что с паттернами мы отойдем в сторонку и доделаем до конца. Сначала мы сделаем preprocess для нашего темплейта для того чтобы добавить новую переменную — ссылку на источник. В нашем случае мы получаем два параметра для источника, заголовок и ссылку. Если указана только ссылка — мы не будем ничего выводить, если же указан один заголовок — мы будем выводить лишь его, если же указана и ссылка и заголовок, то будем в препроцессе генерировать нужную нам ссылку, всё это будет в одной переменной чтобы было проще выводить и проверять. А также полноценно генерировать содержимое для футера цитаты, чтобы все разделить через запятые.
// Добавьте вверху файла
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Implements template_preprocess_HOOK().
*/
function template_preprocess_dummy_example_quote(&$variables) {
// Устанавливаем значение по умолчанию для новой переменной.
$variables['source'] = NULL;
// Сразу же переношу в локальную переменную-ссылку для дальнейшего удобства.
$source = &$variables['source'];
// Если переданы оба значения, то используем их для генерации ссылки.
if ($variables['source_title'] && $variables['source_url']) {
// Генерируем объект URL.
$url = Url::fromUri($variables['source_url'], [
'attributes' => [
'target' => '_blank',
'rel' => 'nofollow',
]
]);
// Генерируем ссылку.
$source = Link::fromTextAndUrl($variables['source_title'], $url)->toString();
}
elseif ($variables['source_title']) {
$source = $variables['source_title'];
}
// Добавляем все объявленные переменные в один общий массив.
$footer = [];
!$variables['author'] ?: $footer[] = $variables['author'];
!$variables['year'] ?: $footer[] = $variables['year'];
!$source ?: $footer[] = $source;
// И соединяем их в одну строку если там что-то есть.
$variables['footer'] = !empty($footer) ? implode(', ', $footer) : FALSE;
}
Создаем дефолтный темплейт файл. Он будем использоваться по умолчанию если в дальнейшем паттерны не найдут переопределения. И хорошая база для копирования для будущих переопределений.
{% if quote %}
<blockquote>
<p>{{ quote }}</p>
{% if footer %}
<footer>
— {{ footer|raw }}
</footer>
{% endif %}
</blockquote>
{% endif %}
И последнее что нам остается, набросать сразу несколько различных примеров чтобы проверить как поведение, так и в дальнейшем задействовать для паттернов.
// Примеры для №3
$results[] = [
'#theme' => 'dummy_example_quote',
'#quote' => 'Praesent sapien ante, rhoncus ac lectus quis, tristique consequat purus.',
];
$results[] = [
'#theme' => 'dummy_example_quote',
'#quote' => 'Praesent sapien ante, rhoncus ac lectus quis, tristique consequat purus.',
'#author' => 'John Doe',
'#year' => 2017,
];
$results[] = [
'#theme' => 'dummy_example_quote',
'#quote' => 'Praesent sapien ante, rhoncus ac lectus quis, tristique consequat purus.',
'#author' => 'John Doe',
'#year' => 2017,
'#source_title' => 'Google',
'#source_url' => 'https://google.com/',
];
Не забываем сбросить кэш, и смотрим на странице результат!
Теперь, когда мы видим что всё в порядке, мы можем заняться паттернами. Паттерны
объявляются в hook_theme_suggestions_HOOK()
. Данный хук можно объявлять в
файле указанном в file
. В него передается массив $variables
со всеми
переменными объявленными в variables
theme hook, а также их значениями. Также
туда передается дополнительная переменная theme_hook_original
— она содержит
название theme hook который это объявил. Сам хук должен возвращать массив со
всеми возможными вариантами переопределения. Вызывается он до всех
preprocess, так что там сырые данные. Например на блоках:
block--MODULE--DELTA
block--MODULE
block--DELTA
Первое на что следует обратить внимание, базовое название block
не
указывается — оно вызывается автоматически если ни один из этого списка не
сработает. Второе, что следует понимать, он ищет темплейты сверху внизу, это
значит у block--MODULE--DELTA
самый высокий приоритет, и если найден темплейт
с таким паттерном, то он применится и дальше поиск остановится. Важно ищет
он их сверху вниз, но объявляются они в обработном порядке. Т.е. первым в
массиве там идет block--DELTA
, а в конце block--MODULE--DELTA
. Т.е. где так
расписаны паттерны, это для удобства, в коде же чутка наоборот.
Вместо MODULE и DELTA вы, подставляете значения переменых на основе тех что имеются на данный момент генерации. Т.е. паттерны будут становиться динамическими, а не статическими. И после того как найден подходящий темплейт по паттерну, ему отдадутся все переменные и отправят на рендер как и с оригиальным темплейтом. А что там будет делать юзер с ними, это уже нас не касается.
Вернемся к нашему примеру, давайте сделаем такие варианты паттернов:
dummy_example_quote--AUTHOR--YEAR--SOURCE
— сразу на трех значениях, чтобы показать что таких составных может быть неограниченное кол-во.dummy_example_quote--AUTHOR
— темплейт под конкретного автора.dummy_example_quote--SOURCE
— темплейт под источник.dummy_example_quote--YEAR
— темплейт под конкретный год.
Собственно, давайте это объявим прямо в коде, не забывая, что паттерны сверху в таком порядке для удобства восприятия, а объявляются в обратном порядке!
/**
* Implements hook_theme_suggestions_HOOK().
*/
function dummy_theme_suggestions_dummy_example_quote(array $variables) {
$suggestions = [];
// Подключаем транслитерацию для будущих изменений.
$transliteration = \Drupal::transliteration();
// Сначала переносим значения в локальные переменные для удобства.
$author = $variables['author'];
$year = $variables['year'];
$source = $variables['source_title'];
// Затем надо поработать с переменными.
// Первым делом подготовим имя автора для использования в названии темплейта.
// Оно, должно быть из латинских символов. А что если передали кириллицу?
// Мы должны это предусмотреть.
if ($author) {
// Первым делаем транслетирируем имя.
$author = $transliteration->transliterate($author);
// Переводм в нижний регистр.
$author = strtolower($author);
// Заменяем пробелы на нижнее подчеркивание, тире использовать НЕЛЬЗЯ.
// В конечном итоге, например Иванов Иван будет ivanov_ivan, John Doe станет
// john_doe.
$author = str_replace(' ', '_', $author);
}
// Год нас не интересует, а вот название источника может быть длинным и опять
// же, на разных языках. Проделываем аналогично с именем автора.
if ($source) {
$source = $transliteration->transliterate($source);
$source = strtolower($source);
$source = str_replace(' ', '_', $source);
}
// Добавляем паттерн для года.
if ($year) {
$suggestions[] = 'dummy_example_quote__' . $year;
}
// Для источника.
if ($source) {
$suggestions[] = 'dummy_example_quote__' . $source;
}
// Добавляем паттерн для автора.
if ($author) {
$suggestions[] = 'dummy_example_quote__' . $author;
}
// Ели все переменные заданы регистрируем самый гибкий паттерн.
if ($author && $year && $source) {
$suggestions[] = 'dummy_example_quote__' . $author . '__' . $year . '__' . $source;
}
return $suggestions;
}
Сбрасываем кэш и готово!
Помните те три цитаты для примера выше? Так вот, если вывести для них suggestions мы увидим следующую картинку:
Первая цитата не получила them hook suggestions, потому что там не передается ни автор, ни название источника, ни год.
У второй цитаты нет лишь источника, поэтому там уже два theme hook suggestions.
У третей цитаты есть все, поэтому там все возможные варианты переопределения.
Теперь обьясню как это использовать. Во-первых, не забывайте что массив с theme
hook suggestions генерируется в ином порядке от того что мы написали и
следовательно, читать его нужно соответственно. Он пойдет снизу вверх.
Получается для третей цитаты он попытается
найти dummy_example_quote__john_doe__2017__google
, если переопределения не
найдется, пойдет искать dummy_example_quote__john_doe
и т.д. Что вам сделать
чтобы переопределить темплейт для цитат от John Doe, 2017 года и источника
Google? Достаточно создать соответствующий файл, заменив все нижнии
подчеркивания на тире, а также добавить расширение движка темизации. Получим
следующее: dummy-example-quote--john-doe--2017--google.html.twig
,
Важно! Данные темплейты (которые переопределяют) ищутся исключительно в
активной теме, в папке /templates
и всех её подпапках. То есть модуль,
объявивший theme hook не может использовать эти переопределения, они не будут
работать. Также эти theme suggestions можно вызывать программно прямо в render
array: ['theme' => 'dummy_example_quote__2017']
. Не знаю какая цель для этого,
видимо чтобы использовать определенный темплейт в определенном месте, т.е.
принудительно вызывать рендер темплейта под год, когда есть темплейт для
источника, например.
Дополнение к примеру №3.
Вы также можете вызывать\объявлять theme hook suggestion без хука выше, прямо из render array, но действовать они будут икслючительно в данном вызове. Пример:
$render_array = [
'#theme' => [
'dummy_example_quoute__john_die__2017__google',
'dummy_example_quoute__johnn_doe',
'dummy_example_quoute__google',
'dummy_example_quoute__2017',
'dummy_example_quoute',
],
];
Обратите внимание что тут они обьявляются уже сверху вниз. Вы также выше можете сгенерировать массив и передать его в '#theme`. Но лучше всего обьявлять это через хук. Либо делать соответствующую функцию для вызова, чтобы другие модули также могли вызывать с suggestions. Используйте этот вариант когда реально подходит под задачу.
На этом примеры для theme hook через variables
заканчиваются и мы переходим к
render element, так как больше тут рассказывать вообще нечего.
Пример №4 — темизируем форму при помощи render element
Конкретно на Render Element мы останавливаться не будем, так как это отдельная
тема и я её как-нибудь раскрою отдельно, она использует hook_theme()
, но это
связка, чем прямое использование, поэтому раскрывать тут это смысла нет, будет
вообще каша.
Мы рассмотрим как можно использовать hook_theme()
в связке с render element
для темизации форм на сайте. Абсолютно все формы на сайте, не важно как созданы
они, программно, в админке, не важно и кем они созданы, ядром, сторонним
модулем — все формы на сайте пробуют вызвать theme hook одноименный своему
form_id, и если такой найден, передают данные в него на рендер, а
в render element
нужно установить form
, и в данной переменной окажется вся
форма. Ядро просто перед самым рендером формы добавляет '#theme' => 'form_id'
,
а найдется такой theme hook или нет, дело ваше. Это очень полезно знать если вы
хотите очень гибко оформить какую-либо форму.
Для примера я создал большую формочку для того чтобы было нагляднее. Для неё я
создал адрес /hook-theme-form
и собственно там форма выводится. Без лишних
подробностей вот все необходимое для копипаста. Так как это я уже расписывал в
топике про Form API — можете изучить или освежить память, если
что-то вызовет трудности при объявлении формы.
<?php
namespace Drupal\dummy\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Class HookThemeFormExample
*
* @package Drupal\dummy\Form
*/
class HookThemeFormExample extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
// Обратите на это внимание, данное значение будет использовано в качестве
// theme hook.
return 'hook_theme_form_example';
}
/**
* {@inheritdoc}.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['tour_length'] = [
'#type' => 'number',
'#default_value' => 1,
'#min' => 1,
'#max' => 30,
];
$form['adults'] = [
'#type' => 'number',
'#title' => 'Adults',
'#default_value' => 1,
'#min' => 1,
'#max' => 4,
];
$form['kids'] = [
'#type' => 'number',
'#title' => 'Kids, 2-12 years',
'#default_value' => 0,
'#min' => 0,
'#max' => 4,
];
$form['infants'] = [
'#type' => 'number',
'#title' => 'Infants, 0-2 years',
'#default_value' => 0,
'#min' => 0,
'#max' => 4,
];
$form['arrival_date'] = [
'#type' => 'date',
'#title' => 'Arrival date',
];
$form['departure_date'] = [
'#type' => 'date',
'#title' => 'Departure date',
];
$form['arrival_city'] = [
'#type' => 'textfield',
'#title' => 'Arrival city',
];
$form['departure_city'] = [
'#type' => 'textfield',
'#title' => 'Departure city',
];
$form['car'] = [
'#type' => 'select',
'#options' => [
'Tesla',
'Mercedes',
'BMW',
],
];
$form['hotel'] = [
'#type' => 'select',
'#options' => [
'5*',
'4*',
'3*',
],
];
$form['my_interests'] = [
'#type' => 'textfield',
];
$form['name'] = [
'#type' => 'textfield',
'#required' => TRUE,
'#attributes' => [
'placeholder' => 'Full name',
],
];
$form['email'] = [
'#type' => 'email',
'#required' => TRUE,
'#attributes' => [
'placeholder' => 'Email*',
],
];
$form['country'] = [
'#type' => 'email',
'#attributes' => [
'placeholder' => 'Your Country',
],
];
$form['phone'] = [
'#type' => 'tel',
'#attributes' => [
'placeholder' => 'Phone number',
],
];
$form['comment'] = [
'#type' => 'textarea',
'#attributes' => [
'placeholder' => 'Leave us a comment',
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => 'Submit',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
drupal_set_message('Good Job!');
}
}
dummy.hook_theme_form:
path: '/hook-theme-form'
defaults:
_title: 'hook_theme() example with form'
_form: '\Drupal\dummy\Form\HookThemeFormExample'
requirements:
_permission: 'access content'
Вот такую формочку мы будем оформлять.
Приступаем. В этом деле самое главное знать form_id нужной формы. В нашей
кастомной он hook_theme_form_example
— это значит что мы должны объявить
одноименный theme hook, чтож, объявляем:
'hook_theme_form_example' => [
'render element' => 'form',
'file' => 'dummy.theme.inc',
],
Объявлять препроцесс в данном случае — дело ваше, я объявлю. Так как мы объявляем данный theme hook в модуле, а тема по умолчанию используется Bartik — это значит что написать стили для формы без создания темы не получится, а как тогда показать? Я сделаю стили прямо в модуле и буду цеплять их вместе с формой, заодно и вы в готовом модуле увидите полностью оформленную форму и сможете поиграться с ней. Для этого я объявлю библиотеку со стилями. Опять же, если не знакомы или что-то подзабыли, про Libraries API я расписывал отдельно.
hook_theme_form.css:
version: VERSION
css:
theme:
styles/css/styles.css: { }
А также добавляем библиотеку в нашу форму. Так как форма наша, мы можем объявить
подключение библиотеки прямо в форме, но если форма не ваша, это делается в
препроцессе. Cтили относящиеся к определенному theme hook разумнее вынести в его
препроцесс, чтобы они не подключались с формой если theme hook не вызван, ведь
разметки не будет и соответственно стили не нужны. Хотя можно подключить прямо
в Twig темплейте {{ attach_library('module/library.name') }}
.
Стили я сюда тащить не буду. Ибо много, а смысла мало, всё сможете найти в готовом модуле. Да и не про это статья.
Теперь нам нужно создать темплейт для формы. Так как мы не указали
параметр template
, как и ранее, theme hook трансформируется а темплейт.
Следовательно, нам нужно создать hook-theme-form-example.html.twig
. Там будет
находиться переменная form
, которая является render array текущей формы. Там
доступны все элементы и мы можем выводить все что нам угодно где угодно. Вы
можете просто вывести всю форму {{ form }}
, но тогда не будет никакого смысла
в theme hook. Вы также можете обращаться к элементам формы
напрямую: {{ form.name }}
и это выведет только определенное поле формы, таким
образом выводите все поля где и как нужно, а затем оформляете стилями. Так как
тут обычный html, можете тащить сюда что хотите, и дополнительные текста, и
картинки вставлять. В общем оформлять форму как вам угодно. Можете также
добавлять нужные переменные в препроцессе и выводить их точно также как и в
обычном theme hook.
Самое важное при темизации формы, это не забыть вывести следующие
элементы: {{ form.form_build_id }}
, {{ form.form_token }}
, {{ form.form_id }}
—
без них форма не будет работать. Это невидимые элементы, их можно смело выводить
как сверху, так и снизу темплейта. Также следует обратить внимание на то, что у
нашей формы кнопка субмит определена напрямую $form['submit']
— значит вывести
мы её можем как {{ form.submit }}
, но у большинства модулей, в особенности из
ядра, и вообще так правильнее делать, они находятся в другом
массиве $form['actions']['submit']
— то уже выводить
нужно {{ form.actions.submit }}
. Самый быстрый способ, пробовать вариант с
actions, если не появилась, то без этой приставки, если и просто не появилось,
то в препроцессе смотрите в $variables['form']
где там субмит и как он
называется.
Также следует запомнить, что при таком подходе если выводите хоть 1 элемент
формы прямым вызовом {{ form.field_name }}
, то выводя там же {{ form }}
будет дубль поля. Говоря проще, тут проще и лучше всего делать вывод каждого
поля руками, впрочем для этого оно и используется. Так что не забудьте поля!
Вот такой получился у меня темплейт для формы выше:
{{ form.form_build_id }}
{{ form.form_token }}
{{ form.form_id }}
<div class="description">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam nisi nisi,
vehicula quis massa at, fermentum tempor risus. Phasellus consequat vel
neque vel euismod. Curabitur tellus leo, commodo at nibh sed, euismod mattis
orci. Nam in semper ante. Vestibulum pellentesque vitae nibh vel
sollicitudin. Aliquam efficitur tortor arcu, eu dignissim urna venenatis
nec. Donec ullamcorper et nibh eget scelerisque. Aenean non nunc tempus,
congue turpis congue, scelerisque metus. Nulla rutrum egestas sapien. Cras
vel suscipit sapien. Donec at est non ligula venenatis blandit. Donec non
tortor eu elit consectetur semper luctus ut erat. Morbi et facilisis dui.
Aenean est dolor, porttitor pretium convallis vel, feugiat vel neque.
</div>
<div class="form-elements">
<div class="left">
<div class="row">
<div class="title">Tour length</div>
<div class="elements">
{{ form.tour_length }}
</div>
</div>
<div class="row">
<div class="title">My Travel Party</div>
<div class="elements three-in-row">
{{ form.adults }}
{{ form.kids }}
{{ form.infants }}
</div>
</div>
<div class="row">
<div class="title">Arrival and departure date</div>
<div class="elements two-in-row">
{{ form.arrival_date }}
{{ form.departure_date }}
</div>
</div>
<div class="row">
<div class="title">Arrival and departure city</div>
<div class="elements two-in-row">
{{ form.arrival_city }}
{{ form.departure_city }}
</div>
</div>
</div>
<div class="right">
<div class="row">
<div class="title">Cars & Hotels options</div>
<div class="elements two-in-row">
{{ form.car }}
{{ form.hotel }}
</div>
</div>
<div class="row">
<div class="title">My interests</div>
<div class="elements">
{{ form.my_interests }}
</div>
</div>
<div class="row">
<div class="title">My Contact Details</div>
<div class="elements">
<div class="information two-in-row">
{{ form.name }}
{{ form.email }}
{{ form.country }}
{{ form.phone }}
</div>
{{ form.comment }}
</div>
</div>
</div>
</div>
<div class="buttons">
{{ form.submit }}
</div>
После темизации, у меня получился следующий результат:
В итоге форма получилась намного компактнее и легче для восприятия. Выглядеть она будет так везде где бы не вызывалась, в модальном окне, или ещё где-то, она будет выглядеть именно такю.
Это все сделано на коленке для демонастрации как это делается. А оформлять вы можете как хотите и как нужно, при таком подходе гибкость очень большая. Весь этот пример есть в конечном модуле, со всеми стилями.
Конец
Надеюсь вам был полезен данный материал, хоть они и не совсем полный, мы немного
все же опустили render array
, так как я считаю, что это совершенно не для
данной темы, но её нужно знать прежде чем лезть в Render Element. Да и в целом,
зная hook_theme()
, как он работает и его возможности, вы сможете делать
намного больше красивых элементов и форм, выводить разметку из кода в шаблоны и
многое-многое другое. Как я уже упомянул в начале статьи, этот хук просто везде,
вы с ним не на прямую, так косвенно поработаете, банально с препроцессами для
других theme hook. Даже если вам не потребуется объявлять свой theme hook, вы,
залезая в препроцесс к чужому, будете понимать что происходит, как работает и
что вы можете там сделать.
Что-то мне кажется гайд получился немного тяжеловатым для восприятия, но я верю, у вас всё получится, если что, спрашивайте и поправляйте меня в комментариях.
Ссылки
Комментарии
Хех, забавно вышло. Вечно я забываю про node_modules. Перезалил, 14кб ;)
Пожалуйста, матиралы обязательно будут.
Спасибо тебе за блог. Наверное это сейчас лучший блог о Друпал на русском языке. Потому что те ребята, которые писали ранее, уже крайне редко что-то постят.
Но немного запутанно все. Тебе кажется, что все просто (такое чувство всегда есть, когда уже знаешь как работает). А я начинаю путаться, и теряю нить.
Хочу сказать, что обязательно продолжай блог :)
Ну и может сделай подборку с готовыми шаблонами для препроцессов и альтеров. Это была бы очень полезная статья.
Какие грамотные исчерпывающие статьи. Помню, учился еще по старым твоим статьям. Сейчас они стали еще лучше!
Спасибо, пригодится. В последнее время проекты заказывают в основном на Друпал
Спасибо огромное за труд, информация оказалась крайне полезной. Особенно в части разъяснения параметров массива, возвращаемого хуком темы.
Друг, товарищ, Бро --- КРАСАУЧЕГ просто!!! Материальчик кидаешь легко, все по сути и с тонкостями. Слушай, в современной инфо-помойке таким людям как мы ; ) нет цены. Сам сижу собираю тоннами инфу со всех блогов и видеотутов, по частицам процеживаю мегатонны хлама и глупости (пытаясь еще в этом и сам разобраться), а у тебя все как надо подано. Огромного тебе стремления, позитива и духа, чтобы продолжать в том же темпе и ритме и набирать обороты.
Ты прицепи какую-нить копилку на блог на видное место, твоя работа действительно ценная, и я думаю стыдно будет тому, кто за такие данные 5-10 рублев (а кто и как полагается) не киданет. Я уже оплатил кучу курсов и доступов, где тупой и еще кривой перевод 10-15% API в сухом и местами непонятном техническом оформлении. Все закругляюсь, слов нет просто )))) могу тут целую статью написать о том, какой ты молодец!
Поправка по suggestions - предлгаемые темплейты (к примеру dummy-example-quote--2017.html.twig) нужно ложить в папку templates в теме, по дефолту с папки модуля они не тянутся
Никита, спасибо большое за статью))) Если есть нужна использовать suggestions в модуле (а не в теме), нужно в hook_theme() добавить hook theme вашего suggestion (например, "dummy_example_quote__2017") и добавить параметр base hook (в нашем случае это "base hook" => "dummy_example_guote")
Отличная статья. Но есть следующий вопрос. Вся темизация с кастомным темплейтом осуществляется в page.content, т.е по сути внутри страницы. А если я хочу темизировать всю страницу и создавать необходимые части страницы в контроллере. Т.е. у меня есть контроллер для страницы /test Эта страница будет иметь сайдбар, при этом в дефолтном темплейте page.html.twig нет сайдбара Я хочу в контроллере создать переменную page_sidebar, передать ее в отдельный темплейт test_page.html.twig, его будет использовать страница /test и в разметке этого темплейта есть сайд бар куда я вставлю содержимое переменной которое получу в контроллере.
У меня возник такой же вопрос. Как переопределить page.html.twig для страницы роута. У меня пока 2 варианта : либо делать это через render element как в статье в примере с формой описано, либо в теме в хуке mytheme_theme_suggestions_page_alter определять что запрашивается страница нужного роута и в переменную $suggestions подставлять название другого page шаблона
Вариант с добавлением суджешена самый адекватный и правильный.
Спасибо, очень хороший и качественный обзор. Начало действительно тяжеловато воспринимается, его лучше всего перечитать повторно после окончания чтения всей статьи. Тогда всё стает на свои места.
Улыбнул размер готового модуля. Сначала подумал, что там целый Drupal дистрибутив.
С нетерпением жду новых материалов.