📜 ⬆️ ⬇️

What happened to hook_menu in Drupal 8?



In connection with the recent release of the stable version of Drupal 8, I decided to make a small contribution and translate a short article. This is a very free translation of the article What Happened to Hook_Menu in Drupal 8? from Lullabot'ov. I hope that someone will come in handy.

In Drupal 7 and earlier, hook_menu was like a Swiss knife. He was responsible for almost everything: page paths, menu handlers, tabs and local tasks, contextual links, access control, arguments and parameters, form handlers, and even set menu items. In my book, this is the most frequently used hook of all. I don't know, there was not a single module in which I would not implement hook_menu.
')
But, in Drupal 8, everything changed. This very important hook is no longer there, and now all these tasks are solved separately using the YAML system of files, in which you need to describe the metadata about each element and the corresponding PHP classes that provide logic.

The new system makes sense, but it may seem confusing, especially since the API has changed several times over the lengthy development of Drupal 8, and the documentation is currently not true. This article will explain how the new system works.

I also want to talk about the situations that I encountered during the transfer of my module from Drupal 7 to Drupal 8 and give examples of the code before and after the transfer.

Custom pages

In the simplest case, hook_menu was used to create custom pages along a specified path. In Drupal 8, paths are managed using the MODULE.routing.yml file, which describes the mapping of paths (routes) and classes of controllers containing data processing logic along this path. Each class is inherited from the base controller . In Drupal 7, such logic controllers could be in MODULE.pages.inc .

Sample code in 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'); } 

In Drupal 8, we describe the route information (path) in the MODULE.routing.yml file. Each route has a name that is not responsible for anything, but is simply a unique identifier of the route, and must be prefixed with the name of your module in order to avoid name conflicts. You can find in the documentation that there were once discussions about using _content or _form suffixes instead of _controller in YAML files, but this was later abandoned and now you always need to use the _controller suffix to determine the corresponding controller.

 example.main_page_controller: path: '/main' defaults: _controller: '\Drupal\example\Controller\MainPageController::mainPage' _title: 'Main Page' requirements: _permission: 'access content' 

Note the use of a slash at the beginning. In Drupal 7, the path would be “main” , and in Drupal 8 it would be “/ main” . I constantly forget about the slash in the beginning, this is one of the problems of switching to the new version. A forward slash is the first thing to check if your code is not working.

In the example above, the controller class is named MainPageController.php, and it is located in the MODULE / src / Controller / MainPageController.php file. The file name must match the class name of the controller, and all the controllers of your module must be in the / src / Controller folder. This place is described in the PSR-4 standard, which is adopted in Drupal 8. In principle, everything that lies in the expected place for / src for Drupal will be automatically loaded if necessary, without using module_load_include () , or listing in the .info file as we did this in Drupal 7.

The method in the controller that will control this route can have any name. In my example, I used the arbitrary name of mainPage . Most importantly, the method that we will use in our controller must correspond to what we have described in the YAML file, in the _controller directive, as class_name :: method .

One controller can manage one or more routes, since each has its own handler (callback) and its own entry in the YAML file. For example, in the core, the nodeController controller manages four routes listed in node.routing.yml .
The controller should always return an array for (render array), not text or HTML, as it was in Drupal 7.

Translation in the controller is available via the $ this-> t () method instead of the t () function. This is done because StringTranslationTrait has been added to BaseController . A good article on how PHP traits such as translations work in Drupal 8 on 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!'), ]; } } 

Paths with arguments

For some routes, arguments (parameters) are needed. If my page had a couple of arguments, then in Drupal 7 it would look like this:

 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'); } 

Let's fix our YAML file for Drupal 8, and see how the transfer of arguments looks like there:

 example.main_page_controller: path: '/main/{first}/{second}' defaults: _controller: '\Drupal\example\Controller\MainPageController::mainPage' _title: 'Main Page' requirements: _permission: 'access content' 

And our controller will look like this (parameters are passed by arguments to the method):

 /** * @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

The above example will work correctly only when both arguments are passed. That is, neither "/ main" nor "/ main / first" will work, only "/ main / first / second" . If you want all three routes to work, you need to make a few changes to the YAML file, namely in the defaults section, add default values ​​for the arguments to be passed:

 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 (Restricting Parameters)

After we have passed the parameters, we need to describe it in the YAML file of the module, which is allowed in these parameters. The example below shows that the parameter named $ first can only contain the values 'Y' or 'N' , and the parameter named $ second must be a number. Any parameters passed that do not comply with these rules will return a page with the code 404 Not found .

To learn more about setting up routes, you can refer to the symfony documentation .

 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+ 

Passing Entities in Parameters (Entity Parameters)

Just as in Drupal 7, when you create a route, you can pass an entity object to it, and not just its identifier. This is called "upcasting" (cast to base type). In the seventh version, instead of the simple “%” sign, you would need to specify the keyword “% node” . In Drupal 8, you just need to use the name of an object as a parameter name, for example, {node} or {user} .

 example.main_page_controller: path: '/node/{node}' defaults: _controller: '\Drupal\example\Controller\MainPageController::mainPage' _title: 'Node Page' requirements: _permission: 'access content' 

Such “upcasting” will work only when the object of the transmitted type is present as a parameter in your controller. Otherwise, there will be just the value of the passed parameter.

JSON handlers (JSON Callbacks)

All that we have considered above, as a result returns already ready HTML code. That is, the array that you return in the handler method will be automatically converted by the system into HTML code. But what if you need to return not HTML, but JSON? I had a problem finding information on this topic. In the old documentation it was written that you need to add _format: json to the requirements section of your YAML file, but this is not at all necessary if you want to provide a different format along the same route.

Create an array consisting of the values ​​you want to return and return it as a JsonResponse object. Do not forget to add “use Symfony \ Component \ HttpFoundation \ JsonResponse” in the upper part of your controller class to make this object available.

 /** * @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

In Drupal 7, hook_menu also allowed access control. Now access control is carried out in the MODULE.routing.yml file. There are several ways to control access:

Allow absolutely access for everyone on this route:

 example.main_page_controller: path: '/main' requirements: _access: 'TRUE' 

Access restriction, for example for those who have access to content, “access content” (access to content):

 example.main_page_controller: path: '/main' requirements: _permission: 'access content' 

Restrictions on roles, for example, only for those users who have the “admin” role:

 example.main_page_controller: path: '/main' requirements: _role: 'admin' 

Restriction on the interaction with the entity, for example, only when the user is allowed to edit the material (the entity must be passed as an argument along the way):

 example.main_page_controller: path: '/node/{node}' requirements: _entity_access: 'node.edit' 

Access control is described in more detail in the documentation .

hook_menu_alter

But what if we want to change an existing route (which was created by the kernel or another module)? In Drupal 7, hook_menu_alter was for this , but it isn't in Drupal 8 either. At the moment it is more difficult than it was before. The simplest example I could find was in the Node module, it changed the route created by the System module.

The file with the MODULE / src / Routing / CLASSNAME.php class is inherited from RouteSubscriberBase and works as follows. It finds the route and modifies it using the alterRoutes () method.

 /** * @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 } 

Most core modules describe a class inherited from RouteSubscriber in the MODULE / src / EventSubscriber / CLASSNAME.php folder instead of MODULE / src / Routing / CLASSNAME.php . I could not figure out why they used another folder.

In fact, changing existing routes and creating new dynamic routes are quite complex topics, and are clearly beyond the scope of this article.

More information on the topic:

Source: https://habr.com/ru/post/272863/


All Articles