Skip to content

Commit

Permalink
Add rule to prevent final constructors in Doctrine entities
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoVie authored Apr 5, 2023
1 parent c1c32c9 commit a1ba454
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
4 changes: 4 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ rules:
conditionalTags:
PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%
PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule:
phpstan.rules.rule: %featureToggles.bleedingEdge%

services:
-
Expand Down Expand Up @@ -54,3 +56,5 @@ services:
bleedingEdge: %featureToggles.bleedingEdge%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule
63 changes: 63 additions & 0 deletions src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use function sprintf;

/**
* @implements Rule<ClassMethod>
*/
class EntityConstructorNotFinalRule implements Rule
{

/** @var ObjectMetadataResolver */
private $objectMetadataResolver;

public function __construct(ObjectMetadataResolver $objectMetadataResolver)
{
$this->objectMetadataResolver = $objectMetadataResolver;
}

public function getNodeType(): string
{
return ClassMethod::class;
}

public function processNode(Node $node, Scope $scope): array
{
if ($node->name->name !== '__construct') {
return [];
}

if (!$node->isFinal()) {
return [];
}

$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
throw new ShouldNotHappenException();
}

if ($this->objectMetadataResolver->isTransient($classReflection->getName())) {
return [];
}

$metadata = $this->objectMetadataResolver->getClassMetadata($classReflection->getName());
if ($metadata !== null && $metadata->isEmbeddedClass === true) {
return [];
}

return [RuleErrorBuilder::message(sprintf(
'Constructor of class %s is final which can cause problems with proxies.',
$classReflection->getDisplayName()
))->build()];
}

}
87 changes: 87 additions & 0 deletions tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Iterator;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;

/**
* @extends RuleTestCase<EntityConstructorNotFinalRule>
*/
class EntityConstructorNotFinalRuleTest extends RuleTestCase
{

/** @var string|null */
private $objectManagerLoader;

protected function getRule(): Rule
{
return new EntityConstructorNotFinalRule(
new ObjectMetadataResolver($this->objectManagerLoader)
);
}

/**
* @dataProvider ruleProvider
* @param list<array{0: string, 1: int, 2?: string}> $expectedErrors
*/
public function testRule(string $file, array $expectedErrors): void
{
$this->objectManagerLoader = __DIR__ . '/entity-manager.php';
$this->analyse([$file], $expectedErrors);
}

/**
* @dataProvider ruleProvider
* @param list<array{0: string, 1: int, 2?: string}> $expectedErrors
*/
public function testRuleWithoutObjectManagerLoader(string $file, array $expectedErrors): void
{
$this->objectManagerLoader = null;
$this->analyse([$file], $expectedErrors);
}

/**
* @return Iterator<mixed[]>
*/
public function ruleProvider(): Iterator
{
yield 'entity final constructor' => [
__DIR__ . '/data/EntityFinalConstructor.php',
[
[
'Constructor of class PHPStan\Rules\Doctrine\ORM\EntityFinalConstructor is final which can cause problems with proxies.',
12,
],
],
];

yield 'entity non-final constructor' => [
__DIR__ . '/data/EntityNonFinalConstructor.php',
[],
];

yield 'correct entity' => [
__DIR__ . '/data/MyEntity.php',
[],
];

yield 'non-entity final constructor' => [
__DIR__ . '/data/NonEntityFinalConstructor.php',
[],
];

yield 'final embeddable' => [
__DIR__ . '/data/FinalEmbeddable.php',
[],
];

yield 'non final embeddable' => [
__DIR__ . '/data/MyEmbeddable.php',
[],
];
}

}
14 changes: 14 additions & 0 deletions tests/Rules/Doctrine/ORM/data/EntityFinalConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
*/
class EntityFinalConstructor
{
final public function __construct(string $x)
{}
}
17 changes: 17 additions & 0 deletions tests/Rules/Doctrine/ORM/data/EntityNonFinalConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
*/
class EntityNonFinalConstructor
{
public function __construct()
{}

final public function foo()
{}
}
9 changes: 9 additions & 0 deletions tests/Rules/Doctrine/ORM/data/NonEntityFinalConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM;

class NonEntityFinalConstructor
{
final public function __construct(string $x)
{}
}

0 comments on commit a1ba454

Please sign in to comment.