Skip to content

Commit eca4c30

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

File tree

5 files changed

+230
-0
lines changed

5 files changed

+230
-0
lines changed
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\DBAL\Types\Type;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use PHPStan\Type\Doctrine\DescriptorRegistry;
9+
use PHPStan\Type\Doctrine\Descriptors\ArrayType;
10+
use PHPStan\Type\Doctrine\Descriptors\BigIntType;
11+
use PHPStan\Type\Doctrine\Descriptors\BinaryType;
12+
use PHPStan\Type\Doctrine\Descriptors\DateTimeImmutableType;
13+
use PHPStan\Type\Doctrine\Descriptors\DateTimeType;
14+
use PHPStan\Type\Doctrine\Descriptors\DateType;
15+
use PHPStan\Type\Doctrine\Descriptors\IntegerType;
16+
use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor;
17+
use PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor;
18+
use PHPStan\Type\Doctrine\Descriptors\StringType;
19+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
20+
use Ramsey\Uuid\Doctrine\UuidType;
21+
22+
/**
23+
* @extends RuleTestCase<EntityColumnRule>
24+
*/
25+
class EntityEmbeddableRuleTest extends RuleTestCase
26+
{
27+
28+
protected function getRule(): Rule
29+
{
30+
if (!Type::hasType(CustomType::NAME)) {
31+
Type::addType(CustomType::NAME, CustomType::class);
32+
}
33+
if (!Type::hasType(UuidType::NAME)) {
34+
Type::addType(UuidType::NAME, UuidType::class);
35+
}
36+
37+
return new EntityEmbeddableRule(
38+
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null),
39+
new DescriptorRegistry([
40+
new BigIntType(),
41+
new StringType(),
42+
new DateTimeType(),
43+
new DateTimeImmutableType(),
44+
new BinaryType(),
45+
new IntegerType(),
46+
new ReflectionDescriptor(CustomType::class, $this->createBroker()),
47+
new DateType(),
48+
new UuidTypeDescriptor(UuidType::class),
49+
new ArrayType(),
50+
]),
51+
true
52+
);
53+
}
54+
55+
public function testEmbedded(): void
56+
{
57+
$this->analyse([__DIR__ . '/data/EntityWithEmbeddable.php'], []);
58+
}
59+
60+
public function testEmbeddedWithWrongTypeHint(): void
61+
{
62+
$this->analyse([__DIR__ . '/data/EntityWithBrokenEmbeddable.php'], [
63+
[
64+
'Property PHPStan\Rules\Doctrine\ORM\EntityWithBrokenEmbeddable::$embedded type mapping mismatch: mapping specifies PHPStan\Rules\Doctrine\ORM\Embeddable but property expects int.',
65+
24,
66+
],
67+
]);
68+
}
69+
70+
}
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+
}
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)