From 498b5f6286245224f27d7ee22e28b2407dd0ef9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 21 Nov 2025 23:17:13 +0100 Subject: [PATCH 1/2] Add onReference event to modify unmapped properties of lazy objects --- docs/en/reference/events.rst | 7 +++++++ src/Events.php | 12 ++++++++++++ src/Proxy/Factory/LazyGhostProxyFactory.php | 12 +++++++----- src/Proxy/Factory/NativeLazyObjectFactory.php | 2 ++ src/Proxy/Factory/StaticProxyFactory.php | 1 + src/Utility/LifecycleEventManager.php | 5 +++++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index e67b56d6af..6f99c60176 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -158,6 +158,13 @@ the life-time of their registered documents. ``postLoad`` - The postLoad event occurs for a document after the document has been loaded into the current DocumentManager from the database or after the refresh operation has been applied to it. +- + ``onReference`` - The onReference event occurs when a lazy document reference + is created. The data of the document are not loaded yet at this point; if + you access any mapped field of the document, a database query will be + triggered to load the document data. Which defeats the purpose of using + lazy references in the first place. + This can be used to inject dependencies into unmapped properties. - ``loadClassMetadata`` - The loadClassMetadata event occurs after the mapping metadata for a class has been loaded from a mapping source diff --git a/src/Events.php b/src/Events.php index b19c7f3afa..646a6f8125 100644 --- a/src/Events.php +++ b/src/Events.php @@ -86,6 +86,18 @@ private function __construct() */ public const postLoad = 'postLoad'; + /** + * The onReference event occurs when a lazy document reference is created. + * + * Note that the data of the document are not loaded yet at this point; if + * you access any mapped field of the document, a database query will be + * triggered to load the document data. Which defeats the purpose of using + * lazy references in the first place. + * + * This is a document lifecycle event. + */ + public const onReference = 'onReference'; + /** * The loadClassMetadata event occurs after the mapping metadata for a class * has been loaded from a mapping source (annotations/xml). diff --git a/src/Proxy/Factory/LazyGhostProxyFactory.php b/src/Proxy/Factory/LazyGhostProxyFactory.php index a8e5246a38..97cb1d6eea 100644 --- a/src/Proxy/Factory/LazyGhostProxyFactory.php +++ b/src/Proxy/Factory/LazyGhostProxyFactory.php @@ -230,18 +230,20 @@ private function getProxyFactory(string $className): Closure $reflector = $reflector->getParentClass(); } - $className = $class->getName(); // aliases and case sensitivity - $entityPersister = $this->uow->getDocumentPersister($className); - $initializer = $this->createLazyInitializer($class, $entityPersister); - $proxyClassName = $this->loadProxyClass($class); + $className = $class->getName(); // aliases and case sensitivity + $entityPersister = $this->uow->getDocumentPersister($className); + $initializer = $this->createLazyInitializer($class, $entityPersister); + $proxyClassName = $this->loadProxyClass($class); + $lifecycleEventManager = $this->lifecycleEventManager; - $proxyFactory = Closure::bind(static function (mixed $identifier) use ($initializer, $skippedProperties, $class): InternalProxy { + $proxyFactory = Closure::bind(static function (mixed $identifier) use ($initializer, $skippedProperties, $class, $lifecycleEventManager): InternalProxy { /** @see LazyGhostTrait::createLazyGhost() */ $proxy = static::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { $initializer($object, $identifier); }, $skippedProperties); $class->setIdentifierValue($proxy, $identifier); + $lifecycleEventManager->onReference($proxy); return $proxy; }, null, $proxyClassName); diff --git a/src/Proxy/Factory/NativeLazyObjectFactory.php b/src/Proxy/Factory/NativeLazyObjectFactory.php index 35552cfb6d..640820a17f 100644 --- a/src/Proxy/Factory/NativeLazyObjectFactory.php +++ b/src/Proxy/Factory/NativeLazyObjectFactory.php @@ -72,6 +72,8 @@ public function getProxy(ClassMetadata $metadata, $identifier): object self::$lazyObjects[$proxy] = true; } + $this->lifecycleEventManager->onReference($proxy); + return $proxy; } diff --git a/src/Proxy/Factory/StaticProxyFactory.php b/src/Proxy/Factory/StaticProxyFactory.php index fdf7b47963..0e95516a9e 100644 --- a/src/Proxy/Factory/StaticProxyFactory.php +++ b/src/Proxy/Factory/StaticProxyFactory.php @@ -61,6 +61,7 @@ public function getProxy(ClassMetadata $metadata, $identifier): GhostObjectInter ); $metadata->setIdentifierValue($ghostObject, $identifier); + $this->lifecycleEventManager->onReference($ghostObject); return $ghostObject; } diff --git a/src/Utility/LifecycleEventManager.php b/src/Utility/LifecycleEventManager.php index 4214c01568..38360bf1e3 100644 --- a/src/Utility/LifecycleEventManager.php +++ b/src/Utility/LifecycleEventManager.php @@ -134,6 +134,11 @@ public function postUpdate(ClassMetadata $class, object $document, ?Session $ses $this->cascadePostUpdate($class, $document, $session); } + public function onReference(object $document): void + { + $this->evm->dispatchEvent(Events::onReference, new LifecycleEventArgs($document, $this->dm)); + } + /** * Invokes prePersist callbacks and events for given document. * From f666fa19b856be275c21c812a7910d0b7959e04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 21 Nov 2025 23:25:22 +0100 Subject: [PATCH 2/2] Add test --- tests/Tests/Events/OnReferenceEvent.php | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/Tests/Events/OnReferenceEvent.php diff --git a/tests/Tests/Events/OnReferenceEvent.php b/tests/Tests/Events/OnReferenceEvent.php new file mode 100644 index 0000000000..1d74ad7c5d --- /dev/null +++ b/tests/Tests/Events/OnReferenceEvent.php @@ -0,0 +1,32 @@ +dm->getEventManager()->addEventListener(Events::onReference, $listener = new class { + public object $eventArgs; + + public function onReference($eventArgs): void + { + $this->eventArgs = $eventArgs; + } + }); + + $this->dm->getReference(User::class, '123456789012345678901234'); + + self::assertTrue(isset($listener->eventArgs), 'onReference event was not dispatched'); + self::assertInstanceOf(LifecycleEventArgs::class, $listener->eventArgs); + self::assertInstanceOf(User::class, $listener->eventArgs->getObject()); + self::assertTrue($this->dm->isUninitializedObject($listener->eventArgs->getObject())); + } +}