Skip to content

Commit 158f4d6

Browse files
authored
[symfony] Add NoConstructorAndRequiredTogetherRule (#168)
* [symfony] Add NoConstructorAndRequiredTogetherRule * docs
1 parent 12b7b11 commit 158f4d6

File tree

9 files changed

+211
-0
lines changed

9 files changed

+211
-0
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,17 @@ final class SomeFixture extends AbstractFixture
11771177

11781178
## 3. Symfony-specific Rules
11791179

1180+
### NoConstructorAndRequiredTogetherRule
1181+
1182+
Constructor injection and `#[Required]` method should not be used together in single class. Pick one, to keep architecture clean.
1183+
1184+
```yaml
1185+
rules:
1186+
- Symplify\PHPStanRules\Rules\Symfony\NoConstructorAndRequiredTogetherRule
1187+
```
1188+
1189+
<br>
1190+
11801191
### NoGetDoctrineInControllerRule
11811192

11821193
Prevents using `$this->getDoctrine()` in controllers, to promote dependency injection.

src/Enum/MethodName.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ final class MethodName
1010
* @var string
1111
*/
1212
public const INVOKE = '__invoke';
13+
14+
public const CONSTRUCTOR = '__construct';
1315
}

src/Enum/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ final class SymfonyRuleIdentifier
2525
public const SINGLE_REQUIRED_METHOD = 'symfony.singleRequiredMethod';
2626

2727
public const SYMFONY_REQUIRED_ONLY_IN_ABSTRACT = 'symfony.requiredOnlyInAbstract';
28+
29+
public const NO_CONSTRUCT_AND_REQUIRED = 'symfony.noConstructAndRequired';
2830
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony;
6+
7+
use PhpParser\Comment\Doc;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\IdentifierRuleError;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symplify\PHPStanRules\Enum\MethodName;
15+
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;
16+
17+
/**
18+
* @implements Rule<Class_>
19+
*
20+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule\NoConstructorAndRequiredTogetherRuleTest
21+
*/
22+
final class NoConstructorAndRequiredTogetherRule implements Rule
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public const ERROR_MESSAGE = 'Avoid using __construct() and @required in the same class. Pick one to keep architecture clean';
28+
29+
public function getNodeType(): string
30+
{
31+
return Class_::class;
32+
}
33+
34+
/**
35+
* @param Class_ $node
36+
* @return IdentifierRuleError[]
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if ($node->isAnonymous()) {
41+
return [];
42+
}
43+
44+
if (! $node->getMethod(MethodName::CONSTRUCTOR)) {
45+
return [];
46+
}
47+
48+
if (! $this->hasAutowiredMethod($node)) {
49+
return [];
50+
}
51+
52+
$ruleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
53+
->identifier(SymfonyRuleIdentifier::NO_CONSTRUCT_AND_REQUIRED)
54+
->build();
55+
56+
return [
57+
$ruleError,
58+
];
59+
}
60+
61+
private function hasAutowiredMethod(Class_ $class): bool
62+
{
63+
foreach ($class->getMethods() as $classMethod) {
64+
if (! $classMethod->isPublic()) {
65+
continue;
66+
}
67+
68+
$docComment = $classMethod->getDocComment();
69+
if (! $docComment instanceof Doc) {
70+
continue;
71+
}
72+
73+
if (! str_contains($docComment->getText(), '@required')) {
74+
continue;
75+
}
76+
77+
// special case when its allowed, to avoid circular references
78+
if (str_contains($docComment->getText(), 'circular')) {
79+
continue;
80+
}
81+
82+
return true;
83+
}
84+
85+
return false;
86+
}
87+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule\Fixture;
6+
7+
final class ConstructorAndRequiredInSingleClass
8+
{
9+
public function __construct()
10+
{
11+
}
12+
13+
/**
14+
* @required
15+
*/
16+
public function someRequired()
17+
{
18+
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule\Fixture;
6+
7+
final class SkipCircularDependencyPrevention
8+
{
9+
public function __construct()
10+
{
11+
}
12+
13+
/**
14+
* Avoid circular dependency
15+
* @required
16+
*/
17+
public function someRequired()
18+
{
19+
20+
}
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule\Fixture;
6+
7+
final class SkipOtherSoleMethod
8+
{
9+
/**
10+
* @required
11+
*/
12+
public function autowire()
13+
{
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule\Fixture;
6+
7+
final class SkipSoleMethod
8+
{
9+
public function __construct()
10+
{
11+
}
12+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\NoConstructorAndRequiredTogetherRule;
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\NoConstructorAndRequiredTogetherRule;
12+
13+
final class NoConstructorAndRequiredTogetherRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param mixed[] $expectedErrorMessagesWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
22+
}
23+
24+
public static function provideData(): Iterator
25+
{
26+
yield [__DIR__ . '/Fixture/ConstructorAndRequiredInSingleClass.php', [[
27+
NoConstructorAndRequiredTogetherRule::ERROR_MESSAGE,
28+
7,
29+
]]];
30+
31+
yield [__DIR__ . '/Fixture/SkipSoleMethod.php', []];
32+
yield [__DIR__ . '/Fixture/SkipOtherSoleMethod.php', []];
33+
34+
yield [__DIR__ . '/Fixture/SkipCircularDependencyPrevention.php', []];
35+
}
36+
37+
protected function getRule(): Rule
38+
{
39+
return new NoConstructorAndRequiredTogetherRule();
40+
}
41+
}

0 commit comments

Comments
 (0)