Skip to content

Commit 3999dd2

Browse files
lookymanondrejmirtes
authored andcommitted
Implemented entity relation checking rule
1 parent 1f21639 commit 3999dd2

File tree

5 files changed

+194
-0
lines changed

5 files changed

+194
-0
lines changed

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ rules:
1818
- PHPStan\Rules\Doctrine\ORM\MagicRepositoryMethodCallRule
1919
- PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule
2020
- PHPStan\Rules\Doctrine\ORM\EntityColumnRule
21+
- PHPStan\Rules\Doctrine\ORM\EntityRelationRule
2122

2223
services:
2324
-
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MissingPropertyFromReflectionException;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
10+
use PHPStan\Type\IterableType;
11+
use PHPStan\Type\MixedType;
12+
use PHPStan\Type\ObjectType;
13+
use PHPStan\Type\TypeCombinator;
14+
use PHPStan\Type\VerbosityLevel;
15+
use function sprintf;
16+
17+
class EntityRelationRule implements Rule
18+
{
19+
20+
/** @var \PHPStan\Type\Doctrine\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 Node\Stmt\PropertyProperty::class;
31+
}
32+
33+
/**
34+
* @param \PhpParser\Node\Stmt\PropertyProperty $node
35+
* @param \PHPStan\Analyser\Scope $scope
36+
* @return string[]
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
$class = $scope->getClassReflection();
41+
if ($class === null) {
42+
return [];
43+
}
44+
45+
$objectManager = $this->objectMetadataResolver->getObjectManager();
46+
if ($objectManager === null) {
47+
return [];
48+
}
49+
50+
$className = $class->getName();
51+
if ($objectManager->getMetadataFactory()->isTransient($className)) {
52+
return [];
53+
}
54+
55+
/** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata */
56+
$metadata = $objectManager->getClassMetadata($className);
57+
$classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo';
58+
if (!$metadata instanceof $classMetadataInfo) {
59+
return [];
60+
}
61+
62+
$propertyName = (string) $node->name;
63+
try {
64+
$property = $class->getNativeProperty($propertyName);
65+
} catch (MissingPropertyFromReflectionException $e) {
66+
return [];
67+
}
68+
69+
if (!isset($metadata->associationMappings[$propertyName])) {
70+
return [];
71+
}
72+
$associationMapping = $metadata->associationMappings[$propertyName];
73+
74+
$columnType = null;
75+
if ((bool) ($associationMapping['type'] & 3)) { // ClassMetadataInfo::TO_ONE
76+
$columnType = new ObjectType($associationMapping['targetEntity']);
77+
if ($associationMapping['joinColumns'][0]['nullable'] ?? true) {
78+
$columnType = TypeCombinator::addNull($columnType);
79+
}
80+
} elseif ((bool) ($associationMapping['type'] & 12)) { // ClassMetadataInfo::TO_MANY
81+
$columnType = TypeCombinator::intersect(
82+
new ObjectType('Doctrine\Common\Collections\Collection'),
83+
new IterableType(new MixedType(), new ObjectType($associationMapping['targetEntity']))
84+
);
85+
}
86+
87+
$errors = [];
88+
if ($columnType !== null) {
89+
if (!$property->getWritableType()->isSuperTypeOf($columnType)->yes()) {
90+
$errors[] = sprintf('Database can contain %s but property expects %s.', $columnType->describe(VerbosityLevel::typeOnly()), $property->getWritableType()->describe(VerbosityLevel::typeOnly()));
91+
}
92+
if (!$columnType->isSuperTypeOf($property->getReadableType())->yes()) {
93+
$errors[] = sprintf('Property can contain %s but database expects %s.', $property->getReadableType()->describe(VerbosityLevel::typeOnly()), $columnType->describe(VerbosityLevel::typeOnly()));
94+
}
95+
}
96+
97+
return $errors;
98+
}
99+
100+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
8+
9+
class EntityRelationRuleTest extends RuleTestCase
10+
{
11+
12+
protected function getRule(): Rule
13+
{
14+
return new EntityRelationRule(
15+
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null)
16+
);
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/EntityWithRelations.php'], []);
22+
}
23+
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 AnotherEntity
11+
{
12+
13+
/**
14+
* @ORM\Id()
15+
* @ORM\Column(type="int")
16+
* @var int
17+
*/
18+
private $id;
19+
20+
/**
21+
* @ORM\ManyToOne(targetEntity="PHPStan\Rules\Doctrine\ORM\EntityWithRelations")
22+
*/
23+
private $manyToOne;
24+
25+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 EntityWithRelations
11+
{
12+
13+
/**
14+
* @ORM\Id()
15+
* @ORM\Column(type="int")
16+
* @var int
17+
*/
18+
private $id;
19+
20+
/**
21+
* @ORM\OneToOne(targetEntity="PHPStan\Rules\Doctrine\ORM\AnotherEntity")
22+
* @var \PHPStan\Rules\Doctrine\ORM\AnotherEntity|null
23+
*/
24+
private $oneToOne;
25+
26+
/**
27+
* @ORM\ManyToOne(targetEntity="PHPStan\Rules\Doctrine\ORM\AnotherEntity")
28+
* @var \PHPStan\Rules\Doctrine\ORM\AnotherEntity|null
29+
*/
30+
private $manyToOne;
31+
32+
/**
33+
* @ORM\OneToMany(targetEntity="PHPStan\Rules\Doctrine\ORM\AnotherEntity", mappedBy="manyToOne")
34+
* @var \Doctrine\Common\Collections\Collection&iterable<\PHPStan\Rules\Doctrine\ORM\AnotherEntity>
35+
*/
36+
private $oneToMany;
37+
38+
/**
39+
* @ORM\ManyToMany(targetEntity="PHPStan\Rules\Doctrine\ORM\AnotherEntity")
40+
* @var \Doctrine\Common\Collections\Collection&iterable<\PHPStan\Rules\Doctrine\ORM\AnotherEntity>
41+
*/
42+
private $manyToMany;
43+
44+
}

0 commit comments

Comments
 (0)