diff --git a/docs/en/cookbook/lookup-reference.rst b/docs/en/cookbook/lookup-reference.rst index e2c362dc4..682ae5156 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 ------------- 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/Documents/DocumentWithUnmappedProperties.php b/tests/Documents/DocumentWithUnmappedProperties.php index 06339e79b..e338fc511 100644 --- a/tests/Documents/DocumentWithUnmappedProperties.php +++ b/tests/Documents/DocumentWithUnmappedProperties.php @@ -4,14 +4,18 @@ namespace Documents; -use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; -use Doctrine\ODM\MongoDB\Mapping\Annotations\Id; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; -#[Document] +#[ODM\Document] class DocumentWithUnmappedProperties { - #[Id] + #[ODM\Id] public string $id; public string $foo = 'bar'; + + // We need at least one mapped field to avoid the native lazy object to be + // switched to "initialized" state immediately after setting all its properties. + #[ODM\Field] + public string $mappedField; } diff --git a/tests/Tests/Functional/ReferencesTest.php b/tests/Tests/Functional/ReferencesTest.php index d2f1761b9..8a04113b0 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); + + self::assertSame('bar', $loadedDocument->foo); + self::assertTrue($this->dm->isUninitializedObject($loadedDocument)); + } + public function testManyDeleteReference(): void { $user = new User();