Skip to content

Commit 3d9c69d

Browse files
committed
Add 'method' option to debug:router command to filter routes by HTTP method
1 parent 6477041 commit 3d9c69d

13 files changed

+265
-4
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add `framework.validation.disable_translation` option
1414
* Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration
1515
* Deprecate the `framework.validation.cache` option
16+
* Add `--method` option to the `debug:router` command
1617

1718
7.2
1819
---

src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php

+11-3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ protected function configure(): void
5555
new InputOption('show-aliases', null, InputOption::VALUE_NONE, 'Show aliases in overview'),
5656
new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'),
5757
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'),
58+
new InputOption('method', null, InputOption::VALUE_REQUIRED, 'Filter by HTTP method', null, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']),
5859
])
5960
->setHelp(<<<'EOF'
6061
The <info>%command.name%</info> displays the configured routes:
@@ -76,6 +77,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7677
{
7778
$io = new SymfonyStyle($input, $output);
7879
$name = $input->getArgument('name');
80+
$methodOption = $input->getOption('method');
81+
$method = $methodOption ? strtoupper($methodOption) : null;
7982
$helper = new DescriptorHelper($this->fileLinkFormatter);
8083
$routes = $this->router->getRouteCollection();
8184
$container = null;
@@ -85,7 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8588

8689
if ($name) {
8790
$route = $routes->get($name);
88-
$matchingRoutes = $this->findRouteNameContaining($name, $routes);
91+
$matchingRoutes = $this->findRouteNameContaining($name, $routes, $method);
8992

9093
if (!$input->isInteractive() && !$route && \count($matchingRoutes) > 1) {
9194
$helper->describe($io, $this->findRouteContaining($name, $routes), [
@@ -94,6 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9497
'show_controllers' => $input->getOption('show-controllers'),
9598
'show_aliases' => $input->getOption('show-aliases'),
9699
'output' => $io,
100+
'method' => $method,
97101
]);
98102

99103
return 0;
@@ -124,17 +128,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
124128
'show_aliases' => $input->getOption('show-aliases'),
125129
'output' => $io,
126130
'container' => $container,
131+
'method' => $method,
127132
]);
128133
}
129134

130135
return 0;
131136
}
132137

133-
private function findRouteNameContaining(string $name, RouteCollection $routes): array
138+
private function findRouteNameContaining(string $name, RouteCollection $routes, ?string $method): array
134139
{
135140
$foundRoutesNames = [];
136141
foreach ($routes as $routeName => $route) {
137-
if (false !== stripos($routeName, $name)) {
142+
if (
143+
false !== stripos($routeName, $name)
144+
&& (null === $method || !$route->getMethods() || \in_array($method, $route->getMethods(), true))
145+
) {
138146
$foundRoutesNames[] = $routeName;
139147
}
140148
}

src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php

+27-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function describe(OutputInterface $output, mixed $object, array $options
4949
}
5050

5151
match (true) {
52-
$object instanceof RouteCollection => $this->describeRouteCollection($object, $options),
52+
$object instanceof RouteCollection => $options['method'] ? $this->describeRouteCollection($this->filterRoutesByHttpMethod($object, $options['method']), $options) : $this->describeRouteCollection($object, $options),
5353
$object instanceof Route => $this->describeRoute($object, $options),
5454
$object instanceof ParameterBag => $this->describeContainerParameters($object, $options),
5555
$object instanceof ContainerBuilder && !empty($options['env-vars']) => $this->describeContainerEnvVars($this->getContainerEnvVars($object), $options),
@@ -360,4 +360,30 @@ protected function getServiceEdges(ContainerBuilder $container, string $serviceI
360360
return [];
361361
}
362362
}
363+
364+
/**
365+
* Filters the given collection of routes by the specified HTTP method.
366+
* This function iterates through the provided routes and removes those
367+
* whose allowed methods do not include the specified HTTP method.
368+
*
369+
* @param RouteCollection $routes the collection of routes to be filtered
370+
* @param string $method The HTTP method to filter routes by (e.g., 'GET', 'POST').
371+
*
372+
* @return RouteCollection the filtered collection of routes
373+
*/
374+
private function filterRoutesByHttpMethod(RouteCollection $routes, string $method): RouteCollection
375+
{
376+
$filteredRoutes = clone $routes;
377+
378+
foreach ($filteredRoutes as $routeName => $route) {
379+
if (
380+
$route->getMethods()
381+
&& !\in_array($method, $route->getMethods(), true)
382+
) {
383+
$filteredRoutes->remove($routeName);
384+
}
385+
}
386+
387+
return $filteredRoutes;
388+
}
363389
}

src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTestCase.php

+19
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ public static function getDescribeRouteCollectionTestData(): array
5050
return static::getDescriptionTestData(ObjectsProvider::getRouteCollections());
5151
}
5252

