Skip to content

Instantly share code, notes, and snippets.

@thelebster
Created June 6, 2023 21:28
Show Gist options
  • Save thelebster/6d06e11b4f43f53464f235db18b6850a to your computer and use it in GitHub Desktop.
Save thelebster/6d06e11b4f43f53464f235db18b6850a to your computer and use it in GitHub Desktop.
Drupal 9: Wildcard Routes

The goal is to provide a dynamic (wildcard) route that should match on all nested routes that starts from some string.

The main issue is that Drupal routing system, does not provide a clean way to describe a wildcard routes that should match some kind of pattern, like /UserGuide, /UserGuide/Development_Notes or /UserGuide/Release_Notes/3.0.x etc.

Initial idea is to use dynamic routes, that will work in case when routes are known or could be generated by some pattern, like /UserGuide/node/1, /UserGuide/node/2 etc. Otherwise this is not possible, like when the route could consist from the multiple undefined parts.

As one possible solution, I decided to try the inbound path processor to catch destination path to redirect to the page controller, and pass an original path as a query parameter.

File example/example.routing.yml:

example.user_guide:
  path: '/UserGuide'
  defaults:
    _controller: '\Drupal\example\Controller\ExamplePageController::view'
    _title: 'User Guide'
  requirements:
    _access: 'TRUE'

File example/example.services.yml:

services:
  example.path_processor:
    class: Drupal\example\PathProcessor\ExamplePathProcessor
    tags:
      - { name: path_processor_inbound, priority: 1000 }

File example/src/PathProcessor/ExamplePathProcessor.php:

<?php

namespace Drupal\example\ExamplePathProcessor;

use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;

// Before PHP 8, use a polyfill.
// @see https://www.php.net/manual/en/function.str-starts-with.php
if (!function_exists('str_starts_with')) {
  function str_starts_with($haystack, $needle) {
    return (string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0;
  }
}

class ExamplePathProcessor implements InboundPathProcessorInterface {

  /**
   * {@inheritdoc}
   */
  public function processInbound($path, Request $request) {
    if (!str_starts_with($path, '/UserGuide')) return $path;
    // Remove leading slash.
    $original_path = ltrim(str_replace('/UserGuide', '', $path), '/');
    $request->query->set('original_path', $original_path);
    return '/UserGuide';
  }
}

File example/src/Controller/ExamplePageController.php:

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;

class ExamplePageController extends ControllerBase {

  public function view() {
    $request_query = \Drupal::request()->query->all();
    
    // Get an original path.
    $original_path = $request_query['original_path'];
    
    // Do anything else...
    // For example, try to fetch page content from some external API etc.
    $page_content = self::getPageContent($original_path);
    
    if (empty($page_content)) {
      // Return 404 not found if page does not exist.
      throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException();
    }
    
    // Return a renderable array with a page content.
    return $build;
  }
  
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment