In the transition from Drupal 7 to Drupal 8 and beyond, one of the most significant architectural shifts was the move from the procedural hook_menu() system to the Symfony-based routing system. If you are a developer tasked with a Drupal route alter operation, you may find yourself looking for the modern equivalent of hook_menu_alter().
In modern Drupal versions (including Drupal 9, 10, and 11), routes are primarily defined in *.routing.yml files. However, because these YAML files are static, you cannot use them to modify routes defined by other modules. To achieve dynamic modifications, you must utilize a RouteSubscriber. This guide will walk you through the process of intercepting, modifying, and even deleting routes programmatically.
Understanding the Shift to Event-Driven Routing
In older versions of Drupal, the menu system was a monolithic array. You could simply implement hook_menu_alter() and change any key-value pair in that array. In modern Drupal, routing is handled by the Symfony Routing component.
When Drupal builds its routing table, it emits an event. By subscribing to this event, your module can intercept the RouteCollection and make changes before the routes are cached. This is a more robust, object-oriented approach that aligns with Symfony standards. Instead of a hook, you will now use a Service tagged as an event_subscriber and a PHP class that extends RouteSubscriberBase.
Step 1: Registering Your Route Subscriber Service
The first step is to tell Drupal that your module has a class capable of listening to routing events. You do this by adding an entry to your module's services.yml file. The critical part of this definition is the event_subscriber tag.
services:
mymodule.route_subscriber:
class: Drupal\mymodule\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
By adding this tag, Drupal's ContainerAwareEventDispatcher will automatically include your class when the RoutingEvents::ALTER event is fired during the route rebuilding process.
Step 2: Creating the RouteSubscriber Class
Next, you need to create the class file. This class should reside in your module's src/Routing directory. The most efficient way to do this is by extending RouteSubscriberBase, which provides a boilerplate for the alterRoutes method.
Here is a basic implementation that demonstrates how to target a specific route and change its controller:
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.
// In modern Drupal, the route name is 'user.page'.
if ($route = $collection->get('user.page')) {
$route->setDefault('_controller', '\\Drupal\\mymodule\\Controller\\UserController::userPage');
}
}
}
In this example, we are overriding the default user profile page with a custom controller. Note that in earlier versions of Drupal 8, the route name was user_page and the property was _content. In current versions, you should use user.page and _controller.
Step 3: Modifying Route Requirements and Access
Beyond just changing the controller, a common use case for a Drupal route alter is changing access requirements. For example, you might want to restrict a specific page to users with a certain role or permission that the original module didn't account for.
/**
* Alters existing routes to add custom requirements.
*/
protected function alterRoutes(RouteCollection $collection) {
if ($route = $collection->get('user.page')) {
// Ensure only anonymous users can see this specific route.
$route->setRequirement('_role', 'anonymous');
// Or change the permission required.
$route->setRequirement('_permission', 'access custom content');
}
}
Using setRequirement(), you can modify the _permission, _role, or even custom access checkers. This is extremely powerful for hardening the security of contributed modules without hacking their core files.
Step 4: Removing Routes Entirely
Sometimes, you don't want to change a route; you want it gone. This is common when a module provides a feature (like a specific search page or a default feed) that conflicts with your site's architecture. You can use the remove method on the RouteCollection object.
/**
* Listens to the dynamic route events to remove unwanted paths.
*/
class SearchAlterRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
// Completely remove the /search route defined by the Search module.
if ($collection->get('search.view')) {
$collection->remove('search.view');
}
}
}
When you remove a route this way, Drupal will return a 404 Not Found for that path, unless another module (or your own) defines a new route for that same URL pattern.
Advanced: Bulk Route Alterations
You can also iterate through the entire collection to perform bulk updates. A classic example is ensuring that all routes starting with a specific path are treated as administrative routes, which triggers the admin theme.
protected function alterRoutes(RouteCollection $collection) {
foreach ($collection->all() as $route) {
// Check if the path starts with /admin and doesn't already have the admin option.
if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route')) {
$route->setOption('_admin_route', TRUE);
}
}
}
This pattern is used by Drupal core's AdminRouteSubscriber and is a great example of how to apply global logic to your site's routing table.
Frequently Asked Questions
Why isn't my RouteSubscriber working after I added the code?
Routing information is heavily cached in Drupal. Whenever you modify a services.yml file or change the logic inside alterRoutes(), you must clear the Drupal cache (e.g., drush cr). This triggers the route rebuilding process, which executes your subscriber.
Can I use RouteSubscriber to add new routes?
While RouteSubscriberBase is designed for altering, you can technically add routes using $collection->add(). However, for static routes, it is always better to use a MODULENAME.routing.yml file. Use the subscriber only when the route needs to be generated dynamically based on other modules' configurations.
How do I find the name of the route I want to alter?
Every route has a unique machine name. You can find these by looking at the *.routing.yml files in the source code of the module that defines the route. Alternatively, you can use the Devel module or Drush command drush route to list all registered routes on your site.
Wrapping Up
Mastering the Drupal route alter process is essential for any developer looking to customize Drupal's behavior without modifying core or contributed code. By using a RouteSubscriber, you tap into the power of Symfony's event system, allowing you to override controllers, tweak access permissions, or remove unnecessary paths cleanly and maintainably.
Remember to always register your service with the event_subscriber tag and extend RouteSubscriberBase to keep your code organized. As Drupal continues to evolve, this object-oriented approach remains the standard for routing manipulation across all modern applications.