В связи с недавним выходом стабильной версии 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. Я не смог выяснить почему они использовали, другую папку.
На самом деле изменение существующих маршрутов и создание новых динамических маршрутов, достаточно сложные темы, и явно выходят за рамки этой статьи.
Более подробная информация по теме:
- https://api.drupal.org/api/drupal/8
- https://www.drupal.org/developing/api/8/routing
- http://symfony.com/doc/current/book/routing.html
- https://drupalize.me/videos/symfony-routing?p=1856
- https://drupalize.me/videos/introduction-yaml
- https://www.drupal.org/node/2092643
ссылка на оригинал статьи http://habrahabr.ru/post/272863/
Добавить комментарий