53+
/** @dataProvider getDescribeRouteCollectionWithHttpMethodFilterTestData */
54+
public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription)
55+
{
56+
$this->assertDescription($expectedDescription, $routes, ['method' => $httpMethod]);
57+
}
58+
59+
public static function getDescribeRouteCollectionWithHttpMethodFilterTestData(): iterable
60+
{
61+
foreach (ObjectsProvider::getRouteCollectionsByHttpMethod() as $httpMethod => $routeCollection) {
62+
foreach (static::getDescriptionTestData($routeCollection) as $testData) {
63+
yield [
64+
$httpMethod,
65+
...$testData,
66+
];
67+
}
68+
}
69+
}
70+
5371
/** @dataProvider getDescribeRouteTestData */
5472
public function testDescribeRoute(Route $route, $expectedDescription)
5573
{
@@ -273,6 +291,7 @@ private function assertDescription($expectedDescription, $describedObject, array
273291
$options['is_debug'] = false;
274292
$options['raw_output'] = true;
275293
$options['raw_text'] = true;
294+
$options['method'] ??= null;
276295
$output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true);
277296

278297
if ('txt' === $this->getFormat()) {

src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php

+32
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,38 @@ public static function getRouteCollections()
3737
return ['route_collection_1' => $collection1];
3838
}
3939

40+
public static function getRouteCollectionsByHttpMethod(): array
41+
{
42+
$collection = new RouteCollection();
43+
foreach (self::getRoutes() as $name => $route) {
44+
$collection->add($name, $route);
45+
}
46+
47+
// Clone the original collection and add a route without any specific method restrictions
48+
$collectionWithRouteWithoutMethodRestriction = clone $collection;
49+
$collectionWithRouteWithoutMethodRestriction->add(
50+
'route_3',
51+
new RouteStub(
52+
'/other/route',
53+
[],
54+
[],
55+
['opt1' => 'val1', 'opt2' => 'val2'],
56+
'localhost',
57+
['http', 'https'],
58+
[],
59+
)
60+
);
61+
62+
return [
63+
'GET' => [
64+
'route_collection_2' => $collectionWithRouteWithoutMethodRestriction,
65+
],
66+
'PUT' => [
67+
'route_collection_3' => $collection,
68+
],
69+
];
70+
}
71+
4072
public static function getRoutes()
4173
{
4274
return [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"route_1": {
3+
"path": "\/hello\/{name}",
4+
"pathRegex": "#PATH_REGEX#",
5+
"host": "localhost",
6+
"hostRegex": "#HOST_REGEX#",
7+
"scheme": "http|https",
8+
"method": "GET|HEAD",
9+
"class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub",
10+
"defaults": {
11+
"name": "Joseph"
12+
},
13+
"requirements": {
14+
"name": "[a-z]+"
15+
},
16+
"options": {
17+
"compiler_class": "Symfony\\Component\\Routing\\RouteCompiler",
18+
"opt1": "val1",
19+
"opt2": "val2"
20+
}
21+
},
22+
"route_3": {
23+
"path": "\/other\/route",
24+
"pathRegex": "#PATH_REGEX#",
25+
"host": "localhost",
26+
"hostRegex": "#HOST_REGEX#",
27+
"scheme": "http|https",
28+
"method": "ANY",
29+
"class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub",
30+
"defaults": [],
31+
"requirements": "NO CUSTOM",
32+
"options": {
33+
"compiler_class": "Symfony\\Component\\Routing\\RouteCompiler",
34+
"opt1": "val1",
35+
"opt2": "val2"
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
route_1
2+
-------
3+
4+
- Path: /hello/{name}
5+
- Path Regex: #PATH_REGEX#
6+
- Host: localhost
7+
- Host Regex: #HOST_REGEX#
8+
- Scheme: http|https
9+
- Method: GET|HEAD
10+
- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub
11+
- Defaults:
12+
- `name`: Joseph
13+
- Requirements:
14+
- `name`: [a-z]+
15+
- Options:
16+
- `compiler_class`: Symfony\Component\Routing\RouteCompiler
17+
- `opt1`: val1
18+
- `opt2`: val2
19+
20+
21+
route_3
22+
-------
23+
24+
- Path: /other/route
25+
- Path Regex: #PATH_REGEX#
26+
- Host: localhost
27+
- Host Regex: #HOST_REGEX#
28+
- Scheme: http|https
29+
- Method: ANY
30+
- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub
31+
- Defaults: NONE
32+
- Requirements: NO CUSTOM
33+
- Options:
34+
- `compiler_class`: Symfony\Component\Routing\RouteCompiler
35+
- `opt1`: val1
36+
- `opt2`: val2
37+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
--------- ---------- ------------ ----------- ---------------
2+
 Name   Method   Scheme   Host   Path 
3+
--------- ---------- ------------ ----------- ---------------
4+
route_1 GET|HEAD http|https localhost /hello/{name}
5+
route_3 ANY http|https localhost /other/route
6+
--------- ---------- ------------ ----------- ---------------
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<routes>
3+
<route name="route_1" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub">
4+
<path regex="#PATH_REGEX#">/hello/{name}</path>
5+
<host regex="#HOST_REGEX#">localhost</host>
6+
<scheme>http</scheme>
7+
<scheme>https</scheme>
8+
<method>GET</method>
9+
<method>HEAD</method>
10+
<defaults>
11+
<default key="name">Joseph</default>
12+
</defaults>
13+
<requirements>
14+
<requirement key="name">[a-z]+</requirement>
15+
</requirements>
16+
<options>
17+
<option key="compiler_class">Symfony\Component\Routing\RouteCompiler</option>
18+
<option key="opt1">val1</option>
19+
<option key="opt2">val2</option>
20+
</options>
21+
</route>
22+
<route name="route_3" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub">
23+
<path regex="#PATH_REGEX#">/other/route</path>
24+
<host regex="#HOST_REGEX#">localhost</host>
25+
<scheme>http</scheme>
26+
<scheme>https</scheme>
27+
<options>
28+
<option key="compiler_class">Symfony\Component\Routing\RouteCompiler</option>
29+
<option key="opt1">val1</option>
30+
<option key="opt2">val2</option>
31+
</options>
32+
</route>
33+
</routes>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"route_2": {
3+
"path": "\/name\/add",
4+
"pathRegex": "#PATH_REGEX#",
5+
"host": "localhost",
6+
"hostRegex": "#HOST_REGEX#",
7+
"scheme": "http|https",
8+
"method": "PUT|POST",
9+
"class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub",
10+
"defaults": [],
11+
"requirements": "NO CUSTOM",
12+
"options": {
13+
"compiler_class": "Symfony\\Component\\Routing\\RouteCompiler",
14+
"opt1": "val1",
15+
"opt2": "val2"
16+
},
17+
"condition": "context.getMethod() in ['GET', 'HEAD', 'POST']"
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
route_2
2+
-------
3+
4+
- Path: /name/add
5+
- Path Regex: #PATH_REGEX#
6+
- Host: localhost
7+
- Host Regex: #HOST_REGEX#
8+
- Scheme: http|https
9+
- Method: PUT|POST
10+
- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub
11+
- Defaults: NONE
12+
- Requirements: NO CUSTOM
13+
- Options:
14+
- `compiler_class`: Symfony\Component\Routing\RouteCompiler
15+
- `opt1`: val1
16+
- `opt2`: val2
17+
- Condition: context.getMethod() in ['GET', 'HEAD', 'POST']
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
--------- ---------- ------------ ----------- -----------
2+
 Name   Method   Scheme   Host   Path 
3+
--------- ---------- ------------ ----------- -----------
4+
route_2 PUT|POST http|https localhost /name/add
5+
--------- ---------- ------------ ----------- -----------
6+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<routes>
3+
<route name="route_2" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub">
4+
<path regex="#PATH_REGEX#">/name/add</path>
5+
<host regex="#HOST_REGEX#">localhost</host>
6+
<scheme>http</scheme>
7+
<scheme>https</scheme>
8+
<method>PUT</method>
9+
<method>POST</method>
10+
<options>
11+
<option key="compiler_class">Symfony\Component\Routing\RouteCompiler</option>
12+
<option key="opt1">val1</option>
13+
<option key="opt2">val2</option>
14+
</options>
15+
<condition>context.getMethod() in ['GET', 'HEAD', 'POST']</condition>
16+
</route>
17+
</routes>

0 commit comments

Comments
 (0)