
Что случилось с hook_menu в Drupal 8? |
21.06.16 12:49 |
Перепост с habrahabr.ru. Спасибо за вклад @ZapevalovAnton
В 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(<strong></strong> '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() { В 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. Перевод в контроллере доступен, через метод $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 { Пути с аргументами (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) { Давайте подправим наш 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 { Маршруты с необязательными аргументами (Paths With Optional Arguments) Приведенный выше пример, будет работать корректно, только тогда, когда переданы оба аргумента. То есть ни "/main", ни "/main/first" работать не будет, только "/main/first/second". Если Вы хотите, чтобы все три маршрута, были работоспособными, Вам необходимо внести несколько изменений в YAML файл, а именно в разделе defaults, добавить значения по умолчанию, для передаваемых аргументов: example.main_page_controller:
path: '/main/{first}/{second}' defaults:<strong></strong> _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; class MainPageController extends ControllerBase { Управление доступом (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; /** /** 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 Еще раз благодарим @ZapevalovAnton за хорошую статью по Drupal 8. |