Маршрутизация

Введение

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

Маршрутизация состоит из двух компонентов:

  • Роут (маршрут) — содержит информацию о будущей странице, её путь, какой контроллер использовать, заголовок, требования и т.д.
  • Контроллер — объект, отвечающий за формирование результата по запрошенному маршруту.

Примерная схема работы маршрутов и контроллеров:

Маршруты

Маршрут — описание какого-то URL-пути для сайта с требованиями по его обработке.

Маршруты задаются в специальном файле *.routing.yml, который должен находиться в корне модуля рядом с *.info.yml. Если ваш модуль имеет название foo, в таком случае файл будет называться foo.routing.yml.

В данном файле вы объявляете все необходимые вашему модулю маршруты и описываете их при помощи YAML формата. Например:

foo.content:
  path: '/my-custom-page'
  defaults:
    _controller: '\Drupal\foo\Controller\FooController::content'
    _title: 'Hello World'
  requirements:
    _permission: 'access content'

Данный пример объясняет Drupal, что маршрут с названием foo.content должен быть обработан по адресу /my-custom-page. Когда пользователь обратится по данному маршруту, для него будет произведена проверка, имеет ли он доступ access content. Если не имеет, то пользователь получит ответ HTTP 403 "Недостаточно прав", если имеет, тогда результат, который возвращает контроллер. В качестве заголовка страницы будет использована строка "Hello World".

Свойства маршрутов

Маршруты, объявляемые в *.routing.yml могут содержать следующие свойства:

  • path: (обязательно) Путь маршрута, должен начинаться со слеша path: '/example'. Понимает динамически части, заключенные в фигурные скобки (например: path: '/foo/{argument}/bar').
  • defaults:
    • Обязательный параметр. Должен быть один из перечисленных:
      • _controller: Значение типа callable (callback-функция):
        • Class:method: В формате \Drupal\[module name]\Controller\[ClassName]::[method], например _controller: '\Drupal\foo\Controller\FooController::build'.

          В данном случае Drupal вызовет метод build() класса FooController из неймспейса \Drupal\foo\Controller.

        • Сервис: Позволяет формировать значение из сервиса. Значение задаётся в формате mymodule.service_name:[method], где mymodule.service_name - название сервиса, а [method] - название метода сервиса, который необходимо вызвать. Например _controller: 'mymodule.service_name:build'.

      • _form: Класс, реализующий Drupal\Core\Form\FormInterface.
      • _entity_view: Значение в формате [entity_type].[view_mode]. Маршрут найдёт сущность в пути и отрендерит её в указанном формате отображения. Например _entity_view: node.teaser.
      • _entity_list: Значение в формате [entity_type]. Выводит список сущностей указанного типа при помощи EntityListController данной сущности. Например _entity_list: user выведет список всех пользователей на сайте.
      • _entity_form: Значение в формате [entity_type].[form_view_mode]. Выведет форму редактирования сущности указанного типа, в определенном формате отображения формы. Например _entity_form: node.default выведет форму редактирования материала в стандартном режиме отображения формы.
      • _route: Название маршрута, чьим алиасом вы хотите сделать данный маршрут. Например _route: views.files.page_1.
    • Опциональные параметры.
      • _title: Заголовок страницы на английском языке. Данное значение будет обработано t().
      • _title_arguments: Дополнительные аргументы, передаваемые с заголовком в t().
      • _title_context: Контекст, передаваемый с заголовком в t().
      • _title_callback: Значение типа callable, которое возвращает значение для заголовка. В данном случае, перевод значения ложится на callback-функцию.
  • methods: (опционально) В дополнение к пути, вы можете ограничить типы запросов, на который будет реагировать маршрут. Значение должно быть в виде массива с перечислением всех допустимых типов запроса. Например methods: [GET, POST, UPDATE, DELETE].
  • requirements: (обязательно) Определяет, какие условия должны быть удовлетворены, для того чтобы получить доступ к маршруту. Должно быть одно или более значений (все они должны пройти успешно).
    • _permission: Строковое значение прав доступа, например _permission: 'access content'. Может принимать несколько значений разделяемых запятой , (логический оператор OR) ( например: _permissions: 'access content,access user profiles) или плюсом + (логический оператор AND) ( например: _permissions: 'acess content+access user').
    • _role: Строковое значение роли, например _role: 'administrator'. Вы также можете использовать указание нескольких ролей, разделяя их запятой , (логический оператор OR), или плюсом + (логический оператор AND). Рекомендуется использовать _permission, вместо _role, так как на различных сайтах роли могут иметь различные настройки и смыслы.
    • _access: Установите значение равное 'TRUE', чтобы не иметь никаких проверок и результат отдавался при любых условиях.
    • _entity_access: Значение в формате [entity_type].[operation]. Доступ будет предоставлен только если пользователь проходит проверки определенных прав, конкретного типа сущности. Например _entity_access: menu.edit.
    • _format: Производит проверку типа запроса. Например, указав _format: json, маршрут будет обрабатываться, только если указать ?_format=json при обращении по пути маршрута.
    • _content_type_format: Производит проверку по значению заголовка запроса Content-Type. Например, если зададите _content_type_format: json, то маршрут отработает только если при запросе был передан заголовок Content-Type: application/json.
    • _custom_access: Позволяет задать метод, который будет вызван для проверки прав доступа. Например _custom_access: '\Drupal\foo\Controller\FooController::access'
    • _module_dependencies: Производит проверку по модулю. Вы также можете использовать указание нескольких модулей, разделяя их запятой , (логический оператор OR), или плюсом + (логический оператор AND). Если указанный модуль отключен, то маршрут будет недоступен. Если модуль имеет зависимости, они также автоматически подразумевают проверку, поэтому нет никакого смысла указывать все зависимости.
    • _csrf_token: Значение равное 'TRUE' произведет проверку X-CSRF-Token из заголовка запроса, для проверки, является ли запрос валидным. Используйте, для того чтобы убедиться что запрос валидный и не изменен.
    • Любой другой существующий Access Check сервис. Для более подробной информации обратитесь к разделу контроля доступа маршрутов.
  • options: (опционально) Дополнительные настройки, с которыми маршрут должен взаимодействовать. Распространенные:
    • _admin_route: Помечает данный маршрут как часть административного интерфейса. Для всех маршрутов начинающихся с /admin, данное значение автоматически устанавливается в 'TRUE'.
    • _auth: Доступные способы авторизации для маршрута. По умолчанию доступны все варианты авторизации с значением global. Данное значение ограничивает варианты авторизации только для указанных. Например _auth: ['basic_auth', 'cookie']. Разработчики могут объявлять свои собственные способы авторизации при помощи Authentication API.
    • _maintenance_access: Значение равное 'TRUE' позволяет отрабатывать данному маршруту в режиме обслуживания.
    • no_cache: Значение равное 'TRUE' отключает кеширование ответа по этому маршруту.
    • parameters: Используются для передачи данных контроллеру. Для более подробной информации обратитесь к разделу «параметры маршрутов».

Контроллеры

Контроллер — обрабатывает входящие запросы конкретного маршрута.

Drupal 9 использует компонент Symfony - HTTP Kernel, который получает запрос и опрашивает другие системы в надежде получить от них результат, который необходимо вернуть пользователю. Результатом является один из объектов, наследующих Symfony\Component\HttpFoundation\Response или непосредственно данный объект. В случае с Drupal, мы также имеем возможность вернуть render array в качестве результата. Drupal обнаружит его, проведет все необходимые обработки и операции с ним, а затем вернет корректный ответ при помощи Response.

Например, если мы хотим создать контроллер для примера из маршрутизации выше, то результат может быть примерно таким:

<?php

namespace Drupal\foo\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * An foo controller.
 */
class FooController extends ControllerBase {

  /**
   * Returns a render-able array for a test page.
   */
  public function content() {
    $build = [
      '#markup' => $this->t('Hello World!'),
    ];
    return $build;
  }

}

В результате чего, Drupal вернет в качестве контента страницы строку "Hello World" на языке пользователя.

Параметры маршрутов

Параметр маршрута, также известный как slug — это переменная в пути маршрута, которая передаётся в качестве аргумента контроллеру.

Переменная маршрута заключаются в фигурные скобки {…} и должна иметь уникальное имя в пределах маршрута.

Примитивные (сырые) параметры

Пример маршрута с объявленным параметром:

blog.page:
  path: '/blog/{slug}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::page'

В примере выше параметром маршрута является {slug}. Значение {slug} будет передаваться контроллеру в качестве аргумента. Параметр {slug} является «сырым» и будет передан контроллеру «как есть» в виде строки.

Для того чтобы принять значение параметра в контроллере, в методе, отвечающим за подготовку ответа, вы должны объявить одноимённый параметр для метода ::page().

public function page(string $slug) { }

Обратившись по пути /blog/foo, $slug получит значение foo.

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

blog.page:
  path: '/blog/{foo}/{bar}/example-{baz}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::page'

Параметры с валидацией

Иногда может потребоваться, чтобы параметр удовлетворял определённым требованиям, например, являлся строкой или числом.

Для того чтобы указать какие параметры допустимы для маршрута, используется свойство requirements, где в качестве ключа указывается название параметра, а в качестве значения — регулярное выражение PHP, которому должно соответствовать значение параметра.

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

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

blog.list:
  path: '/blog/{page}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::list'
  requirements:
    # '\d+' — регулярное выражение «одно или более число».
    slug: \d+

blog.page:
  path: '/blog/{slug}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::page'

В примере выше показано, что если обратиться по пути /blog/* передав в качестве параметра число, то будет вызван BlogController::list(), а если валидация на число не пройдёт, то будет вызван BlogController::page().

Для простых проверок, требования могут быть указаны прямо в параметре пути используя следующий синтаксис: {parameter_name<requirements>}. Это позволяет сделать маршрут более компактным, но уменьшает его читаемость.

blog.list:
  path: '/blog/{page<\d+>}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::list'

Опциональные параметры

В примере выше, для маршрута blog.list мы указали путь /blog/{page}. Если пользователь обратится по пути /blog/1, он будет корректно обработан, но если обратиться по пути /blog, то проверка не пройдёт, ведь значение не будет передано.

Вы можете объявить третий маршрут /blog, который будет обрабатывать этот сценарий, но скорее всего, там будет тоже самое что и при обращении к /blog/1.

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

blog.list:
  path: '/blog/{page}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::list'
    page: 1
  requirements:
    slug: \d+

Теперь, если пользователь посетит /blog, то будет найден маршрут blog.list, а в качестве $page будет передано значение по умолчанию — 1.

Как и в случае с валидацией параметров, значение по умолчанию может быть указано сразу в пути, для эттого используется следующий синтаксис: {parameter_name?default_value}. Этот синтаксис совместим с валидацией, поэтому вы можете использовать их одновременно:

blog.list:
  path: '/blog/{page<\d+>?1}'
  defaults:
    _controller: '\Drupal\blog\Controller\BlogController::list'

Конвертирование параметров

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

Каждому параметру можно указать какой-либо явный тип, а на основе этого типа и переданного значения, конвертеры параметров предоставляют конечный результат. Данные требования описываются в свойстве options.parameters.

Самое распространённое применение в Drupal — конвертация ID сущности в саму сущность, то есть, её загрузку.

example.node_converter:
  path: '/example/{node}'
  defaults:
    _controller: '\Drupal\example\Controller\NodeConverterController::build'
  options:
    parameters:
      node:
        type: entity:node

В примере выше, при обращении по адресу /example/1, будет загружена сущность node с ID равным 1 и передана в качестве аргумента методу контроллера. В данном случае, метод уже должен ожидать конечный тип от используемого конвертера параметров, вместо сырого значения:

public function build(?NodeInterface $node) {}

Конвертер в примере выше является универсальным для любой сущности Drupal и имеет синтаксис entity:[entity_type_id].

Магические параметры

Перечисленные ниже специальные объекты могут передаваться методу контроллера. Чтобы их получать, в методе контроллера должны быть объявлены параметры с соответствующим типом:

  • \Symfony\Component\HttpFoundation\Request: необработанный объект запроса Symfony. Обычно используется если контроллеру необходимо получить значения параметров запроса. Глобальная переменная $_GET доступна в методе $request->query (возвращает ParameterBag, не массив), информация о сессии может быть получена с помощью метода $request->getSession() (возвращает SessionInterface).
  • \Drupal\Core\Routing\RouteMatchInterface: в переменную будет записана информация соответствия маршрутов (данные о результате выполнения процесса роутинга). Например, чтобы получить объект текущего маршрута, можно использовать метод $route_match->getRouteObject().
  • \Psr\Http\Message\ServerRequestInterface: Необработанный запрос, представленный с использованием формата PSR-7 ServerRequest.

Параметры для маршрутов с формой

Если в качестве обработчика для маршрута используется форма, то параметры будут переданы в ::buildForm() после основных.

foo.user_form:
  path: '/example/{foo}/{bar}'
  defaults:
    foo: 'default_foo_value'
    _form: '\Drupal\foo\Form\ExampleForm'
public function buildForm(array $form, FormStateInterface $form_state, string $foo, string $bar = '') { }

Важно: каждому параметру маршрата формы необходимо указывать значение по умолчанию либо на уровне объявления метода ({bar}), либо на уровне маршрута ({foo}).

Контроль доступа

Каждый маршрут в Drupal должен иметь, по крайней мере одну, проверку доступа. Данные требования указываются в requirements разделе маршрута.

В данном разделе маршрута указываются какие Access Check сервисы будут использованы для проверки доступа. Вы можете использовать как стандартные, предоставляемые с ядром, проверки доступа, так и создавать свои собственные.

Использование _custom_access

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

Пример использования:

foo.content:
  path: '/my-custom-page'
  defaults:
    _controller: '\Drupal\foo\Controller\FooController::content'
    _title: 'Hello World'
  requirements:
    _custom_acess: '\Drupal\foo\Controller\FooController::access'

В качестве результата данный метод должен возвращать экземпляр объекта Drupal\Core\Access\AccessResultInterface.

Пример проверки прав доступа:

<?php

namespace Drupal\foo\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;

/**
 * An foo controller.
 */
class FooController extends ControllerBase {

  /**
   * Returns a render-able array for a test page.
   */
  public function content() {
    $build = [
      '#markup' => $this->t('Hello World!'),
    ];
    return $build;
  }

  /**
   * Returns access result for controller.
   */
  public function access(AccountInterface $account) {
    return AccessResult::allowedIf(
      $account->hasPermission('do example things')
      && $account->hasPermission('do other things')
    );
  }

}

Написание Access Check сервиса

Разработчики могут описывать свои access check сервисы. Например _custom_access выше тоже является access check сервисом.

Отличие access check от _custom_access в том, что собственный сервис вы можете применять к любым маршрутам. Он должен быть максимально универсальным в пределах тех маршрутов, к которым предназначен, тогда как в _custom_access, скорее всего, вы напишите индивидуальную проверку доступа, которая касается только данного маршрута и больше никакого.

Access check является сервисом и поддерживает все его возможности. Объекты, используемые для access check сервисов рекомендуется создавать в src/Access.

Пример объекта с проверкой доступа:

<?php
namespace Drupal\foo\Access;

use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;

/**
 * Checks access for displaying configuration translation page.
 */
class FooAccessCheck implements AccessInterface {

  /**
   * A custom access check.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   Run access checks for this account.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  public function access(AccountInterface $account) {
    // Check permissions and combine that with any custom access checking needed. Pass forward
    // parameters from the route and/or request as needed.
    return ($account->hasPermission('do example things') && $this->someOtherCustomCondition()) ? AccessResult::allowed() : AccessResult::forbidden();
  }

}

Метод должен возвращаться экземпляр объекта Drupal\Core\Access\AccessResultInterface в качестве результата проверки прав доступа.

В качестве аргументов метод проверки доступа принимает:

  • Первым делом все динамические части маршрута {name}. Они должны идти в том же порядке что и в path и теми же названиями.
  • Любой из данных объектов в любом порядке:
    • \Symfony\Component\HttpFoundation\Request $request.
    • \Symfony\Component\Routing\Route $route
    • \Drupal\Core\Routing\RouteMatch $route_match
    • \Drupal\Core\Session\AccountInterface $account

Далее вам необходимо объявить данный объект как сервис, например:

services:
  foo.access_checker:
    class: Drupal\foo\Access\FooAccessCheck
    tags:
      - { name: access_check, applies_to: _foo_access_check }

После чего вы можете применять его к любым маршрутам на сайте:

foo.content:
  path: '/my-custom-page'
  defaults:
    _controller: '\Drupal\foo\Controller\FooController::content'
    _title: 'Hello World'
  requirements:
    _foo_access_check: 'TRUE'

Изменения в релизах

  • Drupal 9.2.0 (02.06.2021): Параметр _entity_bundles помечен устаревшим.

Ссылки

🌱 Помогите нам сделать документацию лучше!

Вся документация Druki с отрытым исходным кодом. Нашли ошибку или неточность? Создайте pull request.

Редактировать текущий документ Обсудить улучшение

Или узнайте как контрибутить.

🤔 По-прежнему нужна помощь?

Не нашли ответа на свой вопрос? Попросите помощи у сообщества!

Задайте вопрос на GitHub Смотрите другие ресурсы сообщества