12

According to the Symfony documentation, a route defined as below should trigger the specified controller for both /hello/bob and /hello/bob/bobby:

_hello:
  path:     /hello/{names}
  defaults: { _controller: \Drupal\mymodule\Controller\Main::Controller }
  requirements:
    _access: 'TRUE'
    names: .+

In the case of a request to /hello/bob/bobby the {names} param would be "bob/bobby" (slash intact) and it would be up to the controller to break that down into multiple variables or to leave it as a single string. The trick to that is the altered regex (".+") used to filter that {names} param.

This stackoverflow post also implies that custom regex can be used to allow slashes in a route param (at least in Symfony 2).

If I try this against Drupal 8.0.0-beta15 it does not work and the specified controller is only triggered for a request to /hello/bob. However, I can confirm that this used to work in previous betas (I think up until ~beta13).

Has something changed in the way Drupal integrates with the Symfony routing component that would explain this? Perhaps there is an alternate way to accomplish the passing of slashes in routing params? I know there is a movement toward Symfony 3.0 in core, but I'm not sure if that could explain things.

I also know that route subscribers are available to manage dynamic route structures. However the case I am working on requires an almost infinite combination/number of dynamic parameters at the end of a base path (but which are trivial to parse in my controller). I'm also trying to avoid query strings (e.g. /hello?names[]=bob&names[]=bobby) for this case.

Mainly I'm just confused as to the disconnect with the Symfony documentation, which seems to state that this should be possible.


Additional Notes

After posting this question I discovered this discussion in the D8 core queues: [Discussion] Drop automated passing of extra argument: Y/N. It seems to conclude that "menu tail" support (which is essentially what I am after) will officially be dropped in D8. That discussion ended 3 years ago and so I can only guess that some of the more generalized implementation specifics were not fully realized until recently (~beta13). This may explain why I've only now noticed this change.

I'm guessing that Drupal (not Symfony) is now generating a 404 response based on the raw slash-delimited request before any of the Symfony-specific routing logic further dissects the route (and it's param-specific regex, etc.). If this is the case it could explain why technique above stopped working. I am still wondering however if there are alternative ways to deal with this need that avoid using query params and custom route subscribers.

