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.

Posted by Kim Pepper
Technical Director

Dated

Comments

Comment by RdeBoer

Dated

Thank you so much for this series, Kim.
There have been plenty of delays in the roll-out of D8.
Stuff like this helps accelerate the availability of D8 + contrib as a practical future platform.

Comment by jancis

Dated

Cannot wait to start writing 3 x more code to get the same functionality. Thanks for tutorials nevertheless.

Comment by @AlbertoGarla

Dated

After the last changes of the API is necessary implements

public function alterRoutes(RouteCollection $collection, $provider) {

if ($provider != 'dynamic_routes') {
return;
}

your routes
}

instead of routes()

Regards.

Comment by Sutharsan

Dated

More documentation on Routing can be found at: https://drupal.org/node/2122071 and subpages. I've just finished updating most of the pages.

In your function alterRoutes() $collection->add() is not (no longer?) required.

Comment by Capi Etheriel

Dated

Looking forward to a TrouserManager tutorial.

Comment by zealfire

Dated

How to make menu in Drupal 8 when using route subscriber?
Thanks.

Comment by saurabh.tripathi.cs@gmail.com

Dated

HI,
Can i change page callback of a route.Like in below code , i want the item ''trousers/add/' . $type->machine_name' to redirect to some other function instead of trouser_type_add_page.

$items['trousers/add/' . $type->machine_name] = array(
'title' => $type->title,
'page callback' => 'trouser_type_add_page',
'access arguments' => 'create ' . $type->type,
);
}
Thanks.

Comment by saurabh.tripathi.cs@gmail.com

Dated

Solution for above comment:Chaging callback of already Defined Route in Drupal 8
public function alterRoutes(RouteCollection $collection) {
//Find the route we want to alter
//dsm($collection);
$route = $collection->get('example.route.name');
//Make a change to the controller.
$route->setDefault('_controller', '\Drupal\example\Controller\IndexController::changed_callback_trouser_type_add_page');
}

Comment by jyoti

Dated

$route = $collection->get('example.route.name');
//Make a change to the controller.
Here instead of changing _default -> controller , How can i change requirement ->permission callback ?

Comment by Anonymous

Dated

I did this : but does not work for taxonomy add route (dynamic route)& works for taxonomy list route(static)

if ($route = $collection->get('entity.taxonomy_term.add_form')) {
$route->setRequirement('_permission', 'administer tags vocabulary terms');
This administer tags vocabulary terms is my custom permission.

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.