From b9973a72b237c7119f548c510b15c274d520b0d4 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Wed, 9 Oct 2024 23:12:02 +0200 Subject: [PATCH 01/16] Introduce PHP 8.4 lazy proxy/ghost API. --- phpstan-baseline.neon | 8 +--- src/Configuration.php | 16 ++++++++ src/Proxy/ProxyFactory.php | 41 ++++++++++++++++++- src/UnitOfWork.php | 17 +++++++- .../ORM/Functional/BasicFunctionalTest.php | 8 ++-- .../Functional/ProxiesLikeEntitiesTest.php | 4 ++ .../ORM/Functional/ReferenceProxyTest.php | 4 +- .../SecondLevelCacheQueryCacheTest.php | 5 --- .../SecondLevelCacheRepositoryTest.php | 9 ---- .../ORM/Functional/Ticket/DDC1238Test.php | 4 +- .../ORM/Functional/Ticket/GH10808Test.php | 15 ++----- tests/Tests/OrmFunctionalTestCase.php | 12 ++++++ 12 files changed, 98 insertions(+), 45 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f57c40d880a..cfbac899003 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -583,7 +583,7 @@ parameters: path: src/EntityManager.php - - message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns Doctrine\\ORM\\Proxy\\InternalProxy\.$#' + message: '#^Method Doctrine\\ORM\\EntityManager\:\:getReference\(\) should return \(T of object\)\|null but returns object\.$#' identifier: return.type count: 1 path: src/EntityManager.php @@ -2322,12 +2322,6 @@ parameters: count: 1 path: src/Proxy/ProxyFactory.php - - - message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:getProxy\(\) return type with generic interface Doctrine\\ORM\\Proxy\\InternalProxy does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: src/Proxy/ProxyFactory.php - - message: '#^Method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:loadProxyClass\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#' identifier: missingType.generics diff --git a/src/Configuration.php b/src/Configuration.php index 361d146a50b..5c963b4d577 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -30,6 +30,8 @@ use function is_a; use function strtolower; +use const PHP_VERSION_ID; + /** * Configuration container for all configuration options of Doctrine. * It combines all configuration options from DBAL & ORM. @@ -593,6 +595,20 @@ public function setSchemaIgnoreClasses(array $schemaIgnoreClasses): void $this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses; } + public function isLazyProxyEnabled(): bool + { + return $this->attributes['lazyProxy'] ?? false; + } + + public function setLazyProxyEnabled(bool $lazyProxy): void + { + if (PHP_VERSION_ID < 80400) { + throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.'); + } + + $this->attributes['lazyProxy'] = $lazyProxy; + } + /** * To be deprecated in 3.1.0 * diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 31d57769065..aebca670998 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -163,8 +163,43 @@ public function __construct( * @param class-string $className * @param array $identifier */ - public function getProxy(string $className, array $identifier): InternalProxy + public function getProxy(string $className, array $identifier): object { + if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + $classMetadata = $this->em->getClassMetadata($className); + $entityPersister = $this->uow->getEntityPersister($className); + + $proxy = $classMetadata->reflClass->newLazyGhost(static function ($object) use ($identifier, $entityPersister): void { + $entityPersister->loadById($identifier, $object); + }); + + foreach ($identifier as $idField => $value) { + $classMetadata->reflFields[$idField]->setRawValueWithoutLazyInitialization($proxy, $value); + } + + // todo: this skipLazyInitialization for properites calculation must be moved into ClassMetadata partially + $identifiers = array_flip($classMetadata->getIdentifierFieldNames()); + $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; + $reflector = $classMetadata->getReflectionClass(); + + while ($reflector) { + foreach ($reflector->getProperties($filter) as $property) { + $name = $property->name; + + if ($property->isStatic() || (($classMetadata->hasField($name) || $classMetadata->hasAssociation($name)) && ! isset($identifiers[$name]))) { + continue; + } + + $property->skipLazyInitialization($proxy); + } + + $filter = ReflectionProperty::IS_PRIVATE; + $reflector = $reflector->getParentClass(); + } + + return $proxy; + } + $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className); return $proxyFactory($identifier); @@ -182,6 +217,10 @@ public function getProxy(string $className, array $identifier): InternalProxy */ public function generateProxyClasses(array $classes, string|null $proxyDir = null): int { + if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + return 0; + } + $generated = 0; foreach ($classes as $class) { diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 3d9f594c3a1..3936d5e1744 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -47,6 +47,7 @@ use Doctrine\Persistence\PropertyChangedListener; use Exception; use InvalidArgumentException; +use ReflectionObject; use RuntimeException; use Stringable; use UnexpectedValueException; @@ -61,6 +62,7 @@ use function array_values; use function assert; use function current; +use function get_class; use function get_debug_type; use function implode; use function in_array; @@ -2378,7 +2380,11 @@ public function createEntity(string $className, array $data, array &$hints = []) } if ($this->isUninitializedObject($entity)) { - $entity->__setInitialized(true); + if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + $class->reflClass->markLazyObjectAsInitialized($entity); + } else { + $entity->__setInitialized(true); + } } else { if ( ! isset($hints[Query::HINT_REFRESH]) @@ -3034,6 +3040,11 @@ public function initializeObject(object $obj): void if ($obj instanceof PersistentCollection) { $obj->initialize(); } + + if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + $reflection = new ReflectionObject($obj); + $reflection->initializeLazyObject($obj); + } } /** @@ -3043,6 +3054,10 @@ public function initializeObject(object $obj): void */ public function isUninitializedObject(mixed $obj): bool { + if ($this->em->getConfiguration()->isLazyProxyEnabled() && ! ($obj instanceof Collection)) { + return $this->em->getClassMetadata($obj::class)->reflClass->isUninitializedLazyObject($obj); + } + return $obj instanceof InternalProxy && ! $obj->__isInitialized(); } diff --git a/tests/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Tests/ORM/Functional/BasicFunctionalTest.php index fe03a864060..a159bec6560 100644 --- a/tests/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Tests/ORM/Functional/BasicFunctionalTest.php @@ -9,7 +9,6 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\ORMInvalidArgumentException; use Doctrine\ORM\PersistentCollection; -use Doctrine\ORM\Proxy\InternalProxy; use Doctrine\ORM\Query; use Doctrine\ORM\UnitOfWork; use Doctrine\Tests\IterableTester; @@ -557,7 +556,7 @@ public function testSetToOneAssociationWithGetReference(): void $this->_em->persist($article); $this->_em->flush(); - self::assertFalse($userRef->__isInitialized()); + self::assertTrue($this->isUninitializedObject($userRef)); $this->_em->clear(); @@ -592,7 +591,7 @@ public function testAddToToManyAssociationWithGetReference(): void $this->_em->persist($user); $this->_em->flush(); - self::assertFalse($groupRef->__isInitialized()); + self::assertTrue($this->isUninitializedObject($groupRef)); $this->_em->clear(); @@ -940,8 +939,7 @@ public function testManyToOneFetchModeQuery(): void ->setParameter(1, $article->id) ->setFetchMode(CmsArticle::class, 'user', ClassMetadata::FETCH_EAGER) ->getSingleResult(); - self::assertInstanceOf(InternalProxy::class, $article->user, 'It IS a proxy, ...'); - self::assertFalse($this->isUninitializedObject($article->user), '...but its initialized!'); + self::assertFalse($this->isUninitializedObject($article->user)); $this->assertQueryCount(2); } diff --git a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index 0cc8776ba50..df8ca3b9187 100644 --- a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php +++ b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php @@ -34,6 +34,10 @@ protected function setUp(): void { parent::setUp(); + if ($this->_em->getConfiguration()->isLazyProxyEnabled()) { + self::markTestSkipped('This test is not applicable when lazy proxy is enabled.'); + } + $this->createSchemaForModels( CmsUser::class, CmsTag::class, diff --git a/tests/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Tests/ORM/Functional/ReferenceProxyTest.php index 55f65956757..d404a707b96 100644 --- a/tests/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Tests/ORM/Functional/ReferenceProxyTest.php @@ -6,7 +6,6 @@ use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; -use Doctrine\ORM\Proxy\InternalProxy; use Doctrine\Tests\Models\Company\CompanyAuction; use Doctrine\Tests\Models\ECommerce\ECommerceProduct; use Doctrine\Tests\Models\ECommerce\ECommerceProduct2; @@ -248,7 +247,6 @@ public function testCommonPersistenceProxy(): void assert($entity instanceof ECommerceProduct); $className = DefaultProxyClassNameResolver::getClass($entity); - self::assertInstanceOf(InternalProxy::class, $entity); self::assertTrue($this->isUninitializedObject($entity)); self::assertEquals(ECommerceProduct::class, $className); @@ -257,7 +255,7 @@ public function testCommonPersistenceProxy(): void $proxyFileName = $this->_em->getConfiguration()->getProxyDir() . DIRECTORY_SEPARATOR . str_replace('\\', '', $restName) . '.php'; self::assertTrue(file_exists($proxyFileName), 'Proxy file name cannot be found generically.'); - $entity->__load(); + $this->initializeObject($entity); self::assertFalse($this->isUninitializedObject($entity)); } } diff --git a/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index e2dec649342..4d5d09849a9 100644 --- a/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -11,7 +11,6 @@ use Doctrine\ORM\Cache\Exception\CacheException; use Doctrine\ORM\Cache\QueryCacheEntry; use Doctrine\ORM\Cache\QueryCacheKey; -use Doctrine\ORM\Proxy\InternalProxy; use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Tests\Models\Cache\Attraction; @@ -939,7 +938,6 @@ public function testResolveAssociationCacheEntry(): void self::assertNotNull($state1->getCountry()); $this->assertQueryCount(1); self::assertInstanceOf(State::class, $state1); - self::assertInstanceOf(InternalProxy::class, $state1->getCountry()); self::assertEquals($countryName, $state1->getCountry()->getName()); self::assertEquals($stateId, $state1->getId()); @@ -957,7 +955,6 @@ public function testResolveAssociationCacheEntry(): void self::assertNotNull($state2->getCountry()); $this->assertQueryCount(0); self::assertInstanceOf(State::class, $state2); - self::assertInstanceOf(InternalProxy::class, $state2->getCountry()); self::assertEquals($countryName, $state2->getCountry()->getName()); self::assertEquals($stateId, $state2->getId()); } @@ -1031,7 +1028,6 @@ public function testResolveToManyAssociationCacheEntry(): void $this->assertQueryCount(1); self::assertInstanceOf(State::class, $state1); - self::assertInstanceOf(InternalProxy::class, $state1->getCountry()); self::assertInstanceOf(City::class, $state1->getCities()->get(0)); self::assertInstanceOf(State::class, $state1->getCities()->get(0)->getState()); self::assertSame($state1, $state1->getCities()->get(0)->getState()); @@ -1048,7 +1044,6 @@ public function testResolveToManyAssociationCacheEntry(): void $this->assertQueryCount(0); self::assertInstanceOf(State::class, $state2); - self::assertInstanceOf(InternalProxy::class, $state2->getCountry()); self::assertInstanceOf(City::class, $state2->getCities()->get(0)); self::assertInstanceOf(State::class, $state2->getCities()->get(0)->getState()); self::assertSame($state2, $state2->getCities()->get(0)->getState()); diff --git a/tests/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php index 7ed7732526c..9473551112e 100644 --- a/tests/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php +++ b/tests/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -4,7 +4,6 @@ namespace Doctrine\Tests\ORM\Functional; -use Doctrine\ORM\Proxy\InternalProxy; use Doctrine\Tests\Models\Cache\Country; use Doctrine\Tests\Models\Cache\State; use PHPUnit\Framework\Attributes\Group; @@ -198,8 +197,6 @@ public function testRepositoryCacheFindAllToOneAssociation(): void self::assertInstanceOf(State::class, $entities[1]); self::assertInstanceOf(Country::class, $entities[0]->getCountry()); self::assertInstanceOf(Country::class, $entities[0]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry()); // load from cache $this->getQueryLog()->reset()->enable(); @@ -212,8 +209,6 @@ public function testRepositoryCacheFindAllToOneAssociation(): void self::assertInstanceOf(State::class, $entities[1]); self::assertInstanceOf(Country::class, $entities[0]->getCountry()); self::assertInstanceOf(Country::class, $entities[1]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry()); // invalidate cache $this->_em->persist(new State('foo', $this->_em->find(Country::class, $this->countries[0]->getId()))); @@ -231,8 +226,6 @@ public function testRepositoryCacheFindAllToOneAssociation(): void self::assertInstanceOf(State::class, $entities[1]); self::assertInstanceOf(Country::class, $entities[0]->getCountry()); self::assertInstanceOf(Country::class, $entities[1]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry()); // load from cache $this->getQueryLog()->reset()->enable(); @@ -245,7 +238,5 @@ public function testRepositoryCacheFindAllToOneAssociation(): void self::assertInstanceOf(State::class, $entities[1]); self::assertInstanceOf(Country::class, $entities[0]->getCountry()); self::assertInstanceOf(Country::class, $entities[1]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[0]->getCountry()); - self::assertInstanceOf(InternalProxy::class, $entities[1]->getCountry()); } } diff --git a/tests/Tests/ORM/Functional/Ticket/DDC1238Test.php b/tests/Tests/ORM/Functional/Ticket/DDC1238Test.php index 7a3cce370fd..72dc3496e74 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC1238Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC1238Test.php @@ -57,11 +57,11 @@ public function testIssueProxyClear(): void $user2 = $this->_em->getReference(DDC1238User::class, $userId); - //$user->__load(); + //$this->initializeObject($user); self::assertIsInt($user->getId(), 'Even if a proxy is detached, it should still have an identifier'); - $user2->__load(); + $this->initializeObject($user2); self::assertIsInt($user2->getId(), 'The managed instance still has an identifier'); } diff --git a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php index 0e893233442..30374a3f559 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php @@ -15,8 +15,6 @@ use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\Group; -use function get_class; - #[Group('GH10808')] class GH10808Test extends OrmFunctionalTestCase { @@ -49,18 +47,11 @@ public function testDQLDeferredEagerLoad(): void // Clear the EM to prevent the recovery of the loaded instance, which would otherwise result in a proxy. $this->_em->clear(); + self::assertTrue($this->_em->getUnitOfWork()->isUninitializedObject($deferredLoadResult->child)); + $eagerLoadResult = $query->setHint(UnitOfWork::HINT_DEFEREAGERLOAD, false)->getSingleResult(); - self::assertNotEquals( - GH10808AppointmentChild::class, - get_class($deferredLoadResult->child), - '$deferredLoadResult->child should be a proxy', - ); - self::assertEquals( - GH10808AppointmentChild::class, - get_class($eagerLoadResult->child), - '$eagerLoadResult->child should not be a proxy', - ); + self::assertFalse($this->_em->getUnitOfWork()->isUninitializedObject($eagerLoadResult->child)); } } diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index efd380dc1de..262ab08bb47 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -193,6 +193,7 @@ use function var_export; use const PHP_EOL; +use const PHP_VERSION_ID; /** * Base testcase class for all functional ORM testcases. @@ -941,6 +942,12 @@ protected function getEntityManager( $this->isSecondLevelCacheEnabled = true; } + $enableLazyProxy = getenv('ENABLE_LAZY_PROXY'); + + if (PHP_VERSION_ID >= 80400 && $enableLazyProxy) { + $config->setLazyProxyEnabled(true); + } + $config->setMetadataDriverImpl( $mappingDriver ?? new AttributeDriver([ realpath(__DIR__ . '/Models/Cache'), @@ -1118,4 +1125,9 @@ final protected function isUninitializedObject(object $entity): bool { return $this->_em->getUnitOfWork()->isUninitializedObject($entity); } + + final protected function initializeObject(object $entity): void + { + $this->_em->getUnitOfWork()->initializeObject($entity); + } } From 77754a34c209c4a281c9ec425a7b0586448aa78c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 18 Feb 2025 23:51:57 +0100 Subject: [PATCH 02/16] Call setRawValueWithoutLazyInitialization for support with lazy proxy. --- .../PropertyAccessors/RawValuePropertyAccessor.php | 8 +++++++- src/Proxy/ProxyFactory.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php index 27d3cf6ab87..e048b1bb81b 100644 --- a/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php @@ -5,8 +5,11 @@ namespace Doctrine\ORM\Mapping\PropertyAccessors; use Doctrine\ORM\Proxy\InternalProxy; +use LogicException; use ReflectionProperty; +use const PHP_VERSION_ID; + use function ltrim; /** @@ -28,12 +31,15 @@ public static function fromReflectionProperty(ReflectionProperty $reflectionProp private function __construct(private ReflectionProperty $reflectionProperty, private string $key) { + if (PHP_VERSION_ID < 80400) { + throw new LogicException('This class requires PHP 8.4 or higher.'); + } } public function setValue(object $object, mixed $value): void { if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) { - $this->reflectionProperty->setRawValue($object, $value); + $this->reflectionProperty->setRawValueWithoutLazyInitialization($object, $value); return; } diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index aebca670998..297c114941c 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -174,7 +174,7 @@ public function getProxy(string $className, array $identifier): object }); foreach ($identifier as $idField => $value) { - $classMetadata->reflFields[$idField]->setRawValueWithoutLazyInitialization($proxy, $value); + $classMetadata->propertyAccessors[$idField]->setValue($proxy, $value); } // todo: this skipLazyInitialization for properites calculation must be moved into ClassMetadata partially From e2079f9310d8dd1ae11cd7d45e7add6c7a3661ee Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 21:45:07 +0100 Subject: [PATCH 03/16] Refactorings --- src/UnitOfWork.php | 4 +++- tests/Tests/ORM/Functional/ReferenceProxyTest.php | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 3936d5e1744..8812e6cb9f5 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -3039,10 +3039,12 @@ public function initializeObject(object $obj): void if ($obj instanceof PersistentCollection) { $obj->initialize(); + + return; } if ($this->em->getConfiguration()->isLazyProxyEnabled()) { - $reflection = new ReflectionObject($obj); + $reflection = $this->em->getClassMetadata($obj::class)->getReflectionClass(); $reflection->initializeLazyObject($obj); } } diff --git a/tests/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Tests/ORM/Functional/ReferenceProxyTest.php index d404a707b96..262df033dc9 100644 --- a/tests/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Tests/ORM/Functional/ReferenceProxyTest.php @@ -241,6 +241,10 @@ public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier(): v #[Group('DDC-1604')] public function testCommonPersistenceProxy(): void { + if ($this->_em->getConfiguration()->isLazyProxyEnabled()) { + self::markTestSkipped('Test only works with proxy generation disabled.'); + } + $id = $this->createProduct(); $entity = $this->_em->getReference(ECommerceProduct::class, $id); From db4cede57dfabac855ce35b83de15b5dabf007ad Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 22:10:38 +0100 Subject: [PATCH 04/16] Revert test change partially and skip with lazy objects. --- .../Tests/ORM/Functional/Ticket/GH10808Test.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php index 30374a3f559..0ac23060079 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php @@ -30,6 +30,10 @@ protected function setUp(): void public function testDQLDeferredEagerLoad(): void { + if ($this->_em->getConfiguration()->isLazyProxyEnabled()) { + self::markTestSkipped('Test requires lazy loading to be disabled'); + } + $appointment = new GH10808Appointment(); $this->_em->persist($appointment); @@ -47,11 +51,18 @@ public function testDQLDeferredEagerLoad(): void // Clear the EM to prevent the recovery of the loaded instance, which would otherwise result in a proxy. $this->_em->clear(); - self::assertTrue($this->_em->getUnitOfWork()->isUninitializedObject($deferredLoadResult->child)); - $eagerLoadResult = $query->setHint(UnitOfWork::HINT_DEFEREAGERLOAD, false)->getSingleResult(); - self::assertFalse($this->_em->getUnitOfWork()->isUninitializedObject($eagerLoadResult->child)); + self::assertNotEquals( + GH10808AppointmentChild::class, + get_class($deferredLoadResult->child), + '$deferredLoadResult->child should be a proxy', + ); + self::assertEquals( + GH10808AppointmentChild::class, + get_class($eagerLoadResult->child), + '$eagerLoadResult->child should not be a proxy', + ); } } From 325beaab5494f7dea5f986e67a3bb0af8abe5e3c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 22:12:14 +0100 Subject: [PATCH 05/16] Houskeeping: phpcs --- src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php | 4 ++-- src/UnitOfWork.php | 2 -- tests/Tests/ORM/Functional/Ticket/GH10808Test.php | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php index f459595d3a4..d4e0459f94c 100644 --- a/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php @@ -8,10 +8,10 @@ use LogicException; use ReflectionProperty; -use const PHP_VERSION_ID; - use function ltrim; +use const PHP_VERSION_ID; + /** * This is a PHP 8.4 and up only class and replaces ObjectCastPropertyAccessor. * diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 8812e6cb9f5..08fa718ada9 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -47,7 +47,6 @@ use Doctrine\Persistence\PropertyChangedListener; use Exception; use InvalidArgumentException; -use ReflectionObject; use RuntimeException; use Stringable; use UnexpectedValueException; @@ -62,7 +61,6 @@ use function array_values; use function assert; use function current; -use function get_class; use function get_debug_type; use function implode; use function in_array; diff --git a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php index 0ac23060079..4248e39fa35 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php @@ -15,6 +15,8 @@ use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\Group; +use function get_class; + #[Group('GH10808')] class GH10808Test extends OrmFunctionalTestCase { From f3627044931ef9fd8154e8e92ce6fc060e81c710 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 22:16:40 +0100 Subject: [PATCH 06/16] Run with ENABLE_LAZY_PROXY=1 in php 8.4 matrix. --- .github/workflows/continuous-integration.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 252c9ff5ca0..9b994fb7bf8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -45,17 +45,27 @@ jobs: - "pdo_sqlite" deps: - "highest" + lazy_proxy: + - "0" include: - php-version: "8.2" dbal-version: "4@dev" extension: "pdo_sqlite" + lazy_proxy: "0" - php-version: "8.2" dbal-version: "4@dev" extension: "sqlite3" + lazy_proxy: "0" - php-version: "8.1" dbal-version: "default" deps: "lowest" extension: "pdo_sqlite" + lazy_proxy: "0" + - php-version: "8.4" + dbal-version: "default" + deps: "highest" + extension: "pdo_sqlite" + lazy_proxy: "1" steps: - name: "Checkout" @@ -85,11 +95,13 @@ jobs: run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage-no-cache.xml" env: ENABLE_SECOND_LEVEL_CACHE: 0 + ENABLE_LAZY_PROXY: ${{ matrix.lazy_proxy }} - name: "Run PHPUnit with Second Level Cache" run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml" env: ENABLE_SECOND_LEVEL_CACHE: 1 + ENABLE_LAZY_PROXY: ${{ matrix.lazy_proxy }} - name: "Upload coverage file" uses: "actions/upload-artifact@v4" From dc0f3acb694ca901fa754859955e7e6253467f35 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 22:24:55 +0100 Subject: [PATCH 07/16] Fix ci --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9b994fb7bf8..0c4b44c2de8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -106,7 +106,7 @@ jobs: - name: "Upload coverage file" uses: "actions/upload-artifact@v4" with: - name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-coverage" + name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.lazy_proxy }}-coverage" path: "coverage*.xml" From 0d54881c8361321ca259da96f80b7ef915dc783a Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 22:42:16 +0100 Subject: [PATCH 08/16] Transient properties are not skipping lazy initialization anymore, to expensive and could lead to errors. Adjust lifecycle test that uses transient properittes for assertions. --- src/Proxy/ProxyFactory.php | 20 ----------------- .../ORM/Functional/LifecycleCallbackTest.php | 22 +++++++++---------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 297c114941c..68ecbd47fec 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -177,26 +177,6 @@ public function getProxy(string $className, array $identifier): object $classMetadata->propertyAccessors[$idField]->setValue($proxy, $value); } - // todo: this skipLazyInitialization for properites calculation must be moved into ClassMetadata partially - $identifiers = array_flip($classMetadata->getIdentifierFieldNames()); - $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; - $reflector = $classMetadata->getReflectionClass(); - - while ($reflector) { - foreach ($reflector->getProperties($filter) as $property) { - $name = $property->name; - - if ($property->isStatic() || (($classMetadata->hasField($name) || $classMetadata->hasAssociation($name)) && ! isset($identifiers[$name]))) { - continue; - } - - $property->skipLazyInitialization($proxy); - } - - $filter = ReflectionProperty::IS_PRIVATE; - $reflector = $reflector->getParentClass(); - } - return $proxy; } diff --git a/tests/Tests/ORM/Functional/LifecycleCallbackTest.php b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php index 3dcee0e3712..d5d1e32012d 100644 --- a/tests/Tests/ORM/Functional/LifecycleCallbackTest.php +++ b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php @@ -69,7 +69,7 @@ public function testPreSavePostSaveCallbacksAreInvoked(): void $query = $this->_em->createQuery('select e from Doctrine\Tests\ORM\Functional\LifecycleCallbackTestEntity e'); $result = $query->getResult(); - self::assertTrue($result[0]->postLoadCallbackInvoked); + self::assertTrue($result[0]::$postLoadCallbackInvoked); $result[0]->value = 'hello again'; @@ -132,10 +132,10 @@ public function testGetReferenceWithPostLoadEventIsDelayedUntilProxyTrigger(): v $this->_em->clear(); $reference = $this->_em->getReference(LifecycleCallbackTestEntity::class, $id); - self::assertFalse($reference->postLoadCallbackInvoked); + self::assertArrayNotHasKey('postLoadCallbackInvoked', (array) $reference); $reference->getValue(); // trigger proxy load - self::assertTrue($reference->postLoadCallbackInvoked); + self::assertTrue($reference::$postLoadCallbackInvoked); } #[Group('DDC-958')] @@ -150,11 +150,11 @@ public function testPostLoadTriggeredOnRefresh(): void $this->_em->clear(); $reference = $this->_em->find(LifecycleCallbackTestEntity::class, $id); - self::assertTrue($reference->postLoadCallbackInvoked); + self::assertTrue($reference::$postLoadCallbackInvoked); $reference->postLoadCallbackInvoked = false; $this->_em->refresh($reference); - self::assertTrue($reference->postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.'); + self::assertTrue($reference::$postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.'); } #[Group('DDC-113')] @@ -212,7 +212,7 @@ public function testCascadedEntitiesLoadedInPostLoad(): void ->createQuery(sprintf($dql, $e1->getId(), $e2->getId())) ->getResult(); - self::assertTrue(current($entities)->postLoadCallbackInvoked); + self::assertTrue(current($entities)::$postLoadCallbackInvoked); self::assertTrue(current($entities)->postLoadCascaderNotNull); self::assertTrue(current($entities)->cascader->postLoadCallbackInvoked); self::assertEquals(current($entities)->cascader->postLoadEntitiesCount, 2); @@ -252,7 +252,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIteration(): void $iterableResult = iterator_to_array($query->toIterable()); foreach ($iterableResult as $entity) { - self::assertTrue($entity->postLoadCallbackInvoked); + self::assertTrue($entity::$postLoadCallbackInvoked); self::assertFalse($entity->postLoadCascaderNotNull); break; @@ -276,7 +276,7 @@ public function testCascadedEntitiesNotLoadedInPostLoadDuringIterationWithSimple $result = iterator_to_array($query->toIterable([], Query::HYDRATE_SIMPLEOBJECT)); foreach ($result as $entity) { - self::assertTrue($entity->postLoadCallbackInvoked); + self::assertTrue($entity::$postLoadCallbackInvoked); self::assertFalse($entity->postLoadCascaderNotNull); break; @@ -320,7 +320,7 @@ public function testPostLoadIsInvokedOnFetchJoinedEntities(): void self::assertTrue($fetchedA->postLoadCallbackInvoked); foreach ($fetchedA->entities as $fetchJoinedEntB) { - self::assertTrue($fetchJoinedEntB->postLoadCallbackInvoked); + self::assertTrue($fetchJoinedEntB::$postLoadCallbackInvoked); } } @@ -455,7 +455,7 @@ class LifecycleCallbackTestEntity public $postPersistCallbackInvoked = false; /** @var bool */ - public $postLoadCallbackInvoked = false; + public static $postLoadCallbackInvoked = false; /** @var bool */ public $postLoadCascaderNotNull = false; @@ -502,7 +502,7 @@ public function doStuffOnPostPersist(): void #[PostLoad] public function doStuffOnPostLoad(): void { - $this->postLoadCallbackInvoked = true; + self::$postLoadCallbackInvoked = true; $this->postLoadCascaderNotNull = isset($this->cascader); } From 4007d4a76a8a1dc7181b4d3bbb14ae2b4d02681b Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 27 Feb 2025 23:05:14 +0100 Subject: [PATCH 09/16] Restore behavior preventing property hook use in 8.4 in unsupported coditions --- src/Mapping/ClassMetadataFactory.php | 11 +++++++++++ tests/Tests/ORM/Functional/LifecycleCallbackTest.php | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Mapping/ClassMetadataFactory.php b/src/Mapping/ClassMetadataFactory.php index f66b53ff2ef..651d8f64f23 100644 --- a/src/Mapping/ClassMetadataFactory.php +++ b/src/Mapping/ClassMetadataFactory.php @@ -24,6 +24,7 @@ use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Persistence\Mapping\ReflectionService; +use LogicException; use ReflectionClass; use ReflectionException; @@ -41,6 +42,8 @@ use function strtolower; use function substr; +use const PHP_VERSION_ID; + /** * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the * metadata mapping information of a class which describes how a class should be mapped @@ -296,6 +299,14 @@ protected function validateRuntimeMetadata(ClassMetadata $class, ClassMetadataIn // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy throw MappingException::noInheritanceOnMappedSuperClass($class->name); } + + foreach ($class->propertyAccessors as $propertyAccessor) { + $property = $propertyAccessor->getUnderlyingReflector(); + + if (PHP_VERSION_ID >= 80400 && count($property->getHooks()) > 0) { + throw new LogicException('Doctrine ORM does not support property hooks without also enabling Configuration::setLazyProxyEnabled(true). Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.'); + } + } } protected function newClassMetadataInstance(string $className): ClassMetadata diff --git a/tests/Tests/ORM/Functional/LifecycleCallbackTest.php b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php index d5d1e32012d..59812498057 100644 --- a/tests/Tests/ORM/Functional/LifecycleCallbackTest.php +++ b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php @@ -151,7 +151,7 @@ public function testPostLoadTriggeredOnRefresh(): void $reference = $this->_em->find(LifecycleCallbackTestEntity::class, $id); self::assertTrue($reference::$postLoadCallbackInvoked); - $reference->postLoadCallbackInvoked = false; + $reference::$postLoadCallbackInvoked = false; $this->_em->refresh($reference); self::assertTrue($reference::$postLoadCallbackInvoked, 'postLoad should be invoked when refresh() is called.'); From 65165f7123318c1fe1119d5fbe4515c239bd2b90 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 28 Feb 2025 21:35:42 +0100 Subject: [PATCH 10/16] Add \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE Co-authored-by: Nicolas Grekas --- src/Proxy/ProxyFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 68ecbd47fec..57f45bd3038 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -171,7 +171,7 @@ public function getProxy(string $className, array $identifier): object $proxy = $classMetadata->reflClass->newLazyGhost(static function ($object) use ($identifier, $entityPersister): void { $entityPersister->loadById($identifier, $object); - }); + }, \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE); foreach ($identifier as $idField => $value) { $classMetadata->propertyAccessors[$idField]->setValue($proxy, $value); From dea73d9a5bf40df7234503db358e8cf47e183d6d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 28 Feb 2025 21:42:18 +0100 Subject: [PATCH 11/16] Rename isNativeLazyObjectsEnabled/enableNativeLazyObjects. --- src/Configuration.php | 8 ++++---- src/Proxy/ProxyFactory.php | 4 ++-- src/UnitOfWork.php | 6 +++--- tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php | 2 +- tests/Tests/ORM/Functional/ReferenceProxyTest.php | 2 +- tests/Tests/ORM/Functional/Ticket/GH10808Test.php | 2 +- tests/Tests/OrmFunctionalTestCase.php | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Configuration.php b/src/Configuration.php index 5c963b4d577..ca22d9bb760 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -595,18 +595,18 @@ public function setSchemaIgnoreClasses(array $schemaIgnoreClasses): void $this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses; } - public function isLazyProxyEnabled(): bool + public function isNativeLazyObjectsEnabled(): bool { - return $this->attributes['lazyProxy'] ?? false; + return $this->attributes['nativeLazyObjects'] ?? false; } - public function setLazyProxyEnabled(bool $lazyProxy): void + public function enableNativeLazyObjects(bool $nativeLazyObjects): void { if (PHP_VERSION_ID < 80400) { throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.'); } - $this->attributes['lazyProxy'] = $lazyProxy; + $this->attributes['nativeLazyObjects'] = $nativeLazyObjects; } /** diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 57f45bd3038..372d47c54bb 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -165,7 +165,7 @@ public function __construct( */ public function getProxy(string $className, array $identifier): object { - if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { $classMetadata = $this->em->getClassMetadata($className); $entityPersister = $this->uow->getEntityPersister($className); @@ -197,7 +197,7 @@ public function getProxy(string $className, array $identifier): object */ public function generateProxyClasses(array $classes, string|null $proxyDir = null): int { - if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { return 0; } diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 08fa718ada9..96aba64235a 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2378,7 +2378,7 @@ public function createEntity(string $className, array $data, array &$hints = []) } if ($this->isUninitializedObject($entity)) { - if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { $class->reflClass->markLazyObjectAsInitialized($entity); } else { $entity->__setInitialized(true); @@ -3041,7 +3041,7 @@ public function initializeObject(object $obj): void return; } - if ($this->em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { $reflection = $this->em->getClassMetadata($obj::class)->getReflectionClass(); $reflection->initializeLazyObject($obj); } @@ -3054,7 +3054,7 @@ public function initializeObject(object $obj): void */ public function isUninitializedObject(mixed $obj): bool { - if ($this->em->getConfiguration()->isLazyProxyEnabled() && ! ($obj instanceof Collection)) { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($obj instanceof Collection)) { return $this->em->getClassMetadata($obj::class)->reflClass->isUninitializedLazyObject($obj); } diff --git a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index df8ca3b9187..9201c3d81c8 100644 --- a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php +++ b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php @@ -34,7 +34,7 @@ protected function setUp(): void { parent::setUp(); - if ($this->_em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) { self::markTestSkipped('This test is not applicable when lazy proxy is enabled.'); } diff --git a/tests/Tests/ORM/Functional/ReferenceProxyTest.php b/tests/Tests/ORM/Functional/ReferenceProxyTest.php index 262df033dc9..8184b78dd04 100644 --- a/tests/Tests/ORM/Functional/ReferenceProxyTest.php +++ b/tests/Tests/ORM/Functional/ReferenceProxyTest.php @@ -241,7 +241,7 @@ public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier(): v #[Group('DDC-1604')] public function testCommonPersistenceProxy(): void { - if ($this->_em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) { self::markTestSkipped('Test only works with proxy generation disabled.'); } diff --git a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php index 4248e39fa35..731020e9d1d 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php @@ -32,7 +32,7 @@ protected function setUp(): void public function testDQLDeferredEagerLoad(): void { - if ($this->_em->getConfiguration()->isLazyProxyEnabled()) { + if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) { self::markTestSkipped('Test requires lazy loading to be disabled'); } diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index 262ab08bb47..65e9ee48ca1 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -942,10 +942,10 @@ protected function getEntityManager( $this->isSecondLevelCacheEnabled = true; } - $enableLazyProxy = getenv('ENABLE_LAZY_PROXY'); + $enableNativeLazyObjects = getenv('ENABLE_NATIVE_LAZY_OBJECTS'); - if (PHP_VERSION_ID >= 80400 && $enableLazyProxy) { - $config->setLazyProxyEnabled(true); + if (PHP_VERSION_ID >= 80400 && $enableNativeLazyObjects) { + $config->enableNativeLazyObjects(true); } $config->setMetadataDriverImpl( From db057a2a4860ce3564164053472286da81ccaf4c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 28 Feb 2025 21:43:21 +0100 Subject: [PATCH 12/16] Housekeeping: phpcs --- src/Proxy/ProxyFactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 372d47c54bb..5c362b7429d 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Utility\IdentifierFlattener; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Proxy; +use ReflectionClass; use ReflectionProperty; use Symfony\Component\VarExporter\ProxyHelper; @@ -171,7 +172,7 @@ public function getProxy(string $className, array $identifier): object $proxy = $classMetadata->reflClass->newLazyGhost(static function ($object) use ($identifier, $entityPersister): void { $entityPersister->loadById($identifier, $object); - }, \ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE); + }, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE); foreach ($identifier as $idField => $value) { $classMetadata->propertyAccessors[$idField]->setValue($proxy, $value); From 6b7a492eeee0c350feb16478f97f3bba4acc153e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 2 Mar 2025 00:03:50 +0100 Subject: [PATCH 13/16] Update advanced-configuration docs and make proxy config variables not required anymore with native lazy objects. --- docs/en/reference/advanced-configuration.rst | 35 +++++++++++++++++--- src/Proxy/ProxyFactory.php | 4 +-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 3282bbdb359..a0a9fab7dd4 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -19,7 +19,7 @@ steps of configuration. // ... - if ($applicationMode == "development") { + if ($applicationMode === "development") { $queryCache = new ArrayAdapter(); $metadataCache = new ArrayAdapter(); } else { @@ -35,10 +35,14 @@ steps of configuration. $config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies'); $config->setProxyNamespace('MyProject\Proxies'); - if ($applicationMode == "development") { - $config->setAutoGenerateProxyClasses(true); + if (PHP_VERSION_ID > 80400) { + $config->enableNativeLazyObjects(true); } else { - $config->setAutoGenerateProxyClasses(false); + if ($applicationMode === "development") { + $config->setAutoGenerateProxyClasses(true); + } else { + $config->setAutoGenerateProxyClasses(false); + } } $connection = DriverManager::getConnection([ @@ -71,9 +75,26 @@ Configuration Options The following sections describe all the configuration options available on a ``Doctrine\ORM\Configuration`` instance. +Native Lazy Objects (***OPTIONAL***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With PHP 8.4 we recommend that you use native lazy objects instead of +the code generation approach using the symfony/var-exporter Ghost trait. + +With Doctrine 4, the minimal requirement will become PHP 8.4 and native lazy objects +will become the only approach to lazy loading. + +.. code-block:: php + + enableNativeLazyObjects(true); + Proxy Directory (***REQUIRED***) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This setting is not required if you use native lazy objects with PHP 8.4 +and will be removed in the future. + .. code-block:: php getConfiguration()->isNativeLazyObjectsEnabled()) { throw ORMInvalidArgumentException::proxyDirectoryRequired(); } - if (! $proxyNs) { + if (! $proxyNs && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) { throw ORMInvalidArgumentException::proxyNamespaceRequired(); } From e32fc2f75e8863b24bf5a23b7cf6c0e5195aa252 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 7 Mar 2025 23:40:34 +0100 Subject: [PATCH 14/16] Move code around --- docs/en/reference/advanced-configuration.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index a0a9fab7dd4..6a3376597d9 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -32,12 +32,13 @@ steps of configuration. $driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true); $config->setMetadataDriverImpl($driverImpl); $config->setQueryCache($queryCache); - $config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies'); - $config->setProxyNamespace('MyProject\Proxies'); if (PHP_VERSION_ID > 80400) { $config->enableNativeLazyObjects(true); } else { + $config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies'); + $config->setProxyNamespace('MyProject\Proxies'); + if ($applicationMode === "development") { $config->setAutoGenerateProxyClasses(true); } else { From 7fdc4e548995905ee59cfc5fa8456f1248d4ccd5 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 24 Mar 2025 22:45:49 +0100 Subject: [PATCH 15/16] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Paris --- docs/en/reference/advanced-configuration.rst | 2 +- src/Mapping/ClassMetadataFactory.php | 2 +- src/Proxy/ProxyFactory.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 6a3376597d9..367be9add43 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -80,7 +80,7 @@ Native Lazy Objects (***OPTIONAL***) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ With PHP 8.4 we recommend that you use native lazy objects instead of -the code generation approach using the symfony/var-exporter Ghost trait. +the code generation approach using the ``symfony/var-exporter`` Ghost trait. With Doctrine 4, the minimal requirement will become PHP 8.4 and native lazy objects will become the only approach to lazy loading. diff --git a/src/Mapping/ClassMetadataFactory.php b/src/Mapping/ClassMetadataFactory.php index 651d8f64f23..ec9693594f1 100644 --- a/src/Mapping/ClassMetadataFactory.php +++ b/src/Mapping/ClassMetadataFactory.php @@ -304,7 +304,7 @@ protected function validateRuntimeMetadata(ClassMetadata $class, ClassMetadataIn $property = $propertyAccessor->getUnderlyingReflector(); if (PHP_VERSION_ID >= 80400 && count($property->getHooks()) > 0) { - throw new LogicException('Doctrine ORM does not support property hooks without also enabling Configuration::setLazyProxyEnabled(true). Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.'); + throw new LogicException('Doctrine ORM does not support property hooks without also enabling Configuration::enableNativeLazyObjects(true). Check https://github.com/doctrine/orm/issues/11624 for details of versions that support property hooks.'); } } } diff --git a/src/Proxy/ProxyFactory.php b/src/Proxy/ProxyFactory.php index 5741a8e07cc..2cfeb0f2847 100644 --- a/src/Proxy/ProxyFactory.php +++ b/src/Proxy/ProxyFactory.php @@ -170,7 +170,7 @@ public function getProxy(string $className, array $identifier): object $classMetadata = $this->em->getClassMetadata($className); $entityPersister = $this->uow->getEntityPersister($className); - $proxy = $classMetadata->reflClass->newLazyGhost(static function ($object) use ($identifier, $entityPersister): void { + $proxy = $classMetadata->reflClass->newLazyGhost(static function (object $object) use ($identifier, $entityPersister): void { $entityPersister->loadById($identifier, $object); }, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE); From 17a1d87ac5aa51758406549743a56e088ce6830c Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 28 Mar 2025 20:17:40 +0100 Subject: [PATCH 16/16] Pick suggestions --- docs/en/reference/advanced-configuration.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 5d54975da30..d320f436680 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -90,11 +90,11 @@ will become the only approach to lazy loading. enableNativeLazyObjects(true); -Proxy Directory (**REQUIRED**) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Proxy Directory +~~~~~~~~~~~~~~~ -This setting is not required if you use native lazy objects with PHP 8.4 -and will be removed in the future. +Required except if you use native lazy objects with PHP 8.4. +This setting will be removed in the future. .. code-block:: php @@ -107,11 +107,11 @@ classes. For a detailed explanation on proxy classes and how they are used in Doctrine, refer to the "Proxy Objects" section further down. -Proxy Namespace (**REQUIRED**) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Proxy Namespace +~~~~~~~~~~~~~~~ -This setting is not required if you use native lazy objects with PHP 8.4 -and will be removed in the future. +Required except if you use native lazy objects with PHP 8.4. +This setting will be removed in the future. .. code-block:: php