DEV Community

Max
Max

Posted on

Drupal: Dynamic routes

We know that starting with Drupal 8 we have a good Routing system. But I am faced with one limitation, I cannot define the route with the parameter in the first position ("/{param_1}/some/path"). And in this article I want to share a solution for this case.

Let's imagine that we have a list of articles, each of which belongs to a certain category (taxonomy term). I want to have a router for such a path "/{category}/{article_alias}/{view_mode}".

The list of categories can be changed at any time, so we cannot determine all possible routes in advance. The view_mode is just an additional parameter for demonstration.

Fortunately, Drupal allows us to declare our custom Routers. Let's create a new module for this.

modules/custom/dynamic_router/dynamic_router.info.yml:

name: Dynamic Router
type: module
description: 'Provides Dynamic routes for article CT.'
dependencies:
  - drupal:node
package: Custom
core_version_requirement: ^11
Enter fullscreen mode Exit fullscreen mode

modules/custom/dynamic_router/dynamic_router.routing.yml:

route_callbacks:
  - '\Drupal\dynamic_router\Routing\DynamicRouter::routes'
Enter fullscreen mode Exit fullscreen mode

We usually provide several custom routes in this file, but we can also define route_callback.

modules/custom/dynamic_router/src/Routing/DynamicRouter.php:

<?php

namespace Drupal\dynamic_router\Routing;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;

/**
 * Class provides dynamic routing for the articles.
 */
class DynamicRouter implements ContainerInjectionInterface {

  /**
   * Constructs DynamicRouter class.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager
  ) {}

  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager')
    );
  }

  /**
   * Returns an array of route objects.
   *
   * @return \Symfony\Component\Routing\Route[]
   *   An array of route objects.
   */
  public function routes(): array {
    $routes = [];

    $terms = $this->entityTypeManager->getStorage('taxonomy_term')
      ->loadTree("article_categories", 0, NULL, TRUE);

    /** @var \Drupal\taxonomy\TermInterface $category */
    foreach ($terms as $category) {
      $category_path = $category->path->alias;
      $routes['category.' . $category->id() . '.article'] = new Route(
        $category_path . '/{node}/{view_mode}',
        ['_controller' => 'Drupal\dynamic_router\Controller\ArticleController::node'],
        ['_permission' => 'access content'],
      );
    }

    return $routes;
  }

}
Enter fullscreen mode Exit fullscreen mode

For this example, I created a new taxonomy vocabulary with the machine name "article_categories"

A few categories to test

And a simple controller, just to demonstrate, I returned the standard output of node builder using view_mode parameter from route.

modules/custom/dynamic_router/src/Controller/ArticleController.php:

<?php

namespace Drupal\dynamic_router\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\path_alias\AliasManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\node\NodeInterface;

class ArticleController extends ControllerBase {

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a ArticleController object.
   *
   * @param \Drupal\Core\Path\AliasManagerInterface $aliasManager
   *    The alias manager service.
   */
  public function __construct(
    protected AliasManagerInterface $aliasManager
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('path_alias.manager'),
    );
  }

  /**
   * Renders a node as a render array.
   *
   * @param string $node
   *   The node alias.
   * @param string $view_mode
   *   View mode ('full', 'part').
   *
   * @return array
   *   A render array for the node.
   */
  public function node(string $node, string $view_mode): array {
    $path = $this->aliasManager->getPathByAlias('/' . $node);

    if (preg_match('/^\\/node\\/(\\d+)$/', $path, $matches)) {
      $nid = $matches[1];
      $node = $this->entityTypeManager()->getStorage('node')->load($nid);
    }

    if ($node instanceof NodeInterface && $node->isPublished()) {
      $view_builder = $this->entityTypeManager->getViewBuilder('node');
      return $view_builder->view($node, $view_mode);
    }
    else {
      return [
        '#markup' => $this->t('Node not found or unpublished.'),
      ];
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Now we can check if Drupal already knows about our routes.

> drush core:route | grep category
Enter fullscreen mode Exit fullscreen mode

And yes, it is. We can see routes for all categories

And now, if we add a new term to the vocabulary of article categories, the list of routes will be updated automatically.

That's all! Please respond if you have any thoughts on this solution and good luck :)

Top comments (0)