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.

Revisions

  1. thelebster created this gist Jun 6, 2023.
    95 changes: 95 additions & 0 deletions drupal-widlcard-routes.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,95 @@
    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](https://www.drupal.org/docs/drupal-apis/routing-system/routing-system-overview), 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](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21PathProcessor%21InboundPathProcessorInterface.php/interface/InboundPathProcessorInterface/10) 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;
    }
    }
    ```