21

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.

user_page:
  pattern: '/user'
  defaults:
    _content: '\Drupal\user\Controller\UserController::userPage'
  requirements:
    _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) {
      unset($callbacks[$path]['route_name']);
    }
    // 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']);

      unset($callbacks[$path]);
      $callbacks[$new_path] = $router_item;
    }
  }
0

3 Answers 3

15

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:

  <?php

  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.

3
  • There are details for setting up the service registration in Altering existing routes and adding new routes based on dynamic ones.
    – colan
    Commented Feb 13, 2017 at 16:16
  • The tag to use for the service is the most important part. Without that, the service would not be added to the event subscribers list, and it won't be invoked.
    – avpaderno
    Commented Dec 20, 2021 at 10:09
  • The code example works good, but event method (alterRoutes) should be public, instead of protected. Commented Nov 23, 2023 at 9:14
15

Drupal 8 core code has been changed since when I asked the question. Some of the issues about routes have been fixed.

hook_menu() isn't used from Drupal 8 anymore; the routes a module uses are defined in a .routing.yml file (for example user.routing.yml). Alter hooks are still used, but since hook_menu() isn't invoked from Drupal core, hook_menu_alter() won't be invoked.

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.)

    services:
      mymodule.route_subscriber:
        class: Drupal\mymodule\Routing\RouteSubscriber
        tags:
          - { 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
          ->getDeleteRoute());
      }
    }
    

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 a route controller changed from _content to _controller
4
  • You don't need to add tags: - { name: event_subscriber } if you are using RouteSubscriberBase. You only need that if you are implementing a standard EventSubscriber.
    – Lomax
    Commented Oct 14 at 22:25
  • It is necessary with Drupal 8. See the service which uses the AdminRouteSubscriber class.
    – avpaderno
    Commented Oct 14 at 23:10
  • This example breaks in D10.2+. You need to use the autoconfigure: true flag in your services.yml
    – Lomax
    Commented Oct 22 at 10:30
  • It is code for one of the first Drupal 8 releases. It cannot work for Drupal 10.
    – avpaderno
    Commented Oct 22 at 13:24
10

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.

<?php
/**
 * @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.
    if ($collection->get('search.view')) {
      $collection->remove('search.view');
    }
  }

}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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