diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 252c9ff5ca0..0c4b44c2de8 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,16 +95,18 @@ 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" 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" diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 3282bbdb359..6a3376597d9 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 { @@ -32,13 +32,18 @@ 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 ($applicationMode == "development") { - $config->setAutoGenerateProxyClasses(true); + if (PHP_VERSION_ID > 80400) { + $config->enableNativeLazyObjects(true); } else { - $config->setAutoGenerateProxyClasses(false); + $config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies'); + $config->setProxyNamespace('MyProject\Proxies'); + + if ($applicationMode === "development") { + $config->setAutoGenerateProxyClasses(true); + } else { + $config->setAutoGenerateProxyClasses(false); + } } $connection = DriverManager::getConnection([ @@ -71,9 +76,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 attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses; } + public function isNativeLazyObjectsEnabled(): bool + { + return $this->attributes['nativeLazyObjects'] ?? false; + } + + 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['nativeLazyObjects'] = $nativeLazyObjects; + } + /** * To be deprecated in 3.1.0 * 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/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php index 95263ae0078..d4e0459f94c 100644 --- a/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php +++ b/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php @@ -5,10 +5,13 @@ namespace Doctrine\ORM\Mapping\PropertyAccessors; use Doctrine\ORM\Proxy\InternalProxy; +use LogicException; use ReflectionProperty; use function ltrim; +use const PHP_VERSION_ID; + /** * This is a PHP 8.4 and up only class and replaces ObjectCastPropertyAccessor. * @@ -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 31d57769065..5741a8e07cc 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; @@ -142,11 +143,11 @@ public function __construct( private readonly string $proxyNs, bool|int $autoGenerate = self::AUTOGENERATE_NEVER, ) { - if (! $proxyDir) { + if (! $proxyDir && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) { throw ORMInvalidArgumentException::proxyDirectoryRequired(); } - if (! $proxyNs) { + if (! $proxyNs && ! $em->getConfiguration()->isNativeLazyObjectsEnabled()) { throw ORMInvalidArgumentException::proxyNamespaceRequired(); } @@ -163,8 +164,23 @@ 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()->isNativeLazyObjectsEnabled()) { + $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); + }, ReflectionClass::SKIP_INITIALIZATION_ON_SERIALIZE); + + foreach ($identifier as $idField => $value) { + $classMetadata->propertyAccessors[$idField]->setValue($proxy, $value); + } + + return $proxy; + } + $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className); return $proxyFactory($identifier); @@ -182,6 +198,10 @@ public function getProxy(string $className, array $identifier): InternalProxy */ public function generateProxyClasses(array $classes, string|null $proxyDir = null): int { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { + return 0; + } + $generated = 0; foreach ($classes as $class) { diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 3d9f594c3a1..96aba64235a 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -2378,7 +2378,11 @@ public function createEntity(string $className, array $data, array &$hints = []) } if ($this->isUninitializedObject($entity)) { - $entity->__setInitialized(true); + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { + $class->reflClass->markLazyObjectAsInitialized($entity); + } else { + $entity->__setInitialized(true); + } } else { if ( ! isset($hints[Query::HINT_REFRESH]) @@ -3033,6 +3037,13 @@ public function initializeObject(object $obj): void if ($obj instanceof PersistentCollection) { $obj->initialize(); + + return; + } + + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled()) { + $reflection = $this->em->getClassMetadata($obj::class)->getReflectionClass(); + $reflection->initializeLazyObject($obj); } } @@ -3043,6 +3054,10 @@ public function initializeObject(object $obj): void */ public function isUninitializedObject(mixed $obj): bool { + if ($this->em->getConfiguration()->isNativeLazyObjectsEnabled() && ! ($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/LifecycleCallbackTest.php b/tests/Tests/ORM/Functional/LifecycleCallbackTest.php index 3dcee0e3712..59812498057 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); - $reference->postLoadCallbackInvoked = false; + 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); } diff --git a/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php b/tests/Tests/ORM/Functional/ProxiesLikeEntitiesTest.php index 0cc8776ba50..9201c3d81c8 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()->isNativeLazyObjectsEnabled()) { + 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..8184b78dd04 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; @@ -242,13 +241,16 @@ public function testInitializeProxyOnGettingSomethingOtherThanTheIdentifier(): v #[Group('DDC-1604')] public function testCommonPersistenceProxy(): void { + if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) { + self::markTestSkipped('Test only works with proxy generation disabled.'); + } + $id = $this->createProduct(); $entity = $this->_em->getReference(ECommerceProduct::class, $id); 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 +259,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..731020e9d1d 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10808Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10808Test.php @@ -32,6 +32,10 @@ protected function setUp(): void public function testDQLDeferredEagerLoad(): void { + if ($this->_em->getConfiguration()->isNativeLazyObjectsEnabled()) { + self::markTestSkipped('Test requires lazy loading to be disabled'); + } + $appointment = new GH10808Appointment(); $this->_em->persist($appointment); diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index efd380dc1de..65e9ee48ca1 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; } + $enableNativeLazyObjects = getenv('ENABLE_NATIVE_LAZY_OBJECTS'); + + if (PHP_VERSION_ID >= 80400 && $enableNativeLazyObjects) { + $config->enableNativeLazyObjects(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); + } }