In other words, what is the Drupal 8 equivalent of hook_menu_alter()?

Drupal 8 still uses hook_menu(), but for what I can see, the information returned by the hook is different from what the hook returned in Drupal 7. For example, the definition given in user_menu() for user is the following.

  $items['user'] = array(
    'title' => 'User account',
    'title callback' => 'user_menu_title',
    'weight' => -10,
    'route_name' => 'user_page',
    'menu_name' => 'account',

The route_name property links to an entry in the user.routing.yml file.

  pattern: '/user'
    _content: '\Drupal\user\Controller\UserController::userPage'
    _access: 'TRUE'

This is different from what done with Symphony, and it confuses me about how a module can alter the route defined from another user.

The only function that is still invoking hook_menu_alter() is menu_router_build(), but that function still contains code that needs to be updated, since it is still using the now deprecated drupal_alter().

  // Alter the menu as defined in modules, keys are like user/%user.
  drupal_alter('menu', $callbacks);
  foreach ($callbacks as $path => $router_item) {
    // If the menu item is a default local task and incorrectly references a
    // route, remove it.
    // @todo This may be removed later depending on the outcome of
    // http://drupal.org/node/1889790
    if (isset($router_item['type']) && $router_item['type'] == MENU_DEFAULT_LOCAL_TASK) {
    // If the menu item references a route, normalize the route information
    // into the old structure. Note that routes are keyed by name, not path,
    // so the path of the route takes precedence.
    if (isset($router_item['route_name'])) {
      $router_item['page callback'] = 'USES_ROUTE';
      $router_item['access callback'] = TRUE;
      $new_path = _menu_router_translate_route($router_item['route_name']);

      $callbacks[$new_path] = $router_item;

See Altering existing routes and adding new routes based on dynamic ones; it worked nicely for me to remove /search handled by the Search module. In my case, I used the following code.

 * @file
 * Contains \Drupal\ua_sc_module\Routing\SearchAlterRouteSubscriber.

namespace Drupal\ua_sc_module\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

 * Listens to the dynamic route events.
class SearchAlterRouteSubscriber extends RouteSubscriberBase {

   * {@inheritdoc}
  protected function alterRoutes(RouteCollection $collection) {
    // Remove the /search route.


Allow views to override existing routing items just uses existing functionality.

If you want to change the information attached to a route, not the menu, like the actual used controller, or the requirements (permission/role etc.) you can use an event provided by Drupal:


  use Drupal\Core\Routing\RouteBuildEvent;
  use Drupal\Core\Routing\RoutingEvents;
  use Symfony\Component\EventDispatcher\EventSubscriberInterface;

  class RouteSubscriber implements EventSubscriberInterface {
    * {@inheritdoc}
   public static function getSubscribedEvents() {
      $events[RoutingEvents::ALTER] = 'alterRoutes';
     return $events;

   * Alters existing routes.
   * @param \Drupal\Core\Routing\RouteBuildEvent $event
   *   The route building event.
  protected function alterRoutes(RouteBuildEvent $event) {
    // Fetch the collection which can be altered.
    $collection = $event->getRouteCollection();
    // The event is fired multiple times so ensure that the user_page route
    // is available.
    if ($route = $collection->get('user_page')) {
      // As example add a new requirement.
      $route->setRequirement('_role', 'anonymous');


Additionally you have to register a service with the tag 'event_subscriber' for this class.


Since I asked this question, Drupal 8 core changed, and some of the issues about routes have been fixed.

hook_menu() is not used from Drupal 8 anymore; the routes a module uses are defined in a .routing.yml file (e.g. user.routing.yml). Alter hooks are still used, but since hook_menu() is not used anymore from Drupal core, Drupal core doesn't invoke hook_menu_alter() too.

The steps necessary to alter a route defined from other modules are the following:

  • Define a service tagged as event_subscriber. (The tag is the important part, or the service would not work as expected.)

          class: Drupal\mymodule\Routing\RouteSubscriber
            - { name: event_subscriber }
  • Create a class that extends RouteSubscriberBase.

      namespace Drupal\mymodule\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) {
          // Change the route associated with the user profile page (/user, /user/{uid}).
          if ($route = $collection->get('user.page')) {
            $route->setDefault('_controller', '\Drupal\mymodule\Controller\UserController::userPage');

    For real implementation examples of alterRoutes(), see AdminRouteSubscriber::alterRoutes() or RouteSubscriber::alterRoutes() .

    protected function alterRoutes(RouteCollection $collection) {
      foreach ($collection->all() as $route) {
        if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route') && static::isHtmlRoute($route)) {
          $route->setOption('_admin_route', TRUE);
    protected function alterRoutes(RouteCollection $collection) {
      $mappers = $this->mapperManager->getMappers($collection);
      foreach ($mappers as $mapper) {
        $collection->add($mapper->getOverviewRouteName(), $mapper->getOverviewRoute());
        $collection->add($mapper->getAddRouteName(), $mapper->getAddRoute());
        $collection->add($mapper->getEditRouteName(), $mapper->getEditRoute());
        $collection->add($mapper->getDeleteRouteName(), $mapper

Notice that compared with earlier Drupal 8 releases, some details are changed.

  • The route name is changed from user_page to user.page
  • The requirements for that route changed from _access: 'TRUE' to _user_is_logged_in: 'TRUE'
  • The property to set the controller of a route changed from _content to _controller
  • If you want to match multiple routes, is there a more elegant way than setting a $matched variable to false, then iterating through them all one at a time with $collection->get()? I can't see any obvious methods. Sep 20 '18 at 22:19
  • @WilliamTurrell You can get an ArrayIterator object with RouteCollection::getIterator(); that would make easier to iterate over all the items in $collection. You can also get all the routes in $collection with RouteCollection::all(), which returns an array.
    – apaderno
    May 18 '19 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.