-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[FrameworkBundle] [Routing] Added --sort to debug:router #36579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f663aa0
afba921
ced94da
49029ae
6f743c1
4a034ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Console\Style\SymfonyStyle; | ||
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; | ||
use Symfony\Component\Routing\Route; | ||
use Symfony\Component\Routing\RouteCollection; | ||
use Symfony\Component\Routing\RouterInterface; | ||
|
||
|
@@ -58,6 +59,7 @@ protected function configure() | |
new InputOption('show-controllers', null, InputOption::VALUE_NONE, 'Show assigned controllers in overview'), | ||
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), | ||
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), | ||
new InputOption('sort', null, InputOption::VALUE_REQUIRED, 'The sorting criterion (priority, name, or path)', 'priority'), | ||
]) | ||
->setDescription('Displays current routes for an application') | ||
->setHelp(<<<'EOF' | ||
|
@@ -80,8 +82,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int | |
$io = new SymfonyStyle($input, $output); | ||
$name = $input->getArgument('name'); | ||
$helper = new DescriptorHelper($this->fileLinkFormatter); | ||
$routes = $this->router->getRouteCollection(); | ||
$container = $this->fileLinkFormatter ? \Closure::fromCallable([$this, 'getContainerBuilder']) : null; | ||
$sortOption = $input->getOption('sort'); | ||
$routes = $this->sortRoutes($this->router->getRouteCollection(), $sortOption); | ||
if ('priority' !== $sortOption) { | ||
$io->caution(sprintf('The routes are not sorted in the order they get matched but by %s.', $sortOption)); | ||
} | ||
|
||
if ($name) { | ||
if (!($route = $routes->get($name)) && $matchingRoutes = $this->findRouteNameContaining($name, $routes)) { | ||
|
@@ -125,4 +131,22 @@ private function findRouteNameContaining(string $name, RouteCollection $routes): | |
|
||
return $foundRoutesNames; | ||
} | ||
|
||
private function sortRoutes(RouteCollection $routes, string $propertyName): RouteCollection | ||
{ | ||
$sortedRoutes = $routes->all(); | ||
if ('name' === $propertyName) { | ||
ksort($sortedRoutes); | ||
} elseif ('path' === $propertyName) { | ||
uasort($sortedRoutes, function ($a, $b) { return $a->getPath() <=> $b->getPath(); }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add the type for $a and $b There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if routes have the same path but different prio, this can result is routes getting mixed up. sorting will only be stable in php 8: https://wiki.php.net/rfc/stable_sorting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll refactor the test to verify the stability |
||
} elseif ('priority' !== $propertyName) { | ||
throw new InvalidArgumentException(sprintf('The option "%s" is not valid.', $propertyName)); | ||
} | ||
$routeCollection = new RouteCollection(); | ||
foreach ($sortedRoutes as $routeName => $route) { | ||
$routeCollection->add($routeName, $route); | ||
} | ||
|
||
return $routeCollection; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
<?php | ||
|
||
namespace Symfony\Bundle\FrameworkBundle\Tests\Command; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; | ||
use Symfony\Bundle\FrameworkBundle\Console\Application; | ||
use Symfony\Component\Console\Tester\CommandTester; | ||
use Symfony\Component\DependencyInjection\ContainerInterface; | ||
use Symfony\Component\HttpKernel\KernelInterface; | ||
use Symfony\Component\Routing\RequestContext; | ||
use Symfony\Component\Routing\Route; | ||
use Symfony\Component\Routing\RouteCollection; | ||
use Symfony\Component\Routing\RouterInterface; | ||
|
||
class RouterDebugCommandTest extends TestCase | ||
{ | ||
public function testSortingByPriority() | ||
{ | ||
$tester = $this->createCommandTester(); | ||
$result = $tester->execute(['--sort' => 'priority'], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(charlie).*\n\W*(alfa).*\n\W*(delta).*\n\W*(bravo)/m', $tester->getDisplay(true)); | ||
$this->assertStringNotContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
public function testSortingByName() | ||
{ | ||
$tester = $this->createCommandTester(); | ||
$result = $tester->execute(['--sort' => 'name'], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(alfa).*\n\W*(bravo).*\n\W*(charlie).*\n\W*(delta)/m', $tester->getDisplay(true)); | ||
$this->assertStringContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
public function testSortingByPath() | ||
{ | ||
$tester = $this->createCommandTester(); | ||
$result = $tester->execute(['--sort' => 'path'], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(\/romeo).*\n.*(\/sierra).*\n.*(\/tango).*\n.*(\/uniform)/m', $tester->getDisplay(true)); | ||
$this->assertStringContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
public function testThrowsExceptionWithInvalidParameter() | ||
{ | ||
$tester = $this->createCommandTester(); | ||
$this->expectExceptionMessage('The option "foobar" is not valid'); | ||
$tester->execute(['--sort' => 'foobar'], ['decorated' => false]); | ||
} | ||
|
||
public function testThrowsExceptionWithNullParameter() | ||
{ | ||
$tester = $this->createCommandTester(); | ||
$this->expectExceptionMessage('The "--sort" option requires a value.'); | ||
$tester->execute(['--sort' => null], ['decorated' => false]); | ||
} | ||
|
||
public function testSortingByPriorityWithDuplicatePath() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this test is useless as it doesn't cover anything new |
||
{ | ||
$tester = $this->createCommandTesterWithDuplicatePath(); | ||
$result = $tester->execute(['--sort' => 'priority'], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(charlie).*\n\W*(alfa).*\n\W*(delta).*\n\W*(bravo).*\n\W*(echo)/m', $tester->getDisplay(true)); | ||
$this->assertStringNotContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
public function testSortingByNameWithDuplicatePath() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this test is useless as it doesn't cover anything new |
||
{ | ||
$tester = $this->createCommandTesterWithDuplicatePath(); | ||
$result = $tester->execute(['--sort' => 'name'], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(alfa).*\n\W*(bravo).*\n\W*(charlie).*\n\W*(delta).*\n\W*(echo)/m', $tester->getDisplay(true)); | ||
$this->assertStringContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
public function testSortingByPathWithDuplicatePath() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test should actually also make sure the routes with the same path stay in the correct order as before (check routes names) as I mention in the stable sorting comment |
||
{ | ||
$tester = $this->createCommandTesterWithDuplicatePath(); | ||
$result = $tester->execute(['--sort' => 'path'], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(\/romeo).*\n.*(\/sierra).*\n.*(\/tango).*\n.*(\/uniform).*\n.*(\/uniform)/m', $tester->getDisplay(true)); | ||
$this->assertStringContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
public function testWithoutCallingSortOptionExplicitly() | ||
{ | ||
$tester = $this->createCommandTester(); | ||
$result = $tester->execute([], ['decorated' => false]); | ||
$this->assertSame(0, $result, 'Returns 0 in case of success'); | ||
$this->assertRegExp('/(charlie).*\n\W*(alfa).*\n\W*(delta).*\n\W*(bravo)/m', $tester->getDisplay(true)); | ||
$this->assertStringNotContainsString('! [CAUTION] The routes list is not sorted in the parsing order.', $tester->getDisplay(true)); | ||
} | ||
|
||
private function createCommandTester(): CommandTester | ||
{ | ||
$application = new Application($this->getKernel()); | ||
$application->add(new RouterDebugCommand($this->getRouter())); | ||
|
||
return new CommandTester($application->find('debug:router')); | ||
} | ||
|
||
private function createCommandTesterWithDuplicatePath(): CommandTester | ||
{ | ||
$application = new Application($this->getKernel()); | ||
$application->add(new RouterDebugCommand($this->getRouterWithDuplicatePath())); | ||
|
||
return new CommandTester($application->find('debug:router')); | ||
} | ||
|
||
private function getRouter() | ||
{ | ||
$routeCollection = new RouteCollection(); | ||
$routeCollection->add('charlie', new Route('uniform')); | ||
$routeCollection->add('alfa', new Route('sierra')); | ||
$routeCollection->add('delta', new Route('tango')); | ||
$routeCollection->add('bravo', new Route('romeo')); | ||
$requestContext = new RequestContext(); | ||
$router = $this->createMock(RouterInterface::class); | ||
$router | ||
->expects($this->any()) | ||
->method('getRouteCollection') | ||
->willReturn($routeCollection); | ||
$router | ||
->expects($this->any()) | ||
->method('getContext') | ||
->willReturn($requestContext); | ||
|
||
return $router; | ||
} | ||
|
||
private function getRouterWithDuplicatePath() | ||
{ | ||
$routeCollection = new RouteCollection(); | ||
$routeCollection->add('charlie', (new Route('uniform'))->setMethods('GET')); | ||
$routeCollection->add('alfa', new Route('sierra')); | ||
$routeCollection->add('delta', new Route('tango')); | ||
$routeCollection->add('bravo', new Route('romeo')); | ||
$routeCollection->add('echo', (new Route('uniform'))->setMethods('POST')); | ||
|
||
$requestContext = new RequestContext(); | ||
$router = $this->createMock(RouterInterface::class); | ||
$router | ||
->expects($this->any()) | ||
->method('getRouteCollection') | ||
->willReturn($routeCollection); | ||
$router | ||
->expects($this->any()) | ||
->method('getContext') | ||
->willReturn($requestContext); | ||
|
||
return $router; | ||
} | ||
|
||
private function getKernel() | ||
{ | ||
$container = $this->createMock(ContainerInterface::class); | ||
$container | ||
->expects($this->atLeastOnce()) | ||
->method('has') | ||
->willReturnCallback(function ($id) { | ||
return 'console.command_loader' !== $id; | ||
}) | ||
; | ||
$container | ||
->expects($this->any()) | ||
->method('get') | ||
->with('router') | ||
->willReturn($this->getRouter()) | ||
; | ||
|
||
$kernel = $this->createMock(KernelInterface::class); | ||
$kernel | ||
->expects($this->any()) | ||
->method('getContainer') | ||
->willReturn($container) | ||
; | ||
$kernel | ||
->expects($this->once()) | ||
->method('getBundles') | ||
->willReturn([]) | ||
; | ||
|
||
return $kernel; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a
switch
statement would be a better fit hereThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I'm going to change this