Skip to content

Commit 2cbac09

Browse files
authored
[symfony] Add NoRoutingPrefixRule (#172)
1 parent 404efc4 commit 2cbac09

File tree

8 files changed

+167
-0
lines changed

8 files changed

+167
-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

config/symfony-rules.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ rules:
77
- Symplify\PHPStanRules\Rules\Symfony\RequireInvokableControllerRule
88
- Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule
99

10+
# routing
11+
- Symplify\PHPStanRules\Rules\Symfony\NoRoutingPrefixRule
12+
1013
# dependency injection
1114
- Symplify\PHPStanRules\Rules\Symfony\NoGetDoctrineInControllerRule
1215
- Symplify\PHPStanRules\Rules\Symfony\NoGetInControllerRule

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
}

src/Enum/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ final class SymfonyRuleIdentifier
2929
public const NO_CONSTRUCT_AND_REQUIRED = 'symfony.noConstructAndRequired';
3030

3131
public const FORM_TYPE_CLASS_NAME = 'symfony.formTypeClassName';
32+
33+
public const NO_ROUTING_PREFIX = 'symfony.noRoutingPrefix';
3234
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;
17+
18+
/**
19+
* @implements Rule<MethodCall>
20+
*
21+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoRoutingPrefixRule\NoRoutingPrefixRuleTest
22+
*/
23+
final class NoRoutingPrefixRule implements Rule
24+
{
25+
/**
26+
* @var string
27+
*/
28+
public const ERROR_MESSAGE = 'Avoid global route prefixing, to use single place for paths and improve static analysis';
29+
30+
public function getNodeType(): string
31+
{
32+
return MethodCall::class;
33+
}
34+
35+
/**
36+
* @param MethodCall $node
37+
* @return RuleError[]
38+
*/
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
if (! $node->name instanceof Identifier) {
42+
return [];
43+
}
44+
45+
$methodName = $node->name->toString();
46+
if ($methodName !== 'prefix') {
47+
return [];
48+
}
49+
50+
$callerType = $scope->getType($node->var);
51+
if (! $callerType instanceof ObjectType) {
52+
return [];
53+
}
54+
55+
if (! $callerType->isInstanceOf(ClassName::SYMFONY_ROUTE_IMPORT_CONFIGURATOR)->yes()) {
56+
return [];
57+
}
58+
59+
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
60+
->line($node->getStartLine())
61+
->identifier(SymfonyRuleIdentifier::NO_ROUTING_PREFIX)
62+
->build();
63+
64+
return [$ruleError];
65+
}
66+
}
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)