Skip to content

Commit 05d5b14

Browse files
authored
Skip native lazy object initialization for unmapped properties (#2922)
1 parent a7b02fc commit 05d5b14

File tree

4 files changed

+56
-4
lines changed

4 files changed

+56
-4
lines changed

docs/en/cookbook/lookup-reference.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ need the referenced documents, you can use the ``$lookup`` stage in MongoDB's
1414
aggregation pipeline. It's similar to a SQL join, without duplication of data in
1515
the result set when there is many references to load.
1616

17+
.. note::
18+
19+
Lazy loading of references only occurs when accessing an uninitialized mapped property.
20+
If you access a property that is not mapped in Doctrine, that will not trigger
21+
loading of the referenced document.
22+
1723
Example setup
1824
-------------
1925

src/Proxy/Factory/NativeLazyObjectFactory.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
use Doctrine\Persistence\NotifyPropertyChanged;
1313
use LogicException;
1414
use ReflectionClass;
15+
use ReflectionProperty;
1516
use WeakMap;
1617

18+
use function array_key_exists;
1719
use function count;
1820

1921
use const PHP_VERSION_ID;
@@ -27,6 +29,9 @@ class NativeLazyObjectFactory implements ProxyFactory
2729
private readonly UnitOfWork $unitOfWork;
2830
private readonly LifecycleEventManager $lifecycleEventManager;
2931

32+
/** @var array<class-string, ReflectionProperty[]> */
33+
private array $skippedProperties = [];
34+
3035
public function __construct(
3136
DocumentManager $documentManager,
3237
) {
@@ -68,13 +73,40 @@ public function getProxy(ClassMetadata $metadata, $identifier): object
6873

6974
$metadata->propertyAccessors[$metadata->identifier]->setValue($proxy, $identifier);
7075

76+
foreach ($this->getSkippedProperties($metadata) as $property) {
77+
$property->skipLazyInitialization($proxy);
78+
}
79+
7180
if (isset(self::$lazyObjects)) {
7281
self::$lazyObjects[$proxy] = true;
7382
}
7483

7584
return $proxy;
7685
}
7786

87+
/** @return ReflectionProperty[] */
88+
private function getSkippedProperties(ClassMetadata $metadata): array
89+
{
90+
if (isset($this->skippedProperties[$metadata->name])) {
91+
return $this->skippedProperties[$metadata->name];
92+
}
93+
94+
$skippedProperties = [];
95+
foreach ($metadata->reflClass->getProperties() as $property) {
96+
if (array_key_exists($property->name, $metadata->propertyAccessors)) {
97+
continue;
98+
}
99+
100+
if ($property->isVirtual()) {
101+
continue;
102+
}
103+
104+
$skippedProperties[] = $property;
105+
}
106+
107+
return $this->skippedProperties[$metadata->name] = $skippedProperties;
108+
}
109+
78110
/** @internal Only for tests */
79111
public static function enableTracking(bool $enabled = true): void
80112
{

tests/Documents/DocumentWithUnmappedProperties.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44

55
namespace Documents;
66

7-
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
8-
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
7+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
98

10-
#[Document]
9+
#[ODM\Document]
1110
class DocumentWithUnmappedProperties
1211
{
13-
#[Id]
12+
#[ODM\Id]
1413
public string $id;
1514

1615
public string $foo = 'bar';
16+
17+
// We need at least one mapped field to avoid the native lazy object to be
18+
// switched to "initialized" state immediately after setting all its properties.
19+
#[ODM\Field]
20+
public string $mappedField;
1721
}

tests/Tests/Functional/ReferencesTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
1616
use Documents\Account;
1717
use Documents\Address;
18+
use Documents\DocumentWithUnmappedProperties;
1819
use Documents\Group;
1920
use Documents\Phonenumber;
2021
use Documents\Profile;
@@ -28,6 +29,15 @@
2829

2930
class ReferencesTest extends BaseTestCase
3031
{
32+
public function testSkipInitializationForUnmappedProperties(): void
33+
{
34+
$loadedDocument = $this->dm->getReference(DocumentWithUnmappedProperties::class, '123');
35+
$this->assertInstanceOf(DocumentWithUnmappedProperties::class, $loadedDocument);
36+
37+
self::assertSame('bar', $loadedDocument->foo);
38+
self::assertTrue($this->dm->isUninitializedObject($loadedDocument));
39+
}
40+
3141
public function testManyDeleteReference(): void
3242
{
3343
$user = new User();

0 commit comments

Comments
 (0)