symfonyurl-routingfosrestbundlesymfony5

Symfony 5 dynamic routing resolve


I am migrating legacy project routing (Yii1) to Symfony 5

Right now my config/routing.yaml looks something like this:

- {path: '/login', methods: ['GET'], controller: 'App\Controller\RestController::actionLogin'}
- {path: '/logout', methods: ['GET'], controller: 'App\Controller\RestController::actionLogout'}
# [...]
- {path: '/readme', methods: ['GET'], controller: 'App\Controller\RestController::actionReadme'}

As you can see there is plenty of repetitive url to action conversion.

Is it possible to dynamically resolve controller method depending on some parameter. E.g.

- {path: '/{action<login|logout|...|readme>}', methods: ['GET'], controller: 'App\Controller\RestController::action<action>'}

One option would be to write annotations, but that somehow does not work for me and throws Route.php not found


Solution

  • The controller is determined by a RequestListener, specifically the router RouterListener. This in turn uses UrlMatcher to check the uri against the RouteCollection. You could implement a Matcher that resolves the controller based on the route. All you have to do is return an array with a _controller key.

    Take note that this solution won't allow you to generate a url from a route name, since that's a different Interface, but you could wire it together.

    // src/Routing/NaiveRequestMatcher
    namespace App\Routing;
    
    use App\Controller\RestController;
    use Symfony\Component\Routing\Exception\ResourceNotFoundException;
    use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
    use Symfony\Component\Routing\RequestContext;
    
    class NaiveRequestMatcher implements UrlMatcherInterface
    {
        private $matcher;
    
        /**
         * @param $matcher The original 'router' service (implements UrlMatcher)
         */
        public function __construct($matcher)
        {
            $this->matcher = $matcher;
        }
    
        public function setContext(RequestContext $context)
        {
            return $this->matcher->setContext($context);
        }
    
        public function getContext()
        {
            return $this->matcher->getContext();
        }
    
        public function match(string $pathinfo)
        {
            try {
                // Check if the route is already defined
                return $this->matcher->match($pathinfo);
            } catch (ResourceNotFoundException $resourceNotFoundException) {
                // Allow only GET requests
                if ('GET' != $this->getContext()->getMethod()) {
                    throw $resourceNotFoundException;
                }
    
                // Get the first component of the uri
                $routeName = current(explode('/', ltrim($pathinfo, '/')));
    
                // Check that the method is available...
                $baseControllerClass = RestController::class;
                $controller = $baseControllerClass.'::action'.ucfirst($routeName);
                if (is_callable($controller)) {
                  return [
                    '_controller' => $controller,
                  ];
                }
                // Or bail
                throw $resourceNotFoundException;
            }
        }
    }
    

    Now you need to override the Listener configuration:

    // config/services.yaml
    Symfony\Component\HttpKernel\EventListener\RouterListener:
        arguments:
            - '@App\Routing\NaiveRequestMatcher'
    
    App\Routing\NaiveRequestMatcher:
         arguments:
           - '@router.default'
    

    Not sure if it's the best approach, but seems the simpler one. The other option that comes to mind is to hook into the RouteCompiler itself.