Drupal 8: Route Subscriber — альтерим роуты

Расширенные возможности маршрутизации в Drupal 8: освойте подписчик события Route Subscriber для детальной настройки маршрутов вашего сайта, что позволит сделать его более удобным и функциональным.

13.04.2018
4 комментария
4 мин.

Все адреса на сайте, так или иначе имеют собственный роут, который отвечает за его обработку, подготовку и вывод. Иногда, может потребоваться, что какой-то роут необходимо скорректировать под свои нужды. Одно дело если роут объявлен вами в вашем же модуле, совсем другое, когда он объвлен сторонним модулем. Хакать чужой код, не наш путь, мы будем делать правильно.

Route Subscriber — это всего лишь абстрактный класс-надстройка, который является Event Subscriber. Если подсмотреть в его код, становится всё предельно ясно. Он подписывается на событие RoutingEvents::ALTER , вызывает свой метод onAlterRoutes() , где он получает коллекцию роутов, и затем передает её ещё одному методу alterRoutes() . Метод alterRoutes() является абстрактным и объявляется в объекте-наследнике со всей логикой. Выходит, что Route Subscriber это лишь базовый объект для ваших подписчиков на события альтера роутов. Т.е. вы можете обойтись без него, но это унифицировано, и значит пользоваться будем тем, что предоставляет ядро.

Объявляется Route Subscriber абсолютно идентично Event Subscriber, так как им и является. Только, в отличии от Event Subscriber, мы не будем реализовывать EventSubscriberInterface , а будем расширять RouteSubscriberBase , который уже и реализует данный интерфейс.

Тут всё очень просто, основная часть разжевана в статье про события, поэтому пойдем быстрым темпом и уже перейдем к примеру. Модуль в примере имеет название dummy.

В качестве примера мы напишем свой Route Subscriber, который будет заменять стандартный путь /user/login на /auth , а также отключать доступ к странице /user/logout — т.е. авторизованные пользователи не смогут выйти штатным средством :)

Я ещё раз напоминаю, Route Subscriber = Event Subscriber, и объявляются они в одном и том же месте: src/EventSubscriber .

src/EventSubscriber/RouteSubscriber.php
<?php

namespace Drupal\dummy\EventSubscriber;

use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\RouteCollection;

/**
 * Dummy route subscriber.
 */
class RouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = parent::getSubscribedEvents();
    $events[RoutingEvents::ALTER] = ['onAlterRoutes', -300];
    return $events;
  }

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    /** @var \Symfony\Component\Routing\Route $route */
    if ($route = $collection->get('user.login')) {
      $route->setPath('/auth');
    }

    if ($route = $collection->get('user.logout')) {
      $route->setRequirement('_access', 'FALSE');
    }
  }

}

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

А теперь пройдемся по порядку и разберем что к чему:

  1. Первое на что обратите внимание, это то, о чём я писал чуть выше. Мы расширяем базовый подписчик extends RouteSubscriberBase , а не реализуем интерфейс подписчика.
  2. Мы подписываемся на событие. Данный метод можно опустить, в таком случае всё будет работать, но вес у вызова будет равен 0. Мы же переопределяем его на -300, так как порядок выполнения от большего к меньшему. Чем меньше - тем позже вызовется. Например, все Route Subscriber из ядра имеют значения от 0 до -210 (самый высокий, -210, у ContentTranslationRouteSubscriber ). Это значит что -300 гарантированно, как минимум, переопределит все возможные изменения из ядра и, возможно, сторонних модулей.
  3. Затем описываем метод alterRoutes() , который обязательно придется описать, так как он является абстрактным в базавом классе. В нём мы всё и меняем.

Давайте посмотрим на оригинальный роут user.login :

core/modules/user/user.routing.yml
user.login:
  path: '/user/login'
  defaults:
    _form: '\Drupal\user\Form\UserLoginForm'
    _title: 'Log in'
  requirements:
    _user_is_logged_in: 'FALSE'
  options:
    _maintenance_access: TRUE

Мы видем что значение path установлено в /user/login . Мы его в своем методе, при помощи setPath() метода, меняем на /auth .

Аналогично и с user.logout :

core/modules/user/user.routing.yml
user.logout:
  path: '/user/logout'
  defaults:
    _controller: '\Drupal\user\Controller\UserController::logout'
  requirements:
    _user_is_logged_in: 'TRUE'

Только у него изначально не устаноавлено требование _access (на самом оно там будет по умолчанию 'TRUE'), мы его устанавливаем на 'FALSE' . Обратите внимание, что булевое значение передается как строка, так как это YAML.

Всё что нам осталось, это объявить наш Route Subscriber в качестве сервиса:

dummy.services.yml
services:
  dummy.route_subscriber:
    class: Drupal\dummy\EventSubscriber\RouteSubscriber
    tags:
      - { name: event_subscriber }

Сбрасываем кэш, и вуаля! Авторизация будет открываться по /auth , а /user/logout будет всем отдавать 403 ошибку.

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

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

  • setPath() : Устанавливает путь или его шаблон для роута.
  • setHost() *: Устанавливает хост или его шаблон для роута.
  • setSchemes() *: Принимает строку или массив из строк, с протоколами, для которых данный роут доступен.
  • setMethoods() : Принимает строку или массив из строк, с методами, которые доступны для данного роута. Например ['GET', 'POST'] .
  • setOptions() : Массив с дополнительными опциями роута.
  • addOptions() : Массив с опциями, которые нужно добавить к текущим. Если значения уже есть, они перезатрутся новыми.
  • setOption($name, $value) : Устанавливает значение конкретной опции.
  • setDefaults() : Массив со значениями по умолчанию.
  • addDefaults() : Массив со значениями по умолчанию, которые нужно добавить к текущим.
  • setDefault($name, $default) : Устанавливает значение конретного значения по умолчанию.
  • setRequirements() : Массив с условиями для роута, которые должны быть выполнены.
  • addRequirements() : Массив с условиями для роута, которые нужно добавить к текущим.
  • setRequirement($key, $regex) : Устанавливает значение для конкретного требования.
  • setCondition() *: Устанавливает условие выполнения роута. Подробнее.

* Методы помеченные звездочками существуют, и, скорее всего, работают. Они из Symfony, и их использование в ядре я не нашел.

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

Более детальные описания для значений можно найти на drupal.org.

Ссылки

Drupal
Drupal 8
Route API

Комментарии

Niklan   пт, 27/04/2018 - 19:51

Либо через IDE сразу смотреть кто наследуется от RouteSubscriberBase, либо искать по кодовой базе RoutingEvents::ALTER и смотреть что там творится. Вот так

Андрей   пн, 23/09/2019 - 15:27

Этим РоутСабскрайбером можно отловить роут, по котором только что перешли и выполнить некоторые свои действия? Если нет, то чем переход по роуту возможно отлавливать в момент перехода по нему? Спасибо!

Станислав   ср, 27/11/2019 - 11:28

Еще раз, большое спасибо за труд - всегда помогают ваши статьи и они для меня в приоритете.

Содержание