Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/en/cookbook/lookup-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------

Expand Down
32 changes: 32 additions & 0 deletions src/Proxy/Factory/NativeLazyObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +29,9 @@ class NativeLazyObjectFactory implements ProxyFactory
private readonly UnitOfWork $unitOfWork;
private readonly LifecycleEventManager $lifecycleEventManager;

/** @var array<class-string, ReflectionProperty[]> */
private array $skippedProperties = [];

public function __construct(
DocumentManager $documentManager,
) {
Expand Down Expand Up @@ -68,13 +73,40 @@ 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;
}

return $proxy;
}

/** @return ReflectionProperty[] */
private function getSkippedProperties(ClassMetadata $metadata): array
{
if (isset($this->skippedProperties[$metadata->name])) {
return $this->skippedProperties[$metadata->name];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoization!


$skippedProperties = [];
foreach ($metadata->reflClass->getProperties() as $property) {
if (array_key_exists($property->name, $metadata->propertyAccessors)) {
continue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, every mapped field should have its own property accessor according to ClassMetadata::mapField(), correct?

So this foreach look is really just checking for unmapped, non-virtual properties.

Will this logic be useful in any other contexts? If so, it may be worth capturing this logic in a ClassMetadata method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be an optimization to store the list of unmapped properties in the ClassMetadata, and leverage the cache instead of iterating over all the class properties. Also, there might be an issue if the ClassMetadata is modified after the first lazy object is created, as this list is not updated.

I'd rather keep this simple design for now.

}

if ($property->isVirtual()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pleasantly surprised that the ReflectionProperty docs explain what virtual properties are.

continue;
}

$skippedProperties[] = $property;
}

return $this->skippedProperties[$metadata->name] = $skippedProperties;
}

/** @internal Only for tests */
public static function enableTracking(bool $enabled = true): void
{
Expand Down
10 changes: 10 additions & 0 deletions tests/Tests/Functional/ReferencesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a check using ReflectionClass to ensure this is actually still not initialised?

self::assertSame('bar', $loadedDocument->foo);
}

public function testManyDeleteReference(): void
{
$user = new User();
Expand Down