Skip to content

Commit a1ba454

Browse files
authored
Add rule to prevent final constructors in Doctrine entities
1 parent c1c32c9 commit a1ba454

6 files changed

+194
-0
lines changed

rules.neon

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ rules:
2727
conditionalTags:
2828
PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule:
2929
phpstan.rules.rule: %featureToggles.bleedingEdge%
30+
PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule:
31+
phpstan.rules.rule: %featureToggles.bleedingEdge%
3032

3133
services:
3234
-
@@ -54,3 +56,5 @@ services:
5456
bleedingEdge: %featureToggles.bleedingEdge%
5557
tags:
5658
- phpstan.rules.rule
59+
-
60+
class: PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Stmt\ClassMethod;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
12+
use function sprintf;
13+
14+
/**
15+
* @implements Rule<ClassMethod>
16+
*/
17+
class EntityConstructorNotFinalRule implements Rule
18+
{
19+
20+
/** @var ObjectMetadataResolver */
21+
private $objectMetadataResolver;
22+
23+
public function __construct(ObjectMetadataResolver $objectMetadataResolver)
24+
{
25+
$this->objectMetadataResolver = $objectMetadataResolver;
26+
}
27+
28+
public function getNodeType(): string
29+
{
30+
return ClassMethod::class;
31+
}
32+
33+
public function processNode(Node $node, Scope $scope): array
34+
{
35+
if ($node->name->name !== '__construct') {
36+
return [];
37+
}
38+
39+
if (!$node->isFinal()) {
40+
return [];
41+
}
42+
43+
$classReflection = $scope->getClassReflection();
44+
if ($classReflection === null) {
45+
throw new ShouldNotHappenException();
46+
}
47+
48+
if ($this->objectMetadataResolver->isTransient($classReflection->getName())) {
49+
return [];
50+
}
51+
52+
$metadata = $this->objectMetadataResolver->getClassMetadata($classReflection->getName());
53+
if ($metadata !== null && $metadata->isEmbeddedClass === true) {
54+
return [];
55+
}
56+
57+
return [RuleErrorBuilder::message(sprintf(
58+
'Constructor of class %s is final which can cause problems with proxies.',
59+
$classReflection->getDisplayName()
60+
))->build()];
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Iterator;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
9+
10+
/**
11+
* @extends RuleTestCase<EntityConstructorNotFinalRule>
12+
*/
13+
class EntityConstructorNotFinalRuleTest extends RuleTestCase
14+
{
15+
16+
/** @var string|null */
17+
private $objectManagerLoader;
18+
19+
protected function getRule(): Rule
20+
{
21+
return new EntityConstructorNotFinalRule(
22+
new ObjectMetadataResolver($this->objectManagerLoader)
23+
);
24+
}
25+
26+
/**
27+
* @dataProvider ruleProvider
28+
* @param list<array{0: string, 1: int, 2?: string}> $expectedErrors
29+
*/
30+
public function testRule(string $file, array $expectedErrors): void
31+
{
32+
$this->objectManagerLoader = __DIR__ . '/entity-manager.php';
33+
$this->analyse([$file], $expectedErrors);
34+
}
35+
36+
/**
37+
* @dataProvider ruleProvider
38+
* @param list<array{0: string, 1: int, 2?: string}> $expectedErrors
39+
*/
40+
public function testRuleWithoutObjectManagerLoader(string $file, array $expectedErrors): void
41+
{
42+
$this->objectManagerLoader = null;
43+
$this->analyse([$file], $expectedErrors);
44+
}
45+
46+
/**
47+
* @return Iterator<mixed[]>
48+
*/
49+
public function ruleProvider(): Iterator
50+
{
51+
yield 'entity final constructor' => [
52+
__DIR__ . '/data/EntityFinalConstructor.php',
53+
[
54+
[
55+
'Constructor of class PHPStan\Rules\Doctrine\ORM\EntityFinalConstructor is final which can cause problems with proxies.',
56+
12,
57+
],
58+
],
59+
];
60+
61+
yield 'entity non-final constructor' => [
62+
__DIR__ . '/data/EntityNonFinalConstructor.php',
63+
[],
64+
];
65+
66+
yield 'correct entity' => [
67+
__DIR__ . '/data/MyEntity.php',
68+
[],
69+
];
70+
71+
yield 'non-entity final constructor' => [
72+
__DIR__ . '/data/NonEntityFinalConstructor.php',
73+
[],
74+
];
75+
76+
yield 'final embeddable' => [
77+
__DIR__ . '/data/FinalEmbeddable.php',
78+
[],
79+
];
80+
81+
yield 'non final embeddable' => [
82+
__DIR__ . '/data/MyEmbeddable.php',
83+
[],
84+
];
85+
}
86+
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
/**
8+
* @ORM\Entity()
9+
*/
10+
class EntityFinalConstructor
11+
{
12+
final public function __construct(string $x)
13+
{}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
/**
8+
* @ORM\Entity()
9+
*/
10+
class EntityNonFinalConstructor
11+
{
12+
public function __construct()
13+
{}
14+
15+
final public function foo()
16+
{}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
class NonEntityFinalConstructor
6+
{
7+
final public function __construct(string $x)
8+
{}
9+
}

0 commit comments

Comments
 (0)