В связи с недавним выходом стабильной версии Drupal 8, решил внести свой небольшой вклад, и перевести небольшую статью. Это очень вольный перевод статьи What Happened to Hook_Menu in Drupal 8? от Lullabot'ов. Надеюсь, что кому-нибудь пригодится.
В Drupal 7 и более ранних версиях, hook_menu был как швейцарский нож. Он отвечал практически за все: пути страниц, обработчики меню, вкладки и локальные задачи, контекстные ссылки, управление доступом, аргументы и параметры, обработчики форм, и даже устанавливал пункты меню. В моей книге, это самый часто используемый hook из всех. Я не знаю, ни одного модуля в котором, я не реализовывал бы hook_menu.
Но, в Drupal 8 все изменилось. Этого очень важного hook'a больше нет, и теперь все эти задачи решаются отдельно, используя систему YAML файлов, в которых нужно описать метаданные о каждом элементе и соответствующие ему PHP классы, которые обеспечивают логику.
В новой системе есть смысл, но она может показаться запутанной, тем более что API менялся несколько раз, в течении длительной разработки Drupal 8, и документация в настоящее время, не соответствует действительности. В этой статье будет рассказано как работает новая система.
Так же я хочу рассказать о ситуациях с которыми я столкнулся, во время переноса своего модуля с Drupal 7 на Drupal 8 и приведу примеры кода, до и после переноса.
Пользовательские страницы (Custom pages)
В самом простом случае hook_menu использовался, для создания пользовательских страниц по заданному пути. В Drupal 8, пути управляются с помощью файла MODULE.routing.yml, в котором описывается соответствие путей (маршрутов) и классов контроллеров, содержащих логику обработки данных по этому пути. Каждый класс наследуется от базового контроллера. В Drupal 7 такие логические контроллеры, могли находиться в MODULE.pages.inc.
Пример кода в Drupal 7:
function example_menu() {
$items = array();
$items['main'] = array(
'title' => 'Main Page',
'page callback' => example_main_page,
'access arguments' => array('access content'),
'type' => MENU_NORMAL_ITEM,
'file' => 'MODULE.pages.inc'
);
return $items;
}
function example_main_page() {
return t(‘Something goes here’);
}
В Drupal 8, информацию о маршруте (пути) мы описываем в файле MODULE.routing.yml. У каждого маршрута есть название, которое ни за что не отвечает, а просто является уникальным идентификатором маршрута, и должно быть с префиксом из имени Вашего модуля, чтобы избежать конфликтов имен. В документации можно найти, что когда-то были обсуждения об использовании _content или _form суффиксов вместо _controller в YAML файлах, но позже от этого отказались и теперь всегда нужно использовать суффикс _controller, чтобы определить соответствующий контроллер.
example.main_page_controller:
path: '/main'
defaults:
_controller: '\Drupal\example\Controller\MainPageController::mainPage'
_title: 'Main Page'
requirements:
_permission: 'access content'
Обратите внимание на использование слеша в начале. В Drupal 7 путь был бы «main», а в Drupal 8 стал "/main". Я постоянно забываю про слеш в начале, это одна из проблем перехода на новую версию. Слеш в начале, это первое, что нужно проверить, если Ваш код не работает.
В приведенном выше примере класс контроллера назван MainPageController.php, и располагается он в файле MODULE/src/Controller/MainPageController.php. Имя файла должно соответствовать имени класса контроллера, и все контроллеры Вашего модуля должны лежать в папке /src/Controller. Это место описано в стандарте PSR-4, который принят в Drupal 8. В принципе все что лежит в ожидаемом для Drupal’a месте /src, будет автоматически загружено при необходимости, без использования module_load_include(), или перечисления в .info файле, как мы это делали в Drupal 7.
Метод в контроллере, который будет управлять этим маршрутом, может иметь любое имя. В своем примере я использовал произвольное название mainPage. Самое главное, что метод который мы будем использовать в нашем контроллере должен соответствовать тому, что мы описали в YAML файле, в директиве _controller как class_name::method.
Один контроллер может управлять одним и более маршрутами, так как у каждого есть свой обработчик (callback) и своя запись в YAML файле. Например, в ядре контроллер nodeController управляет четырьмя маршрутами, перечисленными в node.routing.yml.
Контроллер всегда должен возвращать массив для (render array), а не текст или HTML, как это было в Drupal 7.
Перевод в контроллере доступен, через метод $this->t() вместо функции t(). Так сделано потому что в BaseController добавлен StringTranslationTrait. Хорошая статья о том, как PHP трейты, такие как переводы работают в Drupal 8 на DrupalizeMe.
/**
* @file
* Contains \Drupal\example\Controller\MainPageController.
*/
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
class MainPageController extends ControllerBase {
public function mainPage() {
return [
'#markup' => $this->t('Something goes here!'),
];
}
}
Пути с аргументами (Path with arguments)
Для некоторых маршрутов, нужны аргументы (параметры). Если бы у моей страницы была бы пара аргументов, то в Drupal 7 это выглядело бы так:
function example_menu() {
$items = array();
$items[‘main/%/%’] = array(
'title' => 'Main Page',
'page callback' => 'example_main_page',
'page arguments' => array(1, 2),
'access arguments' => array('access content'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
function example_main_page($first, $second) {
return t(‘Something goes here’);
}
Давайте подправим наш YAML файл для Drupal 8, и посмотрим как передача аргументов выглядит там:
example.main_page_controller:
path: '/main/{first}/{second}'
defaults:
_controller: '\Drupal\example\Controller\MainPageController::mainPage'
_title: 'Main Page'
requirements:
_permission: 'access content'
А наш контроллер будет выглядеть так (параметры переданы аргументами в метод):
/**
* @file
* Contains \Drupal\example\Controller\MainPageController.
*/
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
class MainPageController extends ControllerBase {
public function mainPage($first, $second) {
// Do something with $first and $second.
return [
'#markup' => $this->t('Something goes here!'),
];
}
}
Маршруты с необязательными аргументами (Paths With Optional Arguments)
Приведенный выше пример, будет работать корректно, только тогда, когда переданы оба аргумента. То есть ни "/main", ни "/main/first" работать не будет, только "/main/first/second". Если Вы хотите, чтобы все три маршрута, были работоспособными, Вам необходимо внести несколько изменений в YAML файл, а именно в разделе defaults, добавить значения по умолчанию, для передаваемых аргументов:
example.main_page_controller:
path: '/main/{first}/{second}'
defaults:
_controller: '\Drupal\example\Controller\MainPageController::mainPage'
_title: 'Main Page'
first: ''
second: ''
requirements:
_permission: 'access content'
Ограничения в параметрах (Restricting Parameters)
После того как мы передали параметры, нужно описать в YAML файле модуля, что в этих параметрах разрешено передавать. В примере приведенном ниже показано, что параметр с именем $first может содержать только значения 'Y' или 'N', а параметр с именем $second, обязательно должен быть числом. Любые переданные параметры, которые не соответствуют этим правилам вернут страницу с кодом 404 Not found.
Чтобы узнать больше о настройке маршрутов Вы можете обратиться к документации Symfony.
example.main_page_controller:
path: '/main/{first}/{second}'
defaults:
_controller: '\Drupal\example\Controller\MainPageController::mainPage'
_title: 'Main Page'
first: ''
second: ''
requirements:
_permission: 'access content'
first: Y|N
second: \d+
Передача сущностей в параметрах (Entity Parameters)
Так же, как и в Drupal 7, при создании маршрута, в него можно передать объект сущности, а не просто ее идентификатор. Это называется «upcasting» (приведение к базовому типу). В седьмой версии для этого нужно было бы вместо простого знака "%", указать ключевое слово "%node". В Drupal 8, нужно в качестве имени параметра просто использовать имя объекта, например, {node} или {user}.
example.main_page_controller:
path: '/node/{node}'
defaults:
_controller: '\Drupal\example\Controller\MainPageController::mainPage'
_title: 'Node Page'
requirements:
_permission: 'access content'
Такой «upcasting», будет работать только тогда, когда в Вашем контроллере в качестве параметра присутствует объект передаваемого типа. В противном случае, там будет просто значение переданного параметра.
JSON обработчики (JSON Callbacks)
Все то что мы рассмотрели выше, в итоге возвращает уже готовый HTML код. То есть массив который Вы возвращаете в методе обработчике, автоматически будет сконвертирован системой в HTML код. Но что если Вам нужно вернуть не HTML, а JSON? У меня возникла проблема с поиском информации на эту тему. В старой документации было написано, что нужно добавить _format:json в секцию requirements, Вашего YAML файла, но это совсем не обязательно, если Вы хотите предоставить другой формат, по этому же маршруту.
Создайте массив состоящий из значений, которые Вы хотите вернуть и верните его как JsonResponse объект. Не забудьте добавить «use Symfony\Component\HttpFoundation\JsonResponse» в верхнею часть Вашего класса контроллера, чтобы этот объект был доступен.
/**
* @file
* Contains \Drupal\example\Controller\MainPageController.
*/
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
class MainPageController extends ControllerBase {
public function mainPage() {
$return = array();
// Create key/value array.
return new JsonResponse($return);
}
}
Управление доступом (Access Control)
В Drupal 7, hook_menu так же позволял управлять доступом. Сейчас контроль доступа осуществляется в MODULE.routing.yml файле. Есть несколько способов управления доступом:
Разрешить доступ абсолютно для всех по этому маршруту:
example.main_page_controller:
path: '/main'
requirements:
_access: 'TRUE'
Ограничение по праву доступа, например для тех у кого есть доступ к содержимому, «access content» (доступ к содержимому):
example.main_page_controller:
path: '/main'
requirements:
_permission: 'access content'
Ограничение по роли, например только для тех пользователей у которых есть роль «admin»:
example.main_page_controller:
path: '/main'
requirements:
_role: 'admin'
Ограничение по взаимодействию с сущностью, например только когда пользователю разрешено редактировать материал (сущность должна быть передана аргументом в пути):
example.main_page_controller:
path: '/node/{node}'
requirements:
_entity_access: 'node.edit'
Управление доступом более подробно описано в документации.
hook_menu_alter
А что если мы хотим изменить уже существующий маршрут (который был создан ядром или другим модулем)? В Drupal 7 для этого был hook_menu_alter, но в Drupal 8 его тоже нет. На данный момент это сложнее, чем было раньше. Самый простой пример который я смог найти, находился в модуле Node, он изменял маршрут, созданный модулем System.
Файл с классом MODULE/src/Routing/CLASSNAME.php наследуется от RouteSubscriberBase и работает следующим образом. Он находит маршрут и изменяет его используя метод alterRoutes().
/**
* @file
* Contains \Drupal\node\Routing\RouteSubscriber.
*/
namespace Drupal\node\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) {
// As nodes are the primary type of content, the node listing should be
// easily available. In order to do that, override admin/content to show
// a node listing instead of the path's child links.
$route = $collection->get('system.admin_content');
if ($route) {
$route->setDefaults(array(
'_title' => 'Content',
'_entity_list' => 'node',
));
$route->setRequirements(array(
'_permission' => 'access content overview',
));
}
}
}
services:
node.route_subscriber:
class: Drupal\node\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
Большинство основных модулей описывают класс наследующийся от RouteSubscriber в папке MODULE/src/EventSubscriber/CLASSNAME.php вместо MODULE/src/Routing/CLASSNAME.php. Я не смог выяснить почему они использовали, другую папку.
На самом деле изменение существующих маршрутов и создание новых динамических маршрутов, достаточно сложные темы, и явно выходят за рамки этой статьи.
Более подробная информация по теме:
Комментарии (3)
andypost
11.12.2015 14:25В оригинальной статье ошибка относительно _form vs _content — батл был именно за _content (который в итоге и выкинули), так что _form используется всегда, когда нужно вывести форму, а не результат контроллера
blueboar2
Я правильно понимаю, что не только слеш, а еще и слово поменялось?
ZapevalovAnton
Sorry… Поправил.