Skip to main content

Dynamic Routes in Drupal 8 with a RouteSubscriber

Previously I have demonstrated how to create a new route controller in Using Drupal 8's new route controllers then how to restrict access to it in Controlling Access to Drupal 8 Routes with Access Checks. But that's not where the fun ends!

What about when we need to create a route dynamically. For example, if we need to create routes for content types that we don't know will exist in advance?  In Drupal 7, we created dynamic routes with a foreach loop in hook_menu(). In Drupal 8, we can do all this and more with a RouteSubscriber.

by kim.pepper /

In Drupal 7, if you wanted to create a menu item that contained some dynamic aspect to it, you would probably just do something like the following:

/**
* Implements hook_menu().
 */
function trousers_menu() {
$items = array();
foreach (trousers_get_types() as $type) {
$items['trousers/add/' . $type->machine_name] = array(
'title' => $type->title,
'page callback' => 'trouser_type_add_page',
'access arguments' => 'create ' . $type->type,
);
}
return $items;
}

What are we doing here? We're essentially defining menu items for an add trouser form per trouser type (some similarities here to content types). Note, this is different from simple page arguments because we are able to define different permissions, page callbacks, and other routing information depending on what types we have available. Here we are specifying that we have a per-trouser type create permission.

How do we do this in Drupal 8?

Drupal makes use of the new Symfony2 events system.

What are events? If you're worried about learning another new programming concept in Drupal 8, you can rest easy. Drupal has had its own event system in place for many many years. They're called hooks! Hooks are a way of saying "when this event occurs I want you to call my function" and it does it through a function naming convention.

Symfony2's Routing system leverages its Event Dispatcher Component to allow custom code to listen for dynamic routing events.

Step 1: Define our RouteSubscriber

Define a class that extends from RouteSubscriberBase. This gives us a good starting point to define our new routes that are listing for the right Symfony2 routing events.

namespace Drupal\trousers\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection; /** * Listens to the dynamic trousers route events. */ class TrousersRouteSubscriber extends RouteSubscriberBase { public function routes(RouteCollection $collection) { foreach (trousers_get_types() as $type) { $route = new Route(
// the url path to match 'trousers/add/' . $type,
// the defaults (see the trousers.routing.yml for structure) array(
'_title' => $type->title, '_controller' => '\Drupal\trousers\Controller\TrouserController::addType', 'type' => $type->machine_name, ),
// the requirements array( '_permission' => 'create ' . $type-type, ) );
// Add our route to the collection with a unique key.
$collection->add('trousers.add.' . $type->machine_name, $route);
}
}
}

The structure of the array matches that of the standard routing.yml format. We define the path, defaults and requirements as constructor arguments to a new Route object. Then we add it to the RouteCollection with a unique key.

Step 2: Define the Route Subscriber as a Service

We saw in the previous blog post how to create an access check service. We need to do the same for our RouteSubscriber. So in our trousers.services.yml file we need to add the following lines:

  trousers.route_subscriber:
    class: Drupal\trousers\Routing\TrousersRouteSubscriber
    tags:     - { name: event_subscriber }

This is all we need to have our RouteSubcriber called and create our dynamic routes.

Of course now that our RouteSubscriber is defined as a service, we can use the benefit of a dependency injection container and inject other services into it. For example we could pass in the database connection object, and make queries to the database, or better yet, call a TrouserManager interface method and do away with our procedural call to trousers_get_types().

Altering Existing Routes

In Drupal 7, we could alter an existing route, provided by our own or any other core or contrib module, by using hook_menu_alter(). In Drupal 8, we have an equivalent method on our RouteSubscriberBase we can implement to alter existing route definitions.

public function alterRoutes(RouteCollection $collection, $provider) {
// Find the route we want to alter
$route = $collection->get('example.route.name');
// Make a change to the path.
$route->setPath('/my/new/path');
// Re-add the collection to override the existing route.
$collection->add('example.route.name', $route);
} 

This looks up a route with the name 'example.route.name' and changes the path it defined to one of our choosing.

Conclusion

At the time of writing, there are still DX (developer experience) issues being worked on to make this easier for developers. But as you can see, making use of RouteSubscribers gives us a powerful and flexible tool in our toolbelt.