Skip to content

Commit 89e9a1e

Browse files
committed
Add requirements check for generated routes
1 parent f342b18 commit 89e9a1e

10 files changed

+163
-2
lines changed

src/Rules/Symfony/UrlGeneratorInterfaceUnknownRouteRule.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Type\ObjectType;
13+
use PHPStan\Type\TypeUtils;
1314

1415
/**
1516
* @implements Rule<MethodCall>
@@ -65,6 +66,32 @@ public function processNode(Node $node, Scope $scope): array
6566
return [sprintf('Route with name "%s" does not exist.', $routeName)];
6667
}
6768

69+
$routeRequirements = $this->urlGeneratingRoutesMap->getRouteRequirements($routeName);
70+
if (
71+
$routeRequirements !== []
72+
&& count($node->getArgs()) < 2
73+
) {
74+
return [sprintf('Route with name "%s" has requires parameters "%s" to be given.', $routeName, implode(', ', array_keys($routeRequirements)))];
75+
}
76+
77+
if (
78+
$routeRequirements !== []
79+
&& $scope->getType($node->getArgs()[1]->value)->isArray()->yes()
80+
) {
81+
$requiredParamsArrayType = $scope->getType($node->getArgs()[1]->value);
82+
$requiredParamConstantStrings = TypeUtils::getConstantStrings($requiredParamsArrayType->getIterableKeyType());
83+
84+
foreach ($routeRequirements as $name => $requirement) {
85+
foreach ($requiredParamConstantStrings as $requiredParamConstantString) {
86+
if ($name === $requiredParamConstantString->getValue()) {
87+
continue 2;
88+
}
89+
}
90+
91+
return [sprintf('Route with name "%s" is missing required param %s.', $routeName, $name)];
92+
}
93+
}
94+
6895
return [];
6996
}
7097
}

src/Symfony/DefaultUrlGeneratingRoutesMap.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ public function hasRouteName(string $name): bool
3333
return false;
3434
}
3535

36+
public function getRouteRequirements(string $name): array
37+
{
38+
foreach ($this->routes as $route) {
39+
if ($route->getName() === $name) {
40+
return $route->getRequiredUrlParams();
41+
}
42+
}
43+
44+
return [];
45+
}
46+
3647
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
3748
{
3849
$strings = TypeUtils::getConstantStrings($scope->getType($node));

src/Symfony/FakeUrlGeneratingRoutesMap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public function hasRouteName(string $name): bool
1414
return false;
1515
}
1616

17+
public function getRouteRequirements(string $name): array
18+
{
19+
return [];
20+
}
21+
1722
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
1823
{
1924
return null;

src/Symfony/PhpUrlGeneratingRoutesMapFactory.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ public function create(): UrlGeneratingRoutesMap
4343
continue;
4444
}
4545

46-
$routes[] = new UrlGeneratingRoute($routeName, $routeConfiguration[1]['_controller']);
46+
$routes[] = new UrlGeneratingRoute(
47+
$routeName,
48+
$routeConfiguration[1]['_controller'],
49+
$routeConfiguration[2] ?? []
50+
);
4751
}
4852

4953
return new DefaultUrlGeneratingRoutesMap($routes);

src/Symfony/UrlGeneratingRoute.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ class UrlGeneratingRoute implements UrlGeneratingRoutesDefinition
1212
/** @var string */
1313
private $controller;
1414

15+
/** @var array<string, string> */
16+
private $urlRequiredParams;
17+
18+
/**
19+
* @param array<string, string> $urlParams
20+
*/
1521
public function __construct(
1622
string $name,
17-
string $controller
23+
string $controller,
24+
array $urlParams
1825
) {
1926
$this->name = $name;
2027
$this->controller = $controller;
28+
$this->urlRequiredParams = $urlParams;
2129
}
2230

2331
public function getName(): string
@@ -29,4 +37,10 @@ public function getController(): ?string
2937
{
3038
return $this->controller;
3139
}
40+
41+
/** @return array<string, string> */
42+
public function getRequiredUrlParams(): array
43+
{
44+
return $this->urlRequiredParams;
45+
}
3246
}

src/Symfony/UrlGeneratingRoutesDefinition.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ interface UrlGeneratingRoutesDefinition
99
public function getName(): string;
1010

1111
public function getController(): ?string;
12+
13+
/** @return array<string, string> */
14+
public function getRequiredUrlParams(): array;
1215
}

src/Symfony/UrlGeneratingRoutesMap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ interface UrlGeneratingRoutesMap
1111
{
1212
public function hasRouteName(string $name): bool;
1313

14+
/** @return array<string, string> */
15+
public function getRouteRequirements(string $name): array;
16+
1417
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string;
1518
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DaDaDev\Rules\Symfony;
6+
7+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
8+
9+
final class ExampleControllerWithRoutingAndRequirements extends AbstractController
10+
{
11+
public function generateSomeRoute1(): void
12+
{
13+
$this->generateUrl('someRoute1');
14+
}
15+
16+
public function generateSomeRoute2(): void
17+
{
18+
$this->generateUrl('someRoute1', ['number' => 1]);
19+
}
20+
21+
public function generateSomeRoute3(): void
22+
{
23+
$this->generateUrl('someRoute1', ['date' => '2022-05-25']);
24+
}
25+
26+
public function generateSomeRoute4(): void
27+
{
28+
$this->generateUrl('someRoute1', ['number' => 1, 'date' => '2022-05-25']);
29+
}
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DaDaDev\Rules\Symfony;
6+
7+
use DaDaDev\Symfony\Configuration;
8+
use DaDaDev\Symfony\PhpUrlGeneratingRoutesMapFactory;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Testing\RuleTestCase;
11+
12+
/**
13+
* @extends RuleTestCase<UrlGeneratorInterfaceUnknownRouteRule>
14+
*/
15+
final class UrlGeneratorInterfaceRouteWithRequirementsRuleTest extends RuleTestCase
16+
{
17+
protected function getRule(): Rule
18+
{
19+
return new UrlGeneratorInterfaceUnknownRouteRule((new PhpUrlGeneratingRoutesMapFactory(new Configuration(['urlGeneratingRulesFile' => __DIR__ . '/url_generating_routes_with_requirements.php'])))->create());
20+
}
21+
22+
public function testGenerate(): void
23+
{
24+
if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\AbstractController')) {
25+
self::markTestSkipped();
26+
}
27+
28+
$this->analyse(
29+
[
30+
__DIR__ . '/ExampleControllerWithRoutingAndRequirements.php',
31+
],
32+
[
33+
[
34+
'Route with name "someRoute1" has requires parameters "number, date" to be given.',
35+
13,
36+
],
37+
[
38+
'Route with name "someRoute1" is missing required param date.',
39+
18,
40+
],
41+
[
42+
'Route with name "someRoute1" is missing required param number.',
43+
23,
44+
],
45+
]
46+
);
47+
}
48+
49+
public static function getAdditionalConfigFiles(): array
50+
{
51+
return [
52+
__DIR__ . '/../../../extension.neon',
53+
];
54+
}
55+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
return [
6+
'someRoute1' => [[], ['_controller' => 'SomeController'], ['number' => '\\d+', 'date' => '\\d{4}-\\d{2}-\\d{2}'] /* ... */],
7+
'someRoute2' => [[], ['_controller' => 'SomeController'], ['number' => '\\d+', 'date' => '\\d{4}-\\d{2}-\\d{2}'] /* ... */],
8+
'someRoute3' => [[], ['_controller' => 'SomeController'], ['number' => '\\d+', 'date' => '\\d{4}-\\d{2}-\\d{2}'] /* ... */],
9+
];

0 commit comments

Comments
 (0)