Skip to content

Commit 72ba739

Browse files
committed
[symfony] Add NoRoutingPrefixRule
1 parent 404efc4 commit 72ba739

File tree

6 files changed

+160
-0
lines changed

6 files changed

+160
-0
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,41 @@ abstract class AbstractController extends Controller
12901290

12911291
<br>
12921292

1293+
### NoRoutingPrefixRule
1294+
1295+
Avoid global route prefixing. Use single place for paths in @Route/#[Route] and improve static analysis instead.
1296+
1297+
```yaml
1298+
rules:
1299+
- Symplify\PHPStanRules\Rules\Symfony\NoRoutingPrefixRule
1300+
```
1301+
1302+
1303+
```php
1304+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
1305+
1306+
return static function (RoutingConfigurator $routingConfigurator): void {
1307+
$routingConfigurator->import(__DIR__ . '/some-path')
1308+
->prefix('/some-prefix');
1309+
};
1310+
```
1311+
1312+
:x:
1313+
1314+
<br>
1315+
1316+
```php
1317+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
1318+
1319+
return static function (RoutingConfigurator $routingConfigurator): void {
1320+
$routingConfigurator->import(__DIR__ . '/some-path');
1321+
};
1322+
```
1323+
1324+
:+1:
1325+
1326+
<br>
1327+
12931328
### NoRequiredOutsideClassRule
12941329

12951330
Symfony #[Require]/@required should be used only in classes to avoid misuse

src/Enum/ClassName.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@ final class ClassName
9090
* @var string
9191
*/
9292
public const REQUIRED_ATTRIBUTE = 'Symfony\Contracts\Service\Attribute\Required';
93+
94+
/**
95+
* @var string
96+
*/
97+
public const SYMFONY_ROUTE_IMPORT_CONFIGURATOR = 'Symfony\Component\Routing\Loader\Configurator\ImportConfigurator';
9398
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Identifier;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\ObjectType;
15+
use Symplify\PHPStanRules\Enum\ClassName;
16+
17+
/**
18+
* @implements Rule<MethodCall>
19+
*
20+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoRoutingPrefixRule\NoRoutingPrefixRuleTest
21+
*/
22+
final class NoRoutingPrefixRule implements Rule
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public const ERROR_MESSAGE = 'Avoid global route prefixing, to use single place for paths and improve static analysis';
28+
29+
public function getNodeType(): string
30+
{
31+
return MethodCall::class;
32+
}
33+
34+
/**
35+
* @param MethodCall $node
36+
* @return RuleError[]
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if (! $node->name instanceof Identifier) {
41+
return [];
42+
}
43+
44+
$methodName = $node->name->toString();
45+
if ($methodName !== 'prefix') {
46+
return [];
47+
}
48+
49+
$callerType = $scope->getType($node->var);
50+
if (! $callerType instanceof ObjectType) {
51+
return [];
52+
}
53+
54+
if (! $callerType->isInstanceOf(ClassName::SYMFONY_ROUTE_IMPORT_CONFIGURATOR)->yes()) {
55+
return [];
56+
}
57+
58+
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
59+
->line($node->getStartLine())
60+
->build();
61+
62+
return [$ruleError];
63+
}
64+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
6+
7+
return static function (RoutingConfigurator $routingConfigurator): void {
8+
$routingConfigurator->import(__DIR__ . '/some-path')
9+
->prefix('/some-prefix');
10+
};
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+
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
6+
7+
return static function (RoutingConfigurator $routingConfigurator): void {
8+
$routingConfigurator->import(__DIR__ . '/some-path');
9+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoRoutingPrefixRule;
4+
5+
use Iterator;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Symplify\PHPStanRules\Rules\Symfony\NoRoutingPrefixRule;
10+
11+
final class NoRoutingPrefixRuleTest extends RuleTestCase
12+
{
13+
/**
14+
* @param string[] $filePaths
15+
*/
16+
#[DataProvider('provideData')]
17+
public function testRule(array $filePaths, array $expectedErrorsWithLines): void
18+
{
19+
$this->analyse($filePaths, $expectedErrorsWithLines);
20+
}
21+
22+
public static function provideData(): Iterator
23+
{
24+
yield [[
25+
__DIR__ . '/Fixture/skip_no_route_prefix.php',
26+
], []];
27+
28+
yield [[
29+
__DIR__ . '/Fixture/routing_imports.php',
30+
], [[NoRoutingPrefixRule::ERROR_MESSAGE, 8]]];
31+
}
32+
33+
protected function getRule(): Rule
34+
{
35+
return new NoRoutingPrefixRule();
36+
}
37+
}

0 commit comments

Comments
 (0)