diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index d13a2c7e..c52d2870 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -64,9 +64,6 @@ public function processNode(Node $node, Scope $scope): array } $className = $class->getName(); - if ($objectManager->getMetadataFactory()->isTransient($className)) { - return []; - } try { $metadata = $objectManager->getClassMetadata($className); diff --git a/src/Rules/Doctrine/ORM/EntityEmbeddableRule.php b/src/Rules/Doctrine/ORM/EntityEmbeddableRule.php new file mode 100644 index 00000000..0a5d61bf --- /dev/null +++ b/src/Rules/Doctrine/ORM/EntityEmbeddableRule.php @@ -0,0 +1,87 @@ + + */ +class EntityEmbeddableRule implements Rule +{ + + /** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */ + private $objectMetadataResolver; + + public function __construct(ObjectMetadataResolver $objectMetadataResolver) + { + $this->objectMetadataResolver = $objectMetadataResolver; + } + + public function getNodeType(): string + { + return Node\Stmt\PropertyProperty::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $class = $scope->getClassReflection(); + if ($class === null) { + return []; + } + + $objectManager = $this->objectMetadataResolver->getObjectManager(); + if ($objectManager === null) { + return []; + } + + $className = $class->getName(); + + try { + $metadata = $objectManager->getClassMetadata($className); + } catch (\Doctrine\ORM\Mapping\MappingException $e) { + return []; + } + + $classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo'; + if (!$metadata instanceof $classMetadataInfo) { + return []; + } + + $propertyName = (string) $node->name; + try { + $property = $class->getNativeProperty($propertyName); + } catch (MissingPropertyFromReflectionException $e) { + return []; + } + + if (!isset($metadata->embeddedClasses[$propertyName])) { + return []; + } + + $errors = []; + $embeddedClass = $metadata->embeddedClasses[$propertyName]; + $propertyWritableType = $property->getWritableType(); + $accordingToMapping = new ObjectType($embeddedClass['class']); + if (!TypeCombinator::removeNull($propertyWritableType)->equals($accordingToMapping)) { + $errors[] = sprintf( + 'Property %s::$%s type mapping mismatch: mapping specifies %s but property expects %s.', + $class->getName(), + $propertyName, + $accordingToMapping->describe(VerbosityLevel::typeOnly()), + $propertyWritableType->describe(VerbosityLevel::typeOnly()) + ); + } + + return $errors; + } + +} diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index f48bd4c7..29cb817b 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -153,6 +153,21 @@ public function testCustomType(): void ]); } + public function testEmbeddable(): void + { + $this->analyse([__DIR__ . '/data/Embeddable.php'], []); + } + + public function testBrokenEmbeddable(): void + { + $this->analyse([__DIR__ . '/data/BrokenEmbeddable.php'], [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\BrokenEmbeddable::$one type mapping mismatch: database can contain string|null but property expects string.', + 16, + ], + ]); + } + public function testUnknownType(): void { $this->analyse([__DIR__ . '/data/EntityWithUnknownType.php'], [ diff --git a/tests/Rules/Doctrine/ORM/EntityEmbeddableRuleTest.php b/tests/Rules/Doctrine/ORM/EntityEmbeddableRuleTest.php new file mode 100644 index 00000000..3540820b --- /dev/null +++ b/tests/Rules/Doctrine/ORM/EntityEmbeddableRuleTest.php @@ -0,0 +1,37 @@ + + */ +class EntityEmbeddableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EntityEmbeddableRule( + new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null) + ); + } + + public function testEmbedded(): void + { + $this->analyse([__DIR__ . '/data/EntityWithEmbeddable.php'], []); + } + + public function testEmbeddedWithWrongTypeHint(): void + { + $this->analyse([__DIR__ . '/data/EntityWithBrokenEmbeddable.php'], [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\EntityWithBrokenEmbeddable::$embedded type mapping mismatch: mapping specifies PHPStan\Rules\Doctrine\ORM\Embeddable but property expects int.', + 24, + ], + ]); + } + +} diff --git a/tests/Rules/Doctrine/ORM/data/BrokenEmbeddable.php b/tests/Rules/Doctrine/ORM/data/BrokenEmbeddable.php new file mode 100644 index 00000000..6fbb853f --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/BrokenEmbeddable.php @@ -0,0 +1,17 @@ +