Drupal 9/10 dynamic routes

Submitted by oioioooi on 16/04/2024 - 11:00

Building dynamic routes , that are built based on some custom rules (like taxonomy terms, etc) involve the following:

  1. Creating a service that is listening to routing.route_alter event;
  2. Implementing a class that alters the route collection;

Creating a route alter event listener

Create src/EventSubscriber/RouteSubscriber.php class stub in your module directory. In this file, add the namespace and create a dummy class that extends Drupal\Core\Routing\RouteSubscriberBase.

Add an empty stub for the required alterRoutes method.

<?php

namespace Drupal\YOUR_MODULE\EventSubscriber;

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

class RouteSubscriber extends RouteSubscriberBase
{

  protected function alterRoutes(RouteCollection $collection) {
    // TODO: Implement alterRoutes() method.
  }

}

Note: There's a little trick here that Drupal does. Normally, when creating event subscribers, it is required to define which event we register to, in getSubscribedEvents method, and what callback has to be invoked. In this specific case, since we extend the RouteSubscriberBase class, this logic is already in place and there is a onAlterRoutes method that is invoked automatically, which eventually calls the alterRoutes method.

Next, create an entry in YOUR_MODULE.services.yml file and register this event subscriber:

services:
  YOUR_MODULE.route_subscriber:
    class: Drupal\YOUR_MODULE\EventSubscriber\RouteSubscriber
    arguments: []
    tags:
      - { name: event_subscriber }

Clear caches and the event subscriber will be registered.

Implementing the class that alters the route collection

Getting back to the RouteSubscriber class, let's register routes based on some taxonomy vocabulary terms. That is, each term from the selected vocabulary is going to be used to create custom, dynamic route.

To work with taxonomy terms, inject the entity manager service into the class.

class RouteSubscriber extends RouteSubscriberBase
 {

+  /**
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   */
+  public function __construct(protected EntityTypeManagerInterface $entityTypeManager) {}
+

And in services file...

services:
   YOUR_MODULE.route_subscriber:
     class: Drupal\YOUR_MODULE\EventSubscriber\RouteSubscriber
+    arguments: ['@entity_type.manager']
     tags:
       - { name: event_subscriber }

Create a method in the RouteSubscriber class that would create route objects based on the term argument...

   // TODO: Implement alterRoutes() method.
   }

+  protected function createRoute(Term $term): Route
+  {
+    return (new Route("/admin/config/custom/{$term->id()}"))
+      ->addDefaults([
+        '_form' => CustomForm::class,
+        '_title' => $term->label(),
+      ])
+      ->addRequirements([
+        '_permission' => 'administer site configuration',
+      ])
+      ->setOption('_admin_route', TRUE);
+  }
+
 }

In the change above we create new route object with a path mapped to term id. The route, when accessed, would invoke a CustomForm form instance.

What's left is to register the routes, in the alterRoutes method, based on terms from a certain vocabulary...

   protected function alterRoutes(RouteCollection $collection): void {
-    // TODO: Implement alterRoutes() method.
+    /** @var \Drupal\taxonomy\Entity\Term[] $terms */
+    $terms = $this
+      ->entityTypeManager
+      ->getStorage('taxonomy_term')
+      ->loadByProperties(['vid' => 'VOCABULARY_ID']);
+
+    foreach ($terms as $term) {
+      $collection->add(
+        "some.custom.route.tid_{$term->id()}",
+        $this->createRoute($term)
+      );
+    }
   }

The newly registered routes get a some.custom.route.tid_TERM_ID identifier pattern and map to admin/config/custom/TERM_ID path.

Tags