Skip to content

Commit 734ef36

Browse files
authored
Skip EntityNotFinalRule when native lazy objects or lazy ghost objects are enabled (#724)
1 parent f92f4ac commit 734ef36

8 files changed

Lines changed: 208 additions & 4 deletions

File tree

compatibility/orm-3-baseline.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
$hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0;
99
if ($hasOrm3) {
1010
$includes[] = __DIR__ . '/../phpstan-baseline-orm-3.neon';
11+
} else {
12+
$includes[] = __DIR__ . '/../phpstan-baseline-orm-2.neon';
1113
}
1214

1315
$config = [];

phpstan-baseline-orm-2.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: "#^Call to an undefined method Doctrine\\\\ORM\\\\Configuration\\:\\:isNativeLazyObjectsEnabled\\(\\)\\.$#"
5+
identifier: method.notFound
6+
count: 1
7+
path: tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ parameters:
2525
- tests/*/data-attributes/*
2626
- tests/*/data-php-*/*
2727
- tests/Rules/Doctrine/ORM/entity-manager.php
28+
- tests/Rules/Doctrine/ORM/entity-manager-lazy-ghost-objects.php
2829

2930
reportUnmatchedIgnoredErrors: false
3031

src/Doctrine/Mapping/ClassMetadataFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected function initialize(): void
4040
$config = new Configuration();
4141
$config->setMetadataDriverImpl(count($drivers) === 1 ? $drivers[0] : new MappingDriverChain($drivers));
4242

43-
// @phpstan-ignore function.impossibleType (Available since Doctrine ORM 3.4)
43+
// @phpstan-ignore function.impossibleType, function.alreadyNarrowedType (Available since Doctrine ORM 3.4)
4444
if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) {
4545
$config->enableNativeLazyObjects(true);
4646
} else {

src/Rules/Doctrine/ORM/EntityNotFinalRule.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public function processNode(Node $node, Scope $scope): array
4848
return [];
4949
}
5050

51+
if ($this->objectMetadataResolver->isNativeLazyObjectsEnabled()) {
52+
return [];
53+
}
54+
5155
return [
5256
RuleErrorBuilder::message(sprintf(
5357
'Entity class %s is final which can cause problems with proxies.',

src/Type/Doctrine/ObjectMetadataResolver.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PHPStan\Type\Doctrine;
44

55
use Doctrine\Common\Annotations\AnnotationException;
6+
use Doctrine\ORM\Configuration;
7+
use Doctrine\ORM\EntityManagerInterface;
68
use Doctrine\ORM\Mapping\ClassMetadata;
79
use Doctrine\ORM\Mapping\MappingException;
810
use Doctrine\Persistence\ObjectManager;
@@ -12,7 +14,9 @@
1214
use function class_exists;
1315
use function is_file;
1416
use function is_readable;
17+
use function method_exists;
1518
use function sprintf;
19+
use const PHP_VERSION_ID;
1620

1721
final class ObjectMetadataResolver
1822
{
@@ -62,6 +66,30 @@ public function getObjectManager(): ?ObjectManager
6266
return $this->objectManager;
6367
}
6468

69+
public function isNativeLazyObjectsEnabled(): bool
70+
{
71+
$objectManager = $this->getObjectManager();
72+
73+
if ($objectManager instanceof EntityManagerInterface) {
74+
$config = $objectManager->getConfiguration();
75+
76+
// @phpstan-ignore function.impossibleType, function.alreadyNarrowedType (Available since Doctrine ORM 3.4)
77+
if (method_exists($config, 'isNativeLazyObjectsEnabled') && $config->isNativeLazyObjectsEnabled()) {
78+
return true;
79+
}
80+
81+
return false;
82+
}
83+
84+
// No object manager - check if the standalone ClassMetadataFactory would enable native lazy objects
85+
// @phpstan-ignore function.impossibleType, function.alreadyNarrowedType (Available since Doctrine ORM 3.4)
86+
if (PHP_VERSION_ID >= 80400 && class_exists(Configuration::class) && method_exists(Configuration::class, 'enableNativeLazyObjects')) {
87+
return true;
88+
}
89+
90+
return false;
91+
}
92+
6593
/**
6694
* @param class-string $className
6795
*/

tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace PHPStan\Rules\Doctrine\ORM;
44

5+
use Doctrine\ORM\Configuration;
56
use Iterator;
67
use PHPStan\Rules\Rule;
78
use PHPStan\Testing\RuleTestCase;
89
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
10+
use function method_exists;
11+
use const PHP_VERSION_ID;
912

1013
/**
1114
* @extends RuleTestCase<EntityNotFinalRule>
@@ -34,7 +37,7 @@ public function testRule(string $file, array $expectedErrors): void
3437
}
3538

3639
/**
37-
* @dataProvider ruleProvider
40+
* @dataProvider ruleWithoutObjectManagerLoaderProvider
3841
* @param list<array{0: string, 1: int, 2?: string}> $expectedErrors
3942
*/
4043
public function testRuleWithoutObjectManagerLoader(string $file, array $expectedErrors): void
@@ -43,19 +46,86 @@ public function testRuleWithoutObjectManagerLoader(string $file, array $expected
4346
$this->analyse([$file], $expectedErrors);
4447
}
4548

49+
/**
50+
* @dataProvider ruleWithNativeLazyObjectsProvider
51+
* @param list<array{0: string, 1: int, 2?: string}> $expectedErrors
52+
*/
53+
public function testRuleWithNativeLazyObjects(string $file, array $expectedErrors): void
54+
{
55+
// @phpstan-ignore function.impossibleType, function.alreadyNarrowedType
56+
if (PHP_VERSION_ID < 80400 || !method_exists(Configuration::class, 'enableNativeLazyObjects')) {
57+
self::markTestSkipped('Test requires PHP 8.4+ and Doctrine ORM 3.4+.');
58+
}
59+
60+
$this->objectManagerLoader = __DIR__ . '/entity-manager-lazy-ghost-objects.php';
61+
$this->analyse([$file], $expectedErrors);
62+
}
63+
4664
/**
4765
* @return Iterator<mixed[]>
4866
*/
4967
public function ruleProvider(): Iterator
5068
{
69+
$finalEntityErrors = self::isNativeLazyObjectsDefault()
70+
? []
71+
: [
72+
[
73+
'Entity class PHPStan\Rules\Doctrine\ORM\FinalEntity is final which can cause problems with proxies.',
74+
10,
75+
],
76+
];
77+
5178
yield 'final entity' => [
5279
__DIR__ . '/data/FinalEntity.php',
53-
[
80+
$finalEntityErrors,
81+
];
82+
83+
yield 'final annotated entity' => [
84+
__DIR__ . '/data/FinalAnnotatedEntity.php',
85+
[],
86+
];
87+
88+
yield 'final non-entity' => [
89+
__DIR__ . '/data/FinalNonEntity.php',
90+
[],
91+
];
92+
93+
yield 'correct entity' => [
94+
__DIR__ . '/data/MyEntity.php',
95+
[],
96+
];
97+
98+
yield 'final embeddable' => [
99+
__DIR__ . '/data/FinalEmbeddable.php',
100+
[],
101+
];
102+
103+
yield 'non final embeddable' => [
104+
__DIR__ . '/data/MyEmbeddable.php',
105+
[],
106+
];
107+
}
108+
109+
/**
110+
* @return Iterator<mixed[]>
111+
*/
112+
public function ruleWithoutObjectManagerLoaderProvider(): Iterator
113+
{
114+
$nativeLazyObjectsFallback = PHP_VERSION_ID >= 80400
115+
&& method_exists(Configuration::class, 'enableNativeLazyObjects'); // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType
116+
117+
$finalEntityErrors = $nativeLazyObjectsFallback
118+
? []
119+
: [
54120
[
55121
'Entity class PHPStan\Rules\Doctrine\ORM\FinalEntity is final which can cause problems with proxies.',
56122
10,
57123
],
58-
],
124+
];
125+
126+
yield 'final entity' => [
127+
__DIR__ . '/data/FinalEntity.php',
128+
$finalEntityErrors,
59129
];
60130

61131
yield 'final annotated entity' => [
@@ -84,4 +154,50 @@ public function ruleProvider(): Iterator
84154
];
85155
}
86156

157+
/**
158+
* @return Iterator<mixed[]>
159+
*/
160+
public function ruleWithNativeLazyObjectsProvider(): Iterator
161+
{
162+
yield 'final entity with native lazy objects' => [
163+
__DIR__ . '/data/FinalEntity.php',
164+
[],
165+
];
166+
167+
yield 'final annotated entity with native lazy objects' => [
168+
__DIR__ . '/data/FinalAnnotatedEntity.php',
169+
[],
170+
];
171+
172+
yield 'final non-entity with native lazy objects' => [
173+
__DIR__ . '/data/FinalNonEntity.php',
174+
[],
175+
];
176+
177+
yield 'correct entity with native lazy objects' => [
178+
__DIR__ . '/data/MyEntity.php',
179+
[],
180+
];
181+
182+
yield 'final embeddable with native lazy objects' => [
183+
__DIR__ . '/data/FinalEmbeddable.php',
184+
[],
185+
];
186+
187+
yield 'non final embeddable with native lazy objects' => [
188+
__DIR__ . '/data/MyEmbeddable.php',
189+
[],
190+
];
191+
}
192+
193+
private static function isNativeLazyObjectsDefault(): bool
194+
{
195+
// @phpstan-ignore function.impossibleType, function.alreadyNarrowedType
196+
if (!method_exists(Configuration::class, 'isNativeLazyObjectsEnabled')) {
197+
return false;
198+
}
199+
200+
return (new Configuration())->isNativeLazyObjectsEnabled();
201+
}
202+
87203
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
use Cache\Adapter\PHPArray\ArrayCachePool;
4+
use Doctrine\Common\Annotations\AnnotationReader;
5+
use Doctrine\DBAL\DriverManager;
6+
use Doctrine\DBAL\Types\DateTimeImmutableType;
7+
use Doctrine\DBAL\Types\Type;
8+
use Doctrine\ORM\Configuration;
9+
use Doctrine\ORM\EntityManager;
10+
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
11+
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
12+
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
13+
14+
$config = new Configuration();
15+
$config->setProxyDir(__DIR__);
16+
$config->setProxyNamespace('PHPstan\Doctrine\OrmProxies');
17+
$config->setMetadataCache(new ArrayCachePool());
18+
$config->enableNativeLazyObjects(true);
19+
20+
$metadataDriver = new MappingDriverChain();
21+
$metadataDriver->addDriver(new AnnotationDriver(
22+
new AnnotationReader(),
23+
[__DIR__ . '/data'],
24+
), 'PHPStan\\Rules\\Doctrine\\ORM\\');
25+
26+
if (PHP_VERSION_ID >= 80100) {
27+
$metadataDriver->addDriver(
28+
new AttributeDriver([__DIR__ . '/data-attributes']),
29+
'PHPStan\\Rules\\Doctrine\\ORMAttributes\\',
30+
);
31+
}
32+
33+
$config->setMetadataDriverImpl($metadataDriver);
34+
35+
Type::overrideType(
36+
'date',
37+
DateTimeImmutableType::class,
38+
);
39+
40+
return new EntityManager(
41+
DriverManager::getConnection([
42+
'driver' => 'pdo_sqlite',
43+
'memory' => true,
44+
]),
45+
$config,
46+
);

0 commit comments

Comments
 (0)