Один из самых используемых хуков 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() совсем не хочется.

Материалы для изучения

  1. Структура маршрутов
  2. Использование параметров в роутах
  3. Старина Upcasting
  4. Динамические маршруты
  5. Переопределение существующих роутов

Комментарии

Владимир

> ознакомиться с документацией роутинга в Symfony

перевод - https://symfony.com.ua/doc/current/routing.html

Василий

Отличные статьи, вот только надо бы синтаксис массивов на короткий [] поправить

Даниил

А как мне определить, есть ли путь в системе? И путь и алиас... В семёрке понятно, здесь не понимаю пока. Помогите. У меня задача дать пользователям возможность назначать путь к своей странице на сайте. Для этого требуется валидация, проверить не конфликтует ли путь с уже имеющимися.

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