Skip to content

Instantly share code, notes, and snippets.

@malteschlueter
Last active July 1, 2022 11:32
Show Gist options
  • Save malteschlueter/771068077dde312ac9e656acda6cf37d to your computer and use it in GitHub Desktop.
Save malteschlueter/771068077dde312ac9e656acda6cf37d to your computer and use it in GitHub Desktop.
This Symfony command can help you identify mismatched routes. For example, if you migrate the route configuration from YAML to Annotation and the routes do not work correctly afterwards.

RouterMismatchCommand

This Symfony command can help you identify mismatched routes. For example, if you migrate the route configuration from YAML to Annotation and the routes do not work correctly afterwards.

One reason why the routes do not work correctly could be that the controller methods are not in the correct order like the YAML configuration was.

How to use the command

Just copy the RouterMismatchCommand.php into your project and execute it. If you have a mismatched route it will display it in a table.

For me it was sufficient to move the controller method with the route app_foo_staticroute before the method with the route app_foo_paramroute. If you're using Symfony >=5.1 you can use the priority parameter.

> php bin/console router:mismatch
------- --------------------- -------- -------- ------ --------- 
        Name                  Method   Scheme   Host   Path     
------- --------------------- -------- -------- ------ --------- 
Route   app_foo_staticroute                            /static  
Trace   app_foo_paramroute                             /{foo}   
------- --------------------- -------- -------- ------ ---------

Route configuration examples

YAML route order is correct

In this example, the order of the controller methods is unimportant.

# config/routes.yaml

app_static:
    path: '/static'
    defaults:
        _controller: 'App\Controller\FooController::staticRoute'

app_parameter:
    path: '/{foo}'
    defaults:
        _controller: 'App\Controller\FooController::paramRoute'

The requested path is "/static"

First Symfony will try if the request is matched with the app_static route and it matches.

The requested path is "/something"

First Symfony will try if the request is matched with the app_static route and it does not match. So it uses the app_parameter route.

YAML route order is incorrect

In this example, the order of the controller methods is unimportant.

# config/routes.yaml

app_parameter:
    path: '/{foo}'
    defaults:
        _controller: 'App\Controller\FooController::paramRoute'

app_static:
    path: '/static'
    defaults:
        _controller: 'App\Controller\FooController::staticRoute'

The requested path is "/static"

First Symfony will try if the request is matched with the app_parameter route and it matches. So it ignores the real route app_static.

The requested path is "/something"

First Symfony will try if the request is matched with the app_parameter route and it matches.

Annotation route order is correct

In this example, the order of the YAML configuration is unimportant.

# src/Controller/FooController.php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class FooController extends AbstractController
{
    /**
     * @Route("/static")
     */
    public function staticRoute(): Response
    {
        return new Response(__METHOD__);
    }
    
    /**
     * @Route("/{foo}")
     */
    public function paramRoute(string $foo): Response
    {
        return new Response(__METHOD__ . $foo);
    }
}

The requested path is "/static"

First Symfony will try if the request is matched with the app_static route and it matches.

The requested path is "/something"

First Symfony will try if the request is matched with the app_static route and it does not match. So it uses the app_parameter route.

Annotation route order is incorrect

In this example, the order of the YAML configuration is unimportant.

# src/Controller/FooController.php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class FooController extends AbstractController
{
    /**
     * @Route("/{foo}")
     */
    public function paramRoute(string $foo): Response
    {
        return new Response(__METHOD__ . $foo);
    }
    
    /**
     * @Route("/static")
     */
    public function staticRoute(): Response
    {
        return new Response(__METHOD__);
    }
}

The requested path is "/static"

First Symfony will try if the request is matched with the app_parameter route and it matches. So it ignores the real route app_static.

The requested path is "/something"

First Symfony will try if the request is matched with the app_parameter route and it matches.

<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Matcher\TraceableUrlMatcher;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;
class RouterMismatchCommand extends Command
{
protected static $defaultName = 'router:mismatch';
private RouterInterface $router;
private SymfonyStyle $io;
public function __construct(RouterInterface $router)
{
$this->router = $router;
parent::__construct();
}
protected function configure(): void
{
$this->setDescription('Displays mismatched routes for an application');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io = new SymfonyStyle($input, $output);
$routes = $this->router->getRouteCollection();
foreach ($routes as $name => $route) {
$this->convertController($route);
$methods = $route->getMethods();
if (count($methods) === 0) {
$methods = [''];
}
$schemes = $route->getSchemes();
if (count($schemes) === 0) {
$schemes = [''];
}
foreach ($methods as $method) {
if ($method === Request::METHOD_OPTIONS) {
continue;
}
foreach ($schemes as $scheme) {
$this->match($method, $scheme, $route->getHost(), $route->getPath(), $name);
}
}
}
return 0;
}
private function convertController(Route $route): void
{
if ($route->hasDefault('_controller')) {
$nameParser = new ControllerNameParser($this->getApplication()->getKernel());
try {
$route->setDefault('_controller', $nameParser->build($route->getDefault('_controller')));
} catch (\InvalidArgumentException $e) {
}
}
}
private function match(string $method, string $scheme, string $host, string $pathInfo, string $name): void
{
$context = $this->router->getContext();
if ('' !== $method) {
$context->setMethod($method);
}
if ('' !== $scheme) {
$context->setScheme($scheme);
}
if ('' !== $host) {
$context->setHost($host);
}
$matcher = new TraceableUrlMatcher($this->router->getRouteCollection(), $context);
$traces = $matcher->getTraces($pathInfo);
foreach ($traces as $trace) {
if (TraceableUrlMatcher::ROUTE_MATCHES === $trace['level']) {
if ($trace['name'] === $name && $trace['path'] === $pathInfo) {
continue;
}
$this->io->table(
[
'',
'Name',
'Method',
'Scheme',
'Host',
'Path',
],
[
[
'Route',
$name,
$method,
$scheme,
$host,
$pathInfo,
],
[
'Trace',
$trace['name'],
'',
'',
'',
$trace['path'],
],
],
);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment