Drupal 9: Altering Routes With The Route Subscriber Service

Creating services that listen events in Drupal is pretty straightforward; you just need to create and register a service class and let Drupal know what event you want to trigger on.

Altering routes in a Drupal site is also done through an event trigger, but in this case the implementation is slightly different. This is because Drupal takes care of the trigger setup and allows you to create an event subscriber that will automatically trigger when the routes are created.

In this article I will go through how to set up a route alter event subscriber, why the setup is slightly different from normal events, and what this event might be used for.

Altering Routes Using A Route Subscriber

Registering a route subscriber is done just like any other even subscriber with an entry in the module services YAML file. Adding a tag with the name of "event_subscriber" tells Drupal that this class should be included when events are triggered.

services:
  mymodule.route_subscriber:
    class: Drupal\mymodule\Routing\CustomRouteSubscriber
    tags:
      - { name: event_subscriber }

Next, we need to add the routing class to listen to the event. This can be put anywhere in the module, but its conventionally put into a Routing directory within the module src directory. Your module's directory structure should look something like this with the route subscriber in place.

mymodule.info.yml
mymodule.services.yml
src
- Routing
- - CustomRouteSubscriber.php

The route subscriber itself it pretty simple as you just extend the Drupal\Core\Routing\RouteSubscriberBase class and define a method called alterRoutes(), which accepts a RouteCollection object. This RouteCollection object is every route that has been discovered by Drupal, essentially as a big list of Route objects. This collection of routes is referenced by the route name and can be used to alter, add, or delete any routes from the system.

Here is a skeleton route subscriber class that can be used in an project.

<?php

namespace Drupal\mymodule\Routing;

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

/**
 * Route subscriber class.
 */
class CustomRouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    // Alter or add routes here.
  }

}

When setting up an event service we normally have to create a getSubscribedEvents() method to register what event we are listening for. Also the event method callback receives an event object as a parameter.

This isn't the case with route subscribers as the Drupal\Core\Routing\RouteSubscriberBase class implements the getSubscribedEvents() method like an event subscriber would. The RouteSubscriberBase class registers a method called onAlterRoutes() as an event callback, in which the event object is translated into the RouteCollection and passed onto the alterRoutes() method.

It is possible to change the weight of the route event callback by overriding the getSubscribedEvents() method in your route subscriber class. The following changes the event weight to -1025 so that it will be called after any other route subscribers have altered the routes. This is counter intuitive as higher values make the route subscriber trigger earlier in the alter process.

  public static function getSubscribedEvents() {
    $events[RoutingEvents::ALTER] = ['onAlterRoutes', -1025];
    return $events;
  }

Make sure you call onAlterRoutes() here as this will translate the event object into a route collection ready for you to make use of.

Note that routes are only altered as the caches are cleared, so once the route changes have been applied they won't be run again until the caches are cleared and the route information is rediscovered.

Some Uses Of A Route Subscriber

Route subscribers essentially allows custom modules to alter, add or remove any route in Drupal. As it isn't immediately clear why you want to do this, here are a few examples of this in practice.

Change Common Paths

One common way of securing a site is to hide the user login path. Many auto detection scripts will probe login pages with user account details, so a good way of thwarting their efforts is to move the login page to a different path.

The following code will extract the 'user.login' path from the collection of routes and change the path to "/my-secure/login".

  protected function alterRoutes(RouteCollection $collection) {
    /** @var \Symfony\Component\Routing\Route $userLoginRoute */
    $userLoginRoute = $collection->get('user.login');
    $userLoginRoute->setPath('/my-secure/login');
  }

Change Entity Canonical Paths

Content entities can be accessed through a canonical route name and their paths changed. For example, node entities can be access through the entity.node.canonical route, which has a path of "/node/{node id}". We can use the route subscriber to change this to "/page/{node id}" in just a couple of lines of code.

  protected function alterRoutes(RouteCollection $collection) {
    /** @var \Symfony\Component\Routing\Route $entityNodeCanonical */
    $entityNodeCanonical = $collection->get('entity.node.canonical');
    $entityNodeCanonical->setPath('/page/{node}');
  }

As a side note, this isn't how the Path Auto module works, which creates an additional path to the entity, instead of changing the entity route.

Adding A Route

I've mentioned that it's possible to create a route in a route subscriber, so let's look at that in action. In order to create a route we need to generate a Route object and then add it to the RouteCollection using the add() method. The Route object takes a number of parameters that can either be added to the controller or afterwards using methods of the object.

The following code creates a Route object with the path "/a_custom_path" and then defines some attributes for that route. Once ready, the route is added to Drupal using the add() method and given a name of mymodule.a_custom_path.

  protected function alterRoutes(RouteCollection $collection) {
    $route = new Route('/a_custom_path');
    $route->addDefaults([
      '_controller' => '\Drupal\mymodule\Controller\SomeController::testAction',
      '_title' => 'Page added through route subscriber',
    ]);
    $route->addRequirements(
      [
        '_permission' => 'access administration pages'
      ]
    );
    $route->addOptions(
      [
        '_admin_route' => 'TRUE',
      ]
    );
    $collection->add('mymodule.a_custom_path', $route);
  }

When we visit the page at "/a_custom_path" we will see whatever is in the controller output.

This might seem like a daft example, but this technique is used in quite a few places in the Drupal world. For example, if you install the Devel module then it will inject tabs into certain content entity pages using this exact method. This is done dynamically using information within Drupal about the sorts of entities that are installed, but it essentially follows the same code.

Removing A Route

The RouteCollection class has a method called remove() that can be used to delete a route from the collection. The Webform module makes use of this technique by removing certain default routes from the module if the "webform_node" module hasn't also been installed.

  protected function alterRoutes(RouteCollection $collection) {
    if (!$this->moduleHandler->moduleExists('webform_node')) {
      $collection->remove('entity.node.webform.results_log');
      $collection->remove('entity.node.webform_submission.log');
    }
  }

The two routes seen here are defined in the Webform submission log module and the route subscriber is used to remove them from the site if the webform node module isn't present.

More in this series

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
1 + 5 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.