-1

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

Justinas
  • 41,402
  • 5
  • 66
  • 96

1 Answers1

1

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.

msg
  • 7,863
  • 3
  • 14
  • 33