Что случилось с hook_menu в Drupal 8?

от автора

В связи с недавним выходом стабильной версии 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 стал "/mail". Я постоянно забываю про слеш в начале, это одна из проблем перехода на новую версию. Слеш в начале, это первое, что нужно проверить, если Ваш код не работает.

В приведенном выше примере класс контроллера назван 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. Я не смог выяснить почему они использовали, другую папку.

На самом деле изменение существующих маршрутов и создание новых динамических маршрутов, достаточно сложные темы, и явно выходят за рамки этой статьи.

Более подробная информация по теме:

ссылка на оригинал статьи http://habrahabr.ru/post/272863/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *