Skip to content

[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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ CHANGELOG
* Made `BrowserKitAssertionsTrait` report the original error message in case of a failure
* Added ability for `config:dump-reference` and `debug:config` to dump and debug kernel container extension configuration.
* Deprecated `session.attribute_bag` service and `session.flash_bag` service.
* Added `debug:router --sort` option to see routes sorted by name or path as well as by priority

5.0.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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'
Expand All @@ -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)) {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

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 here

Copy link
Author

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

ksort($sortedRoutes);
} elseif ('path' === $propertyName) {
uasort($sortedRoutes, function ($a, $b) { return $a->getPath() <=> $b->getPath(); });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add the type for $a and $b

Copy link
Contributor

Choose a reason for hiding this comment

The 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
So we would need to implement the stable part ourselves prio to php 8.

Copy link
Author

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy