Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2892226

Browse files
Add analysis for route generation by controllers and UrlGeneratorInterface
1 parent 96ee630 commit 2892226

19 files changed

+433
-0
lines changed

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This extension provides following features:
2323
* Provides correct return types for `TreeBuilder` and `NodeDefinition` objects.
2424
* Notifies you when you try to get an unregistered service from the container.
2525
* Notifies you when you try to get a private service from the container.
26+
* Notifies you when you try to generate a URL for a non-existing route name.
2627
* Optionally correct return types for `InputInterface::getArgument()`, `::getOption`, `::hasArgument`, and `::hasOption`.
2728

2829

@@ -148,3 +149,13 @@ Call the new env in your `console-application.php`:
148149
```php
149150
$kernel = new \App\Kernel('phpstan_env', (bool) $_SERVER['APP_DEBUG']);
150151
```
152+
153+
# Analysis of generating URLs
154+
155+
You have to provide a path to `url_generating_routes.php` for the url generating analysis to work.
156+
157+
```yaml
158+
parameters:
159+
symfony:
160+
urlGeneratingRulesFile: var/cache/dev/url_generating_routes.xml
161+
```

‎composer.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"symfony/http-foundation": "^5.1",
3434
"symfony/messenger": "^4.2 || ^5.0",
3535
"symfony/polyfill-php80": "^1.24",
36+
"symfony/routing": "^4.4 || ^5.0",
3637
"symfony/serializer": "^4.0 || ^5.0"
3738
},
3839
"config": {

‎extension.neon‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ parameters:
1111
constantHassers: true
1212
console_application_loader: null
1313
consoleApplicationLoader: null
14+
url_generating_rules_file: null
15+
urlGeneratingRulesFile: null
1416
stubFiles:
1517
- stubs/Psr/Cache/CacheItemInterface.stub
1618
- stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub
@@ -65,6 +67,8 @@ parametersSchema:
6567
constantHassers: bool()
6668
console_application_loader: schema(string(), nullable())
6769
consoleApplicationLoader: schema(string(), nullable())
70+
url_generating_rules_file: schema(string(), nullable())
71+
urlGeneratingRulesFile: schema(string(), nullable())
6872
])
6973

7074
services:
@@ -89,6 +93,13 @@ services:
8993
-
9094
factory: @symfony.parameterMapFactory::create()
9195

96+
# url generating routes map
97+
symfony.urlGeneratingRoutesMapFactory:
98+
class: PHPStan\Symfony\UrlGeneratingRoutesMapFactory
99+
factory: PHPStan\Symfony\PhpUrlGeneratingRoutesMapFactory
100+
-
101+
factory: @symfony.urlGeneratingRoutesMapFactory::create()
102+
92103
# ControllerTrait::get()/has() return type
93104
-
94105
factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface)

‎rules.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ rules:
55
- PHPStan\Rules\Symfony\InvalidArgumentDefaultValueRule
66
- PHPStan\Rules\Symfony\UndefinedOptionRule
77
- PHPStan\Rules\Symfony\InvalidOptionDefaultValueRule
8+
- PHPStan\Rules\Symfony\UrlGeneratorInterfaceUnknownRouteRule
89

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Symfony;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\Symfony\UrlGeneratingRoutesMap;
11+
use PHPStan\Type\ObjectType;
12+
13+
/**
14+
* @implements Rule<MethodCall>
15+
*/
16+
final class UrlGeneratorInterfaceUnknownRouteRule implements Rule
17+
{
18+
19+
/** @var UrlGeneratingRoutesMap */
20+
private $urlGeneratingRoutesMap;
21+
22+
public function __construct(UrlGeneratingRoutesMap $urlGeneratingRoutesMap)
23+
{
24+
$this->urlGeneratingRoutesMap = $urlGeneratingRoutesMap;
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return MethodCall::class;
30+
}
31+
32+
/**
33+
* @param \PhpParser\Node $node
34+
* @param \PHPStan\Analyser\Scope $scope
35+
* @return (string|\PHPStan\Rules\RuleError)[] errors
36+
*/
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
if (!$node instanceof MethodCall) {
40+
throw new ShouldNotHappenException();
41+
}
42+
43+
if (!$node->name instanceof Node\Identifier) {
44+
return [];
45+
}
46+
47+
if (in_array($node->name->name, ['generate', 'generateUrl'], true) === false || !isset($node->getArgs()[0])) {
48+
return [];
49+
}
50+
51+
$argType = $scope->getType($node->var);
52+
$isControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'))->isSuperTypeOf($argType);
53+
$isAbstractControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'))->isSuperTypeOf($argType);
54+
$isUrlGeneratorInterface = (new ObjectType('Symfony\Component\Routing\Generator\UrlGeneratorInterface'))->isSuperTypeOf($argType);
55+
if (
56+
$isControllerType->no()
57+
&& $isAbstractControllerType->no()
58+
&& $isUrlGeneratorInterface->no()
59+
) {
60+
return [];
61+
}
62+
63+
$routeName = $this->urlGeneratingRoutesMap::getRouteNameFromNode($node->getArgs()[0]->value, $scope);
64+
if ($routeName === null) {
65+
return [];
66+
}
67+
68+
if ($this->urlGeneratingRoutesMap->hasRouteName($routeName) === false) {
69+
return [sprintf('Route with name "%s" does not exist.', $routeName)];
70+
}
71+
72+
return [];
73+
}
74+
75+
}

‎src/Symfony/Configuration.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public function getConsoleApplicationLoader(): ?string
3131
return $this->parameters['consoleApplicationLoader'] ?? $this->parameters['console_application_loader'] ?? null;
3232
}
3333

34+
public function getUrlGeneratingRoutesFile(): ?string
35+
{
36+
return $this->parameters['urlGeneratingRulesFile'] ?? $this->parameters['url_generating_rules_file'] ?? null;
37+
}
38+
3439
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\TypeUtils;
8+
use function count;
9+
10+
final class DefaultUrlGeneratingRoutesMap implements UrlGeneratingRoutesMap
11+
{
12+
13+
/** @var \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] */
14+
private $routes;
15+
16+
/**
17+
* @param \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] $routes
18+
*/
19+
public function __construct(array $routes)
20+
{
21+
$this->routes = $routes;
22+
}
23+
24+
public function hasRouteName(string $name): bool
25+
{
26+
foreach ($this->routes as $route) {
27+
if ($route->getName() === $name) {
28+
return true;
29+
}
30+
}
31+
32+
return false;
33+
}
34+
35+
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
36+
{
37+
$strings = TypeUtils::getConstantStrings($scope->getType($node));
38+
return count($strings) === 1 ? $strings[0]->getValue() : null;
39+
}
40+
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
8+
final class FakeUrlGeneratingRoutesMap implements UrlGeneratingRoutesMap
9+
{
10+
11+
public function hasRouteName(string $name): bool
12+
{
13+
return false;
14+
}
15+
16+
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
17+
{
18+
return null;
19+
}
20+
21+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use function sprintf;
6+
7+
final class PhpUrlGeneratingRoutesMapFactory implements UrlGeneratingRoutesMapFactory
8+
{
9+
10+
/** @var string|null */
11+
private $urlGeneratingRoutesFile;
12+
13+
public function __construct(Configuration $configuration)
14+
{
15+
$this->urlGeneratingRoutesFile = $configuration->getUrlGeneratingRoutesFile();
16+
}
17+
18+
public function create(): UrlGeneratingRoutesMap
19+
{
20+
if ($this->urlGeneratingRoutesFile === null) {
21+
return new FakeUrlGeneratingRoutesMap();
22+
}
23+
24+
if (file_exists($this->urlGeneratingRoutesFile) === false) {
25+
throw new UrlGeneratingRoutesFileNotExistsException(sprintf('File %s containing route generator information does not exist.', $this->urlGeneratingRoutesFile));
26+
}
27+
28+
$urlGeneratingRoutes = require $this->urlGeneratingRoutesFile;
29+
30+
if (!is_array($urlGeneratingRoutes)) {
31+
throw new UrlGeneratingRoutesFileNotExistsException(sprintf('File %s containing route generator information cannot be parsed.', $this->urlGeneratingRoutesFile));
32+
}
33+
34+
/** @var \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] $routes */
35+
$routes = [];
36+
foreach ($urlGeneratingRoutes as $routeName => $routeConfiguration) {
37+
if (!is_string($routeName)) {
38+
continue;
39+
}
40+
41+
if (!is_array($routeConfiguration) || !isset($routeConfiguration[1]['_controller'])) {
42+
continue;
43+
}
44+
45+
$routes[] = new UrlGeneratingRoute($routeName, $routeConfiguration[1]['_controller']);
46+
}
47+
48+
return new DefaultUrlGeneratingRoutesMap($routes);
49+
}
50+
51+
}

‎src/Symfony/UrlGeneratingRoute.php‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
class UrlGeneratingRoute implements UrlGeneratingRoutesDefinition
6+
{
7+
8+
/** @var string */
9+
private $name;
10+
11+
/** @var string */
12+
private $controller;
13+
14+
public function __construct(
15+
string $name,
16+
string $controller
17+
)
18+
{
19+
$this->name = $name;
20+
$this->controller = $controller;
21+
}
22+
23+
public function getName(): string
24+
{
25+
return $this->name;
26+
}
27+
28+
public function getController(): ?string
29+
{
30+
return $this->controller;
31+
}
32+
33+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /