diff --git a/rules.neon b/rules.neon index 3c9e3444..8f88fae9 100644 --- a/rules.neon +++ b/rules.neon @@ -22,6 +22,10 @@ rules: - PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule - PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule +conditionalTags: + PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + services: - class: PHPStan\Rules\Doctrine\ORM\QueryBuilderDqlRule @@ -36,6 +40,8 @@ services: allowNullablePropertyForRequiredField: %doctrine.allowNullablePropertyForRequiredField% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule - class: PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule - diff --git a/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php b/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php new file mode 100644 index 00000000..86c75bee --- /dev/null +++ b/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php @@ -0,0 +1,62 @@ + + */ +class EntityMappingExceptionRule implements Rule +{ + + /** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */ + private $objectMetadataResolver; + + public function __construct( + ObjectMetadataResolver $objectMetadataResolver + ) + { + $this->objectMetadataResolver = $objectMetadataResolver; + } + + public function getNodeType(): string + { + return InClassNode::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 { + if ($objectManager->getMetadataFactory()->isTransient($className)) { + return []; + } + } catch (\ReflectionException $e) { + return []; + } + + try { + $objectManager->getClassMetadata($className); + } catch (\Doctrine\ORM\Mapping\MappingException $e) { + return [$e->getMessage()]; + } + + return []; + } + +} diff --git a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php new file mode 100644 index 00000000..02bf4163 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php @@ -0,0 +1,37 @@ + + */ +class EntityMappingExceptionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EntityMappingExceptionRule( + new ObjectMetadataResolver($this->createReflectionProvider(), __DIR__ . '/entity-manager.php', null) + ); + } + + public function testValidEntity(): void + { + $this->analyse([__DIR__ . '/data-attributes/enum-type.php'], []); + } + + public function testInvalidEntity(): void + { + $this->analyse([__DIR__ . '/data-attributes/enum-type-without-pk.php'], [ + [ + 'No identifier/primary key specified for Entity "PHPStan\Rules\Doctrine\ORMAttributes\FooWithoutPK". Every Entity must have an identifier/primary key.', + 7, + ], + ]); + } + +} diff --git a/tests/Rules/Doctrine/ORM/data-attributes/enum-type-without-pk.php b/tests/Rules/Doctrine/ORM/data-attributes/enum-type-without-pk.php new file mode 100644 index 00000000..9b9905b9 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data-attributes/enum-type-without-pk.php @@ -0,0 +1,14 @@ += 8.1 + +namespace PHPStan\Rules\Doctrine\ORMAttributes; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class FooWithoutPK +{ + + #[ORM\Column(type: "string", enumType: FooEnum::class)] + public FooEnum $type; + +}