Skip to content

Commit 050990c

Browse files
authored
[symfony] Add NoControllerMethodInjectionRule (#241)
1 parent 947b9c7 commit 050990c

File tree

10 files changed

+201
-0
lines changed

10 files changed

+201
-0
lines changed

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+
# controllers
11+
- Symplify\PHPStanRules\Rules\Symfony\NoControllerMethodInjectionRule
12+
1013
# routing
1114
- Symplify\PHPStanRules\Rules\Symfony\NoRoutingPrefixRule
1215
- Symplify\PHPStanRules\Rules\Symfony\NoClassLevelRouteRule

src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,6 @@ final class SymfonyRuleIdentifier
6161
public const RULE_IDENTIFIER = 'symfony.noServiceAutowireDuplicate';
6262

6363
public const NO_SET_CLASS_SERVICE_DUPLICATE = 'symfony.noSetClassServiceDuplicate';
64+
65+
public const NO_CONTROLLER_METHOD_INJECTION = 'symfony.noControllerMethodInjection';
6466
}

src/Enum/SymfonyClass.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,6 @@ final class SymfonyClass
4343
public const IS_GRANTED = 'Symfony\Component\Security\Http\Attribute\IsGranted';
4444

4545
public const ATTRIBUTE = 'Symfony\Component\DependencyInjection\Attribute\Autowire';
46+
47+
public const REQUEST = 'Symfony\Component\HttpFoundation\Request';
4648
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Identifier;
9+
use PhpParser\Node\Name;
10+
use PhpParser\Node\Stmt\Class_;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier;
15+
use Symplify\PHPStanRules\Enum\SymfonyClass;
16+
17+
/**
18+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\NoControllerMethodInjectionRuleTest
19+
*
20+
* @implements Rule<Class_>
21+
*/
22+
final class NoControllerMethodInjectionRule implements Rule
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public const ERROR_MESSAGE = 'Instead of service "%s" action injection, use __construct() and invokable controller with __invoke() to clearly separate services and parameters';
28+
29+
public function getNodeType(): string
30+
{
31+
return Class_::class;
32+
}
33+
34+
/**
35+
* @param Class_ $node
36+
*/
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
$ruleErrors = [];
40+
41+
if (! $node->name instanceof Identifier) {
42+
return [];
43+
}
44+
45+
$className = $node->name->toString();
46+
47+
if (! str_ends_with($className, 'Controller')) {
48+
return [];
49+
}
50+
51+
foreach ($node->getMethods() as $classMethod) {
52+
if (! $classMethod->isPublic()) {
53+
continue;
54+
}
55+
56+
if ($classMethod->isMagic()) {
57+
continue;
58+
}
59+
60+
if ($classMethod->getParams() === []) {
61+
continue;
62+
}
63+
64+
foreach ($classMethod->getParams() as $param) {
65+
if (! $param->type instanceof Name) {
66+
continue;
67+
}
68+
69+
// Request is allwoed
70+
$typeName = $param->type->toString();
71+
if ($typeName === SymfonyClass::REQUEST) {
72+
continue;
73+
}
74+
75+
$identifierRuleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $typeName))
76+
->identifier(SymfonyRuleIdentifier::NO_CONTROLLER_METHOD_INJECTION)
77+
->build();
78+
79+
$ruleErrors[] = $identifierRuleError;
80+
}
81+
}
82+
83+
return $ruleErrors;
84+
}
85+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\HttpFoundation;
4+
5+
class Request
6+
{
7+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Fixture;
6+
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\Routing\Annotation\Route;
9+
10+
final class SkipRequestParameterController
11+
{
12+
/**
13+
* @Route("/some-action", name="some_action")
14+
*/
15+
public function someRequired(Request $request)
16+
{
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Fixture;
6+
7+
use Symfony\Component\Routing\Annotation\Route;
8+
use Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Source\SomeService;
9+
10+
final class SkipScalarParameterController
11+
{
12+
/**
13+
* @Route("/some-action", name="some_action")
14+
*/
15+
public function someRequired(int $id)
16+
{
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Fixture;
6+
7+
use Symfony\Component\Routing\Annotation\Route;
8+
use Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Source\SomeService;
9+
10+
final class SomeActionInjectionController
11+
{
12+
/**
13+
* @Route("/some-action", name="some_action")
14+
*/
15+
public function someRequired(SomeService $someService)
16+
{
17+
}
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Symfony\NoControllerMethodInjectionRule;
12+
use Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Source\SomeService;
13+
14+
final class NoControllerMethodInjectionRuleTest extends RuleTestCase
15+
{
16+
/**
17+
* @param mixed[] $expectedErrorMessagesWithLines
18+
*/
19+
#[DataProvider('provideData')]
20+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
21+
{
22+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
23+
}
24+
25+
public static function provideData(): Iterator
26+
{
27+
yield [__DIR__ . '/Fixture/SomeActionInjectionController.php', [[
28+
sprintf(NoControllerMethodInjectionRule::ERROR_MESSAGE, SomeService::class),
29+
10,
30+
]]];
31+
32+
yield [__DIR__ . '/Fixture/SkipRequestParameterController.php', []];
33+
yield [__DIR__ . '/Fixture/SkipScalarParameterController.php', []];
34+
}
35+
36+
protected function getRule(): Rule
37+
{
38+
return new NoControllerMethodInjectionRule();
39+
}
40+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoControllerMethodInjectionRule\Source;
4+
5+
final class SomeService
6+
{
7+
8+
}

0 commit comments

Comments
 (0)