Skip to content

Commit 6c2421a

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

File tree

5 files changed

+244
-0
lines changed

5 files changed

+244
-0
lines changed

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

+100
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\DescriptorRegistry;
10+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\TypeCombinator;
13+
use PHPStan\Type\VerbosityLevel;
14+
use function sprintf;
15+
16+
/**
17+
* @implements Rule<Node\Stmt\PropertyProperty>
18+
*/
19+
class EntityEmbeddableRule implements Rule
20+
{
21+
22+
/** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */
23+
private $objectMetadataResolver;
24+
25+
/** @var \PHPStan\Type\Doctrine\DescriptorRegistry */
26+
private $descriptorRegistry;
27+
28+
/** @var bool */
29+
private $reportUnknownTypes;
30+
31+
public function __construct(
32+
ObjectMetadataResolver $objectMetadataResolver,
33+
DescriptorRegistry $descriptorRegistry,
34+
bool $reportUnknownTypes
35+
)
36+
{
37+
$this->objectMetadataResolver = $objectMetadataResolver;
38+
$this->descriptorRegistry = $descriptorRegistry;
39+
$this->reportUnknownTypes = $reportUnknownTypes;
40+
}
41+
42+
public function getNodeType(): string
43+
{
44+
return Node\Stmt\PropertyProperty::class;
45+
}
46+
47+
public function processNode(Node $node, Scope $scope): array
48+
{
49+
$class = $scope->getClassReflection();
50+
if ($class === null) {
51+
return [];
52+
}
53+
54+
$objectManager = $this->objectMetadataResolver->getObjectManager();
55+
if ($objectManager === null) {
56+
return [];
57+
}
58+
59+
$className = $class->getName();
60+
61+
try {
62+
$metadata = $objectManager->getClassMetadata($className);
63+
} catch (\Doctrine\ORM\Mapping\MappingException $e) {
64+
return [];
65+
}
66+
67+
$classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo';
68+
if (!$metadata instanceof $classMetadataInfo) {
69+
return [];
70+
}
71+
72+
$propertyName = (string) $node->name;
73+
try {
74+
$property = $class->getNativeProperty($propertyName);
75+
} catch (MissingPropertyFromReflectionException $e) {
76+
return [];
77+
}
78+
79+
if (!isset($metadata->embeddedClasses[$propertyName])) {
80+
return [];
81+
}
82+
83+
$errors = [];
84+
$embeddedClass = $metadata->embeddedClasses[$propertyName];
85+
$propertyWritableType = $property->getWritableType();
86+
$accordingToMapping = new ObjectType($embeddedClass['class']);
87+
if (!TypeCombinator::removeNull($propertyWritableType)->equals($accordingToMapping)) {
88+
$errors[] = sprintf(
89+
'Property %s::$%s type mapping mismatch: mapping specifies %s but property expects %s.',
90+
$class->getName(),
91+
$propertyName,
92+
$accordingToMapping->describe(VerbosityLevel::typeOnly()),
93+
$propertyWritableType->describe(VerbosityLevel::typeOnly())
94+
);
95+
}
96+
97+
return $errors;
98+
}
99+
100+
}
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\DBAL\Types\Type;
6+
use Iterator;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
use PHPStan\Type\Doctrine\DescriptorRegistry;
10+
use PHPStan\Type\Doctrine\Descriptors\ArrayType;
11+
use PHPStan\Type\Doctrine\Descriptors\BigIntType;
12+
use PHPStan\Type\Doctrine\Descriptors\BinaryType;
13+
use PHPStan\Type\Doctrine\Descriptors\DateTimeImmutableType;
14+
use PHPStan\Type\Doctrine\Descriptors\DateTimeType;
15+
use PHPStan\Type\Doctrine\Descriptors\DateType;
16+
use PHPStan\Type\Doctrine\Descriptors\IntegerType;
17+
use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor;
18+
use PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor;
19+
use PHPStan\Type\Doctrine\Descriptors\StringType;
20+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
21+
use Ramsey\Uuid\Doctrine\UuidType;
22+
23+
/**
24+
* @extends RuleTestCase<EntityColumnRule>
25+
*/
26+
class EntityEmbeddableRuleTest extends RuleTestCase
27+
{
28+
29+
protected function getRule(): Rule
30+
{
31+
if (!Type::hasType(CustomType::NAME)) {
32+
Type::addType(CustomType::NAME, CustomType::class);
33+
}
34+
if (!Type::hasType(UuidType::NAME)) {
35+
Type::addType(UuidType::NAME, UuidType::class);
36+
}
37+
38+
return new EntityEmbeddableRule(
39+
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null),
40+
new DescriptorRegistry([
41+
new BigIntType(),
42+
new StringType(),
43+
new DateTimeType(),
44+
new DateTimeImmutableType(),
45+
new BinaryType(),
46+
new IntegerType(),
47+
new ReflectionDescriptor(CustomType::class, $this->createBroker()),
48+
new DateType(),
49+
new UuidTypeDescriptor(UuidType::class),
50+
new ArrayType(),
51+
]),
52+
true
53+
);
54+
}
55+
56+
public function testEmbedded(): void
57+
{
58+
$this->analyse([__DIR__ . '/data/EntityWithEmbeddable.php'], []);
59+
}
60+
61+
public function testEmbeddedWithWrongTypeHint(): void
62+
{
63+
$this->analyse([__DIR__ . '/data/EntityWithBrokenEmbeddable.php'], [
64+
[
65+
'Property PHPStan\Rules\Doctrine\ORM\EntityWithBrokenEmbeddable::$embedded type mapping mismatch: mapping specifies PHPStan\Rules\Doctrine\ORM\Embeddable but property expects int.',
66+
24,
67+
],
68+
]);
69+
}
70+
71+
}

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)