From b4022ab74ab28733c7da6ec8733e70e5146575b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 21 Nov 2025 22:01:58 +0100 Subject: [PATCH 1/2] Skip native lazy object initualisation for unmapped properties --- src/Proxy/Factory/NativeLazyObjectFactory.php | 32 +++++++++++++++++++ tests/Tests/Functional/ReferencesTest.php | 10 ++++++ 2 files changed, 42 insertions(+) diff --git a/src/Proxy/Factory/NativeLazyObjectFactory.php b/src/Proxy/Factory/NativeLazyObjectFactory.php index 35552cfb6..0608f1e43 100644 --- a/src/Proxy/Factory/NativeLazyObjectFactory.php +++ b/src/Proxy/Factory/NativeLazyObjectFactory.php @@ -12,8 +12,10 @@ use Doctrine\Persistence\NotifyPropertyChanged; use LogicException; use ReflectionClass; +use ReflectionProperty; use WeakMap; +use function array_key_exists; use function count; use const PHP_VERSION_ID; @@ -27,6 +29,9 @@ class NativeLazyObjectFactory implements ProxyFactory private readonly UnitOfWork $unitOfWork; private readonly LifecycleEventManager $lifecycleEventManager; + /** @var array */ + private array $skippedProperties = []; + public function __construct( DocumentManager $documentManager, ) { @@ -68,6 +73,10 @@ public function getProxy(ClassMetadata $metadata, $identifier): object $metadata->propertyAccessors[$metadata->identifier]->setValue($proxy, $identifier); + foreach ($this->getSkippedProperties($metadata) as $property) { + $property->skipLazyInitialization($proxy); + } + if (isset(self::$lazyObjects)) { self::$lazyObjects[$proxy] = true; } @@ -75,6 +84,29 @@ public function getProxy(ClassMetadata $metadata, $identifier): object return $proxy; } + /** @return ReflectionProperty[] */ + private function getSkippedProperties(ClassMetadata $metadata): array + { + if (isset($this->skippedProperties[$metadata->name])) { + return $this->skippedProperties[$metadata->name]; + } + + $skippedProperties = []; + foreach ($metadata->reflClass->getProperties() as $property) { + if (array_key_exists($property->name, $metadata->propertyAccessors)) { + continue; + } + + if ($property->isVirtual()) { + continue; + } + + $skippedProperties[] = $property; + } + + return $this->skippedProperties[$metadata->name] = $skippedProperties; + } + /** @internal Only for tests */ public static function enableTracking(bool $enabled = true): void { diff --git a/tests/Tests/Functional/ReferencesTest.php b/tests/Tests/Functional/ReferencesTest.php index d2f1761b9..764bd6d96 100644 --- a/tests/Tests/Functional/ReferencesTest.php +++ b/tests/Tests/Functional/ReferencesTest.php @@ -15,6 +15,7 @@ use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\Account; use Documents\Address; +use Documents\DocumentWithUnmappedProperties; use Documents\Group; use Documents\Phonenumber; use Documents\Profile; @@ -28,6 +29,15 @@ class ReferencesTest extends BaseTestCase { + public function testSkipInitializationForUnmappedProperties(): void + { + $loadedDocument = $this->dm->getReference(DocumentWithUnmappedProperties::class, '123'); + $this->assertInstanceOf(DocumentWithUnmappedProperties::class, $loadedDocument); + + // Accessing unmapped property should not initialize the document + self::assertSame('bar', $loadedDocument->foo); + } + public function testManyDeleteReference(): void { $user = new User(); From 68b08e8d62a0f6a64262f899fa3d9a68b64dab34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 21 Nov 2025 22:42:27 +0100 Subject: [PATCH 2/2] Update documentation --- docs/en/cookbook/lookup-reference.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/cookbook/lookup-reference.rst b/docs/en/cookbook/lookup-reference.rst index 5f7fe8dd8..6598c98a0 100644 --- a/docs/en/cookbook/lookup-reference.rst +++ b/docs/en/cookbook/lookup-reference.rst @@ -14,6 +14,12 @@ need the referenced documents, you can use the ``$lookup`` stage in MongoDB's aggregation pipeline. It's similar to a SQL join, without duplication of data in the result set when there is many references to load. +.. note:: + + Lazy loading of references only occurs when accessing an uninitialized mapped property. + If you access a property that is not mapped in Doctrine, that will not trigger + loading of the referenced document. + Example setup -------------