Один из самых используемых хуков Drupal 7 – это, конечно же, hook_menu. Его реализация имеется в любом мало-мальски серьёзном модуле. Функционал, которым управлял данный хук, был поистине огромен. С его помощью можно было объявлять страницы, вкладки, контекстные ссылки, устанавливать обработчики, управлять доступом и многое другое. С приходом Drupal 8 данный хук более не нужен и система роутинга теперь базируется на компонентах Symfony (перед изучением роутинга Drupal 8 я бы рекомендовал ознакомиться с документацией роутинга в Symfony). При первом знакомстве данная система может вызывать отторжение и ощущение запутанности у прожжённого фаната Drupal 7. Однако, при более подробном рассмотрении приходит понимание того, что новая система гораздо удобнее, и возвращаться к hook_menu больше не хочется.
Статичные маршруты
Начнём с простого – определение статичного маршрута. Для этого в Drupal 7 нужно было написать следующий код внутри своего модуля:
function example_menu() { $items = array(); $items['example_page'] = array( 'title' => 'Example Page', 'page callback' => example_page, 'access arguments' => array('access content'), ); return $items; }
В Drupal 8 для описания роутов используется формат YAML. Информация о роутах модуля, в том числе статических, содержится в файле MODULE_NAME.routing.yml. Важно (!) отметить, что в YAML файлах рекомендуется использовать одинарные кавычки. Использование двойных кавычек может привести к непредсказуемым последствиям. Каждый маршрут описывается отдельно и обязательно должен иметь следующие параметры:
-
Название - служит уникальным идентификатором роута;
Путь - в отличие от hook_menu путь обязательно должен начинаться со слеша;
Обработчик роута;
Условия для управления доступом к роуту.
Данные параметры являются необходимым минимумом для определения маршрута. Подробнее почитать про структуру описания роутов в Drupal 8 можно на странице официальной документации.
Вернёмся к рассмотренному ранее примеру. В Drupal 8 код будет следующим:
example.page: path: '/example_page' defaults: _controller: '\Drupal\example\Controller\PageController::page' _title: 'Example Page' requirements: _permission: 'access content'
Нетрудно заметить, что подобное представление в разы читабельнее массивов из hook_menu. Скажем несколько слов о параметрах вышеуказанного маршрута.
Во избежание конфликтов, имя роута должно быть построено по следующей схеме module_name.route_name. Параметр _controller отвечает за обработчик роута. Обработчиком может являться любой публичный метод класса, являющегося наследником класса базового контроллера (ControllerBase). Согласно стандартам Drupal 8, класс обработчик должен быть реализован в файле с названием соответствующим имени класса и располагаться этот файл должен в папке MODULE_NAME/src/Controller/. Для приведённого выше примера класс должен располагаться в файле example/src/Controller/PageController.php. Один контроллер может управлять любым количеством роутов и в отличие от Drupal 7 контроллер всегда должен возвращать массив.
Рассмотрим пример минимального контроллера:
/** * @file * Contains \Drupal\example\Controller\PageController. */ namespace Drupal\example\Controller; use Drupal\Core\Controller\ControllerBase; class PageController extends ControllerBase { /** * Page callback example. */ public function page() { return [ '#markup' => $this->t('Example page, Wow!'), ]; } }
Для работы с формами в роутах предусмотрен специальный параметр _form, который следует использовать вместо параметра _controller. Однако этот функционал заслуживает отдельной статьи и в рамках данной статьи рассматриваться не будет.
Для управления доступом к роуту по наличию разрешения используется параметр _permission. Подробнее о других вариантах управления доступом будет сказано далее.
Статичные маршруты с аргументами
По большей части, маршруты со статичными путями никому не нужны, поэтому необходимо использование аргументов. В Drupal 7 мы делали так:
function example_menu() { $items = array(); $items[‘example_page/%’] = array( 'title' => 'Example Page', 'page callback' => example_page, 'page arguments' => array(1), 'access arguments' => array('access content'), ); return $items; }
Если же мы хотели, чтобы при передаче параметра в callback Drupal автоматически подгружал связанную с ним сущность, то мы применяли механизм Wildcard. К примеру для загрузки ноды использовали аргумент %node, для пользователя %user и т.д. В Drupal 8 использование аргументов в маршрутах выглядит так:
example.page: path: '/example_page/{argument}' defaults: _controller: '\Drupal\example\Controller\PageController::page' _title: 'Example Page' requirements: _permission: 'access content'
Соответственно в методе контроллера роута нужно будет описать аргумент $argument. В секции defaults можно определять статичные аргументы для контроллера либо объявлять значения по умолчанию для аргументов, описанных в пути маршрута (при их отсутствии в URL). Любые значения в секции defaults, у которых ключ не начинается с подчёркивания считаются аргументами и будут переданы в контроллер. Например, при определении нижеуказанного роута, первый аргумент (animal) является динамическим и обязателен для данного маршрута, второй аргумент (fruit) – динамический, но необязательный и имеет значение по умолчанию, третий аргумент (count) – статический, он присутствует в метода контроллера, но отсутствует в URL.
example.page: path: '/example_page/{animal}/{fruit}' defaults: _controller: '\Drupal\example\Controller\PageController::page' _title: 'Example Page' fruit: 'banana' count: 7 requirements: _permission: 'access content'
Помимо всего прочего в маршрутах Drupal 8 можно устанавливать ограничения на аргументы. Ограничения представляют собой регулярное выражение с ключом, равным имени аргумента. В примере ниже параметр fruit должен обязательно иметь значение banana или mango, а аргумент count должен быть целым числом.
example.page: path: '/example_page/{fruit}/{count}' defaults: _controller: '\Drupal\example\Controller\PageController::page' _title: 'Example Page' requirements: _permission: 'access content' fruit: banana|mango count: \d+
Как и в Drupal 7, в 8-ой версии также имеется механизм передачи сущности в контроллер на основе значения аргумента (по-умному это называется Upcasting). Для этого аргумент нужно назвать именем сущности, которую надо передать в контроллер. Хотим передать ноду? Что же, нет ничего проще - называем аргумент node. Нужно загрузить пользователя – называем user и т.д для любой другой сущности. Рано или поздно у Вас возникнет вопрос - "Как же быть, если мне нужно передать в контроллер две ноды? Я ведь не могу определить два аргумента с одинаковым именем.". Да, определять аргументы с одинаковыми именами действительно нельзя. Здесь на помощь приходит секция options. Говоря словами официальной документации, этот раздел представляют собой дополнительные параметры для интерпретации и работы маршрута. Изменим наш роут, сообщив ему о том, что хотим иметь дело с двумя нодами в контроллере:
example.page: path: '/example_page/{node1}/{node2}' defaults: _controller: '\Drupal\example\Controller\PageController::page' _title: 'Example Page' requirements: _permission: 'access content' options: parameters: node1: type: entity:node node2: type: entity:node
Остаётся отметить, что Upcasting работает только при наличии в методе контроллера аргумента с типом загружаемой сущности.
Управление доступом
Один из важнейших аспектов роутинга – управление доступом. Мы совершенно не хотим, чтобы закрытую информацию мог посмотреть любой желающий. Для общего понимания слудет знать, что в Drupal 8 имеется возможность управлять доступом по следующим значениям:
-
Роль пользователя (_role) – можно указывать, как одну роль, так и комбинации AND/OR посредством символов ","/"+" соответственно;
Наличие разрешения (_permission) – аналогично роли можно указывать одиночное разрешение или комбинацию;
Наличие разрешения выполнения операции для сущности (_entity_access) – например, node.view выполняет проверку на наличие прав на просмотр ноды;
Кастомный callback управления доступом роута (_custom_access) – подробно можно прочитать в официальной документации;
На основе типа HTTP-запроса (_method) – множество типов указывается с помощью соответствующего символа - "|".
Кроме этого имеется возможность управления доступом в зависимости от включённых модулей и формата контента в запросе. Параметры управления доступом располагаются в секции requirements.
Динамические маршруты
Статичные маршруты – это, конечно, хорошо, но когда возникает необходимость определить множество похожих роутов на основе определённого правила, не будем же мы их все писать в MODULE.routing.yml. К тому же мы не можем точно быть уверены в том, сколько именно роутов нам нужно. В Drupal 7 логику построения множества маршрутов нужно было добавлять в код реализации hook_menu() в вашем модуле. На основе этой логики (например, с помощью цикла) в результирующий массив добавлялись требуемые маршруты. Но можем ли мы что-то похожее реализовать в Drupal 8? Идём читать официальную документацию. Действительно, присутствует специальный механизм для динамического роутинга. В MODULE.routing.yml объявляем элемент route_callbacks. Стоит отметить, что он является элементом верхнего уровня – говоря простым языком, располагается в файле максимально близко к левому краю без отступов. Для примера, можно рассмотреть часть содержимого файла роутинга модуля Views:
views.ajax: path: '/views/ajax' defaults: _controller: '\Drupal\views\Controller\ViewAjaxController::ajaxView' options: _theme: ajax_base_page requirements: _access: 'TRUE' route_callbacks: - 'views.route_subscriber:routes'
В элементе route_callbacks определяется callback для генерации маршрутов. Им может быть как метод класса (\Drupal\example\Routing\ExampleRoutes::routes), так и метод сервиса (example.service:routes).
Определим динамические маршруты в нашем модуле с помощью кастомного класса ExampleRoutes. В MODULE.routing.yml добавим:
route_callbacks: - '\Drupal\example\Routing\ExampleRoutes::routes'
Теперь опишем класс ExampleRoutes в файле src/Routing/ExampleRoutes.php относительно папки нашего модуля.
namespace Drupal\example\Routing; use Symfony\Component\Routing\Route; /** * Defines dynamic routes. */ class ExampleRoutes { /** * {@inheritdoc} */ public function routes() { $routes = array(); // Declares a single route under the name 'example.content'. // Returns an array of Route objects. $routes['example.content'] = new Route( // Path to attach this route to: '/example', // Route defaults: array( '_controller' => '\Drupal\example\Controller\ExampleController::content', '_title' => 'Hello' ), // Route requirements: array( '_permission' => 'access content', ) ); return $routes; } }
Метод генерации роутов должен возвращать массив объектов класса \Symfony\Component\Routing\Route либо объект класса \Symfony\Component\Routing\RouteCollection.
Переопределение существующих маршрутов
Что же, мы уже много узнали о маршрутизации в Drupal 8. Мы умеем создавать статичные маршруты, умеем снабжать их аргументами, умеем управлять доступом к роутам и даже можем создавать маршруты динамически...однако, тут мы вспоминаем, что в Drupal 7 ещё был такой полезный хук, как hook_menu_alter(). Да, в 8-ой версии его также убрали. Настало время поговорить о таком важном функционале, как переопределение роутов.
Файл MODULE.routing.yml оставляем в покое – более он нам не понадобится. В подпапке src/Routing нашего модуля создаём файл RouteSubscriber.php. В этом файле описываем класс RouteSubsriber, являющийся наследником RouteSubscriberBase. В классе реализуем метод alterRoutes(). Ознакомимся с тем, что у нас получилось.
namespace Drupal\example\Routing; use Drupal\Core\Routing\RouteSubscriberBase; use Symfony\Component\Routing\RouteCollection; /** * Listens to the dynamic route events. */ class RouteSubscriber extends RouteSubscriberBase { /** * {@inheritdoc} */ protected function alterRoutes(RouteCollection $collection) { // Your operations with existing routes here. } }
Изменять созданные роуты можно путём манипуляций с объектом RouteCollection. После создания класса зарегистрируем его в соответствующем сервисе нашего модуля. Сервисы модуля описываются в файле MODULE.services.ym (файл должен располагаться в корневой папке модуля).
services: example.route_subscriber: class: Drupal\example\Routing\RouteSubscriber tags: - { name: event_subscriber }
Вот такой вот простой механизм предлагает нам Drupal 8 вместо hook_menu_alter(). Осталось отметить, что в alterRoutes() можно не только изменять существующие, но и добавлять новые маршруты аналогично route_callbacks.
В виде заключения
С уверенностью сказать, что маршрутизация в Drupal 8 стала проще для понимания и программирования, чем в Drupal 7 нельзя – нужно по крайней мере быть знакомым с принципами ООП, плюсом также было бы знание основ Symfony и YAML. Однако точно можно утверждать, что механизм роутинга стал более функциональным, более гибким и более читаемым. Как и говорилось в начале статьи – возвращаться к старине hook_menu() совсем не хочется.
Материалы для изучения
Комментарии
Отличные статьи, вот только надо бы синтаксис массивов на короткий [] поправить
А как мне определить, есть ли путь в системе? И путь и алиас... В семёрке понятно, здесь не понимаю пока. Помогите. У меня задача дать пользователям возможность назначать путь к своей странице на сайте. Для этого требуется валидация, проверить не конфликтует ли путь с уже имеющимися.
> ознакомиться с документацией роутинга в Symfony
перевод - https://symfony.com.ua/doc/current/routing.html