diff --git a/rules.neon b/rules.neon index 66d47315..7e1e6bc8 100644 --- a/rules.neon +++ b/rules.neon @@ -19,6 +19,8 @@ rules: - PHPStan\Rules\Doctrine\ORM\DqlRule - PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule - PHPStan\Rules\Doctrine\ORM\EntityRelationRule + - PHPStan\Rules\Doctrine\ORM\EntityMustNotBeFinalRule + - PHPStan\Rules\Doctrine\ORM\EntityMustNotContainFinalMethodsRule services: - diff --git a/src/Rules/Doctrine/ORM/EntityMustNotBeFinalRule.php b/src/Rules/Doctrine/ORM/EntityMustNotBeFinalRule.php new file mode 100644 index 00000000..484a39e6 --- /dev/null +++ b/src/Rules/Doctrine/ORM/EntityMustNotBeFinalRule.php @@ -0,0 +1,58 @@ +objectMetadataResolver = $objectMetadataResolver; + } + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + /** + * @param \PhpParser\Node\Stmt\Class_ $node + * @param \PHPStan\Analyser\Scope $scope + * @return string[] + */ + public function processNode(Node $node, Scope $scope): array + { + $objectManager = $this->objectMetadataResolver->getObjectManager(); + if ($objectManager === null) { + return []; + } + + $className = (string) $node->namespacedName; + + // not an entity + if ($objectManager->getMetadataFactory()->isTransient($className)) { + return []; + } + + if ($node->isFinal()) { + return [ + sprintf( + 'Class %s is a Doctrine ORM entity, so it should not be marked as final', + $className + ), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Doctrine/ORM/EntityMustNotContainFinalMethodsRule.php b/src/Rules/Doctrine/ORM/EntityMustNotContainFinalMethodsRule.php new file mode 100644 index 00000000..8df9bb8c --- /dev/null +++ b/src/Rules/Doctrine/ORM/EntityMustNotContainFinalMethodsRule.php @@ -0,0 +1,62 @@ +objectMetadataResolver = $objectMetadataResolver; + } + + public function getNodeType(): string + { + return Node\Stmt\ClassMethod::class; + } + + /** + * @param \PhpParser\Node\Stmt\ClassMethod $node + * @param \PHPStan\Analyser\Scope $scope + * @return string[] + */ + 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(); + if ($objectManager->getMetadataFactory()->isTransient($className)) { + return []; + } + + if ($node->isFinal()) { + return [ + sprintf( + 'Class %s is a Doctrine ORM entity, so its method %s() must not be marked as final.', + $className, + (string) $node->name + ), + ]; + } + + return []; + } + +} diff --git a/tests/Rules/Doctrine/ORM/EntityMustNotBeFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityMustNotBeFinalRuleTest.php new file mode 100644 index 00000000..5ce9804d --- /dev/null +++ b/tests/Rules/Doctrine/ORM/EntityMustNotBeFinalRuleTest.php @@ -0,0 +1,45 @@ +analyse([$file], $expectedErrors); + } + + public function ruleProvider(): Iterator + { + yield 'regular class' => [__DIR__ . '/data/FinalClassNotAnEntity.php', []]; + + yield 'nice entity' => [__DIR__ . '/data/EntityWithRelations.php', []]; + + yield 'final entity class' => [ + __DIR__ . '/data/FinalEntity.php', + [ + [ + 'Class PHPStan\Rules\Doctrine\ORM\FinalEntity is a Doctrine ORM entity, so it should not be marked as final', + 10, + ], + ], + ]; + } + +} diff --git a/tests/Rules/Doctrine/ORM/EntityMustNotContainFinalMethodsRuleTest.php b/tests/Rules/Doctrine/ORM/EntityMustNotContainFinalMethodsRuleTest.php new file mode 100644 index 00000000..26362bbd --- /dev/null +++ b/tests/Rules/Doctrine/ORM/EntityMustNotContainFinalMethodsRuleTest.php @@ -0,0 +1,45 @@ +analyse([$file], $expectedErrors); + } + + public function ruleProvider(): Iterator + { + yield 'regular class' => [__DIR__ . '/data/FinalClassNotAnEntity.php', []]; + + yield 'nice entity' => [__DIR__ . '/data/EntityWithRelations.php', []]; + + yield 'entity with final method' => [ + __DIR__ . '/data/EntityWithFinalMethod.php', + [ + [ + 'Class PHPStan\Rules\Doctrine\ORM\EntityWithFinalMethod is a Doctrine ORM entity, so its method getData() must not be marked as final.', + 31, + ], + ], + ]; + } + +} diff --git a/tests/Rules/Doctrine/ORM/data/EntityWithFinalMethod.php b/tests/Rules/Doctrine/ORM/data/EntityWithFinalMethod.php new file mode 100644 index 00000000..487c0ca4 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/EntityWithFinalMethod.php @@ -0,0 +1,36 @@ +