Drupal's menu system API underwent a large number of significant changes in Drupal 8, just like many other areas in Drupal's newest version.
The way we define what piece of PHP logic should respond to a particular path, e.g. /example, is no longer part of the menu; instead it is now defined via the route system. The menu system - as it should logically - is now used to define how a particular page (a route) fits into the menu system, tabs, and contextual links. Let’s take a quick look at how the familiar tasks of defining and altering such entries have changed.
Routes and menu
In Drupal 7 hook_menu()
had two tasks:
- define a path, with the necessary resources (access control, callback definitions)
- define how the given path will be represented in the menu system.
If you think about it, these are fundamentally different tasks. Not only were we tied to the restriction of treating the routes uniquely if they have a path, but we did two things at one place. We had to abuse the MENU_CALLBACK
menu type to register a path, which did not show up in the menu system. Defining and altering the menu items also became somewhat difficult, as soon as more complex changes were required.
Define routes and the logic
Letting Drupal know about our route and menu definitions is now handled via YAML files, instead of by hook_menu()
implementations. This is similar to the way other APIs are handled in Drupal 8. You need to create and populate the following files, respective of the type of definition you want to create:
MODULENAME.routing.yml
- defines the routes and their controllers (the PHP logic responding to the calls as well as other requirements, which when met, allow you to access the resource). Equivalent to Drupal 7'sMENU_CALLBACK
- MODULENAME.links.menu.yml - defines the links you may want to place into the standard menu system, equivalent to:
MENU_SUGGESTED_ITEM
MENU_NORMAL_ITEM
- MODULENAME.links.task.yml - defines the tabs you want to put on a page. Equivalent to:
MENU_DEFAULT_LOCAL_TASK
MENU_LOCAL_TASK
- MODULENAME.links.actions.yml - exposes the route as an action. Equivalent to:
MENU_LOCAL_ACTION
- MODULENAME.links.contextual.yml - defines contextual links. Equivalent to:
- menu items using the ‘context’ property and
MENU_CONTEXT_INLINE
- menu items using the ‘context’ property and
For default tabs, simply chose the same route name for your tab entry as the route itself and it will become the tab which is active when the route is accessed.
For further reading, the documentation on drupal.org is very good, a highly recommended read.
Responding to a route
The output for a given response is generated by a page controller - a class with a method that returns a build array. The class is defined by the _controller
key of a route in MODULENAME.routing.yml. Here is an example from the excellent documentation page on drupal.org:
example.content:
path: '/example'
defaults:
_controller: '\Drupal\example\Controller\ExampleController::content'
_title: 'Hello World'
requirements:
_permission: 'access content'
where the “::” tells us which method will be invoked from the given class, allowing us to use a single class for various pages.
Since it is fairly typical of Drupal to display just a form on a given route (e.g. configuration pages), there is a shortcut to that, similar to how one used the drupal_get_form()
and the form id as callback argument back in Drupal 7. The "_form
" property in the defaults section allows this, and there are tons of good examples in Core. For instance the user module does the following:
entity.user.admin_form:
path: '/admin/config/people/accounts'
defaults:
_form: '\Drupal\user\AccountSettingsForm'
_title: 'Account settings'
requirements:
_permission: 'administer account settings'
For the other parameters, such as _entity_form
, or _entity_list,
make sure to read through the "Structure of routes" page on drupal.org. To learn more about creating controllers check out this documentation page.
Altering routes
To change the way the route itself behaves we need to use a service called RouteSubscriber
. This is a two step process:
- we need to tell Drupal that we have this service,
- and we need to create the right class
We declare our services in a MODULENAME.services.yml file. Let’s assume we want to change how the login system works by replacing some of the routes the user module defines (e.g. user/login, user/password).
In the service declaration, we tell Drupal where our subscriber lives (MODULENAME/src/Routing/RouteSubscriber.php becomes class: Drupal\MODULENAME\Routing\RouteSubscriber), and we declare the name of the service, e.g. token_login.route_subscriber
for the subscriber of the token_login module. We also tell Drupal that this is an event_subscriber
:
services:
token_login.route_subscriber:
class: Drupal\token_login\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
Secondly we have to create the actual class. According to the PSR-4 standards, the class registered above will translate to the following file: token_login/src/Routing/RouteSubscriber.php, which is where we have to create our RouteSubscriber
class.
In the RouteSubscriber
we implement a method called alterRoutes()
, which will be invoked when the route is built. But how does Drupal know to call this method? The RouteSubscriber
extends the RouteSubscriberBase
class, which assigns the alterRoutes()
method to be invoked for route alter events in its default getSubscribedEvents()
implementation:
public static function getSubscribedEvents() {
$events[RoutingEvents::ALTER] = 'onAlterRoutes';
return $events;
}
When the alterRoutes()
method is invoked, it receives a list of routes (RouteCollection $collection
), from which we can get the one we want simply by calling the get method:$route = $collection->get(‘user.login);
it’s a good idea to wrap this into a conditional to ensure that we only try to change the route if it actually exists.
Since this $route
is of type Route
, we can proceed and use Route::setDefault()
, Route::setDefaults()
, Route::setRequirement(),
and other methods to change (add or remove) various bits defined in the original route YAML file:
protected function alterRoutes(RouteCollection $collection) {
// Use the password reset form for the login page.
if ($route = $collection->get('user.login')) {
$route->setDefaults(array('_form' => '\Drupal\token_login\Form\TokenLoginForm'));
}
// Always deny access to '/user/logout'.
// Note that the second parameter of setRequirement() is a string.
if ($route = $collection->get('user.pass')) {
$route->setRequirement('_access', 'FALSE');
}
// Change the title of the password reset page.
if ($route = $collection->get('user.reset')) {
$route->setDefaults(array(
'_title' => 'Use log in link',
'_controller' => '\Drupal\token_login\Controller\UserController::resetPass',
));
}
}
Although there is no distinct method to remove properties, the empty assignment does the job, for instance $route->setDefault(‘_form’ => NULL);
Check out the Token login module for some examples of this on drupal.org, or take a look at the documentation on drupal.org. This page also contains information on dynamic route definitions and references to further reading material.