10
  • From what I know about Symfony I would expect /hello/{username} to match on /hello/bob but not on /hello/bob/smith. If you want additional parameters, you have to set defaults for them as in stackoverflow.com/questions/11980175/…. But maybe I don't understand the question.
    – cilefen
    Sep 27, 2015 at 2:31
  • Configuring defaults works as expected (i.e. it's possible to set {names} as an optional param). So yes, I suppose I could have a route path like /hello/{arg1}/{arg2}/{arg3}/.../{argN}, which could work with 0-N params. However that sets a static limit on the number of params and feels pretty messy. My understanding that it should be possible to do this differently, via slash delimiters in a single param, is based on the Symfony documentation (symfony.com/doc/master/cookbook/routing/slash_in_parameter.html) along with previous experience with older Drupal core betas.
    – rjacobs
    Sep 27, 2015 at 4:41
  • 1
    It's not clear what path: /hello/{names} and username: .+ have to do with each other.
    – user49
    Sep 27, 2015 at 9:31
  • @chx, sorry, I had edited my question right after posting in an effort to make it more generic. In that process I forgot to update the yml snippet. I've fixed that. It should read "names: .+"
    – rjacobs
    Sep 27, 2015 at 15:41
  • I just noticed that my last comment may give the impression that I solved things. Please note that I only had a typo in my post. The question/issue still stands as it's now phrased.
    – rjacobs
    Sep 28, 2015 at 14:00

3 Answers 3

12

You can alter the path by adding a class that implements the InboundPathProcessorInterface

namespace Drupal\mymodule\PathProcessor;

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

class HelloPathProcessor implements InboundPathProcessorInterface {

  public function processInbound($path, Request $request) {
    if (strpos($path, '/hello/') === 0) {
      $names = preg_replace('|^\/hello\/|', '', $path);
      $names = str_replace('/',':', $names);
      return "/hello/$names";
    }
    return $path;
  }

}

This way the router will interpret the path /hello/bob/bobby as /hello/bob:bobby and you will get the parameters separated by : (or any other character that won't conflict with the parameters) in your controller.

You will also need to register the class as a service in mymodule.services.yml (make sure the priority is set higher than 200)

services:
  mymodule.path_processor:
    class: Drupal\mymodule\PathProcessor\HelloPathProcessor
    tags:
      - { name: path_processor_inbound, priority: 250 }
2
  • Yeah this is a good point, and I suppose this is the same mechanism that's used to do path aliasing? If going this route I assume that the router path would be unchanged (/hello/{names}) and then "names" could still be handled as a single route param. So if a URL using this route was generated programmatically names would still have to be constructed with the custom-delimiters (bob:bobby), but the system would still be compatible with more "friendly" URL input (bob/bobby)?
    – rjacobs
    Jan 20, 2016 at 16:06
  • When I am generating the URL in my twig, it doesn't work. My code: {{ path(urlPath, {'hello': "name/anothername"}) }} May 5, 2021 at 1:40
1

You can do the following to retrieve path params: In this case, I want to retrieve everything that comes after /rest/ as an array of strings.

Your yourmodule.routing.yml file should look something like this.

yourmodule.rest:
  path: /rest/{path_parms}
  defaults:
    _controller: 'Drupal\yourmodule\Controller\YourController::response'
    _title: 'Rest API Title'
  requirements:
    path_params: '^[^\?]*$'
    _permission: 'access content'

or in path\to\yourmodule\src\Routing\RouteProvider.php

/**
 * @file
 * Contains \Drupal\yourmodule\Routing\RouteProvider.
 */

namespace Drupal\yourmodule\Routing;

use Symfony\Component\Routing\Route;

/**
 * Defines dynamic routes.
 */
class RouteProvider
{
    /**
     * Returns all your module routes.
     *
     * @return RouteCollection
     */
    public function routes()
    {
        $routes = [];

        // This route leads to the JS REST controller
        $routes['yourmodule.rest'] = new Route(
            '/rest/{path_params}',
            [
              '_controller' => '\Drupal\yourmodule\Controller\YourController::response',
              '_title' => 'REST API Title',
            ],
            [
              'path_params' => '^[^\?]*$',
              '_permission' => 'access content',
            ]
        );

        \Drupal::service('router.builder')->setRebuildNeeded();

        return $routes;
    }
}

Next, add a Path processor to your module as follows. path\to\yourmodule\src\PathProcessor\YourModulePathProcessor.php

namespace Drupal\yourmodule\PathProcessor;

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

class YourModulePathProcessor implements InboundPathProcessorInterface {

    public function processInbound($path, Request $request) {
        // If a path begins with `/rest`
        if (strpos($path, '/rest/') === 0) {
            // Transform the rest of the path after `/rest`
            $names = preg_replace('|^\/rest\/|', '', $path);
            $names = str_replace('/',':', $names);

            return "/rest/$names";
        }

        return $path;
    }
}

Finally, in your controller, do this: path\to\yourmodule\src\Controller\YourController.php

namespace Drupal\yourmodule\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

/**
 * Controller routines for test_api routes.
 */
class YourController extends ControllerBase {

    /**
     * Callback for `rest/{path_params}` API method.
     */
    public function response(Request $request) {
        $params = explode(':', $request->attributes->get('path_params'));

        // The rest of your logic goes here ...
    }

}
4
  • its not working with drupal 8.7.9
    – Ekta Puri
    Feb 28, 2020 at 3:19
  • Really? Have you found an alternative though?
    – GuruKay
    Mar 2, 2020 at 22:05
  • not yet, if you have any solution please share
    – Ekta Puri
    Mar 2, 2020 at 22:11
  • Sorry it took me so long, I have updated my post to show what I finally did to get path params. In my own case, I needed it for my custom REST API.
    – GuruKay
    Mar 8, 2020 at 1:21
0

Alternative solution:

Create a route parameter that receives a json array.


mymodule.routing.yml

_hello:
  path:     /hello/{names}
  defaults: { _controller: \Drupal\mymodule\Controller\Main::index }
  requirements:
    _access: 'TRUE'

\Drupal\mymodule\Controller\Main

public function index($names_json) {
  $names = Json::decode($names_json);
  // Do something with $names
}
2
  • Yes, encoding multiple arguments into one route parameter is certainly an option. I assume you are implying that the "names" param would be a formal php array that's directly encoded/serialized? If so, wouldn't there be more appropriate encoding options for a path component that JSON? Note that this would also make for a much less user-friendly path regardless of the encoding technique (a potential issue for human input, but not necessarily for programmatic path generation).
    – rjacobs
    Jan 4, 2016 at 22:50
  • Of course you can use any encoding technique you want. I prefer JSON as it is very standardized with an encoder (/decoder) in most languages. This would make paths much less user friendly. If the path is exposed outside then I think passing the names in the query (name[]=Ben&name[]=George) is much more readable.
    – Eyal
    Jan 4, 2016 at 23:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.