Skip to content

Commit 7597152

Browse files
committed
Check if embedded class matches property's type
1 parent 5762a21 commit 7597152

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

Diff for: src/Rules/Doctrine/ORM/EntityEmbeddableRule.php

+87
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 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\ObjectType;
11+
use PHPStan\Type\TypeCombinator;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function sprintf;
14+
15+
/**
16+
* @implements Rule<Node\Stmt\PropertyProperty>
17+
*/
18+
class EntityEmbeddableRule implements Rule
19+
{
20+
21+
/** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */
22+
private $objectMetadataResolver;
23+
24+
public function __construct(ObjectMetadataResolver $objectMetadataResolver)
25+
{
26+
$this->objectMetadataResolver = $objectMetadataResolver;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return Node\Stmt\PropertyProperty::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
$class = $scope->getClassReflection();
37+
if ($class === null) {
38+
return [];
39+
}
40+
41+
$objectManager = $this->objectMetadataResolver->getObjectManager();
42+
if ($objectManager === null) {
43+
return [];
44+
}
45+
46+
$className = $class->getName();
47+
48+
try {
49+
$metadata = $objectManager->getClassMetadata($className);
50+
} catch (\Doctrine\ORM\Mapping\MappingException $e) {
51+
return [];
52+
}
53+
54+
$classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo';
55+
if (!$metadata instanceof $classMetadataInfo) {
56+
return [];
57+
}
58+
59+
$propertyName = (string) $node->name;
60+
try {
61+
$property = $class->getNativeProperty($propertyName);
62+
} catch (MissingPropertyFromReflectionException $e) {
63+
return [];
64+
}
65+
66+
if (!isset($metadata->embeddedClasses[$propertyName])) {
67+
return [];
68+
}
69+
70+
$errors = [];
71+
$embeddedClass = $metadata->embeddedClasses[$propertyName];
72+
$propertyWritableType = $property->getWritableType();
73+
$accordingToMapping = new ObjectType($embeddedClass['class']);
74+
if (!TypeCombinator::removeNull($propertyWritableType)->equals($accordingToMapping)) {
75+
$errors[] = sprintf(
76+
'Property %s::$%s type mapping mismatch: mapping specifies %s but property expects %s.',
77+
$class->getName(),
78+
$propertyName,
79+
$accordingToMapping->describe(VerbosityLevel::typeOnly()),
80+
$propertyWritableType->describe(VerbosityLevel::typeOnly())
81+
);
82+
}
83+
84+
return $errors;
85+
}
86+
87+
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
/**
10+
* @extends RuleTestCase<EntityEmbeddableRule>
11+
*/
12+
class EntityEmbeddableRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new EntityEmbeddableRule(
18+
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null)
19+
);
20+
}
21+
22+
public function testEmbedded(): void
23+
{
24+
$this->analyse([__DIR__ . '/data/EntityWithEmbeddable.php'], []);
25+
}
26+
27+
public function testEmbeddedWithWrongTypeHint(): void
28+
{
29+
$this->analyse([__DIR__ . '/data/EntityWithBrokenEmbeddable.php'], [
30+
[
31+
'Property PHPStan\Rules\Doctrine\ORM\EntityWithBrokenEmbeddable::$embedded type mapping mismatch: mapping specifies PHPStan\Rules\Doctrine\ORM\Embeddable but property expects int.',
32+
24,
33+
],
34+
]);
35+
}
36+
37+
}

Diff for: tests/Rules/Doctrine/ORM/data/Embeddable.php

+17
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\Embeddable()
9+
*/
10+
class Embeddable
11+
{
12+
/**
13+
* @ORM\Column(type="string")
14+
* @var string
15+
*/
16+
private $one;
17+
}
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 EntityWithBrokenEmbeddable
11+
{
12+
13+
/**
14+
* @ORM\Id()
15+
* @ORM\Column(type="integer")
16+
* @var int
17+
*/
18+
private $id;
19+
20+
/**
21+
* @ORM\Embedded(class=Embeddable::class)
22+
* @var int
23+
*/
24+
private $embedded;
25+
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 EntityWithEmbeddable
11+
{
12+
13+
/**
14+
* @ORM\Id()
15+
* @ORM\Column(type="integer")
16+
* @var int
17+
*/
18+
private $id;
19+
20+
/**
21+
* @ORM\Embedded(class=Embeddable::class)
22+
* @var Embeddable
23+
*/
24+
private $embedded;
25+
26+
/**
27+
* @ORM\Embedded(class=Embeddable::class)
28+
* @var ?Embeddable
29+
*/
30+
private $nullable;
31+
}

0 commit comments

Comments
 (0)