orm icon indicating copy to clipboard operation
orm copied to clipboard

test persist an uninitialized lazy ghost

Open soyuka opened this issue 3 months ago • 10 comments

We've the use case inside API Platform where the JsonStreamer deserializes a JSON into a Lazy Ghost as while its not consumed (streaming) we don't need to initialize this object. Therefore at some point we try to persist an uninitialized lazy ghost and it'll likely throw "column title must not be null".

    public function testPersistLazyGhost(): void 
    {
        if (PHP_VERSION_ID < 80400) {
            $this->markTestSkipped('Lazy objects are only available in PHP 8.4+.');
        }

        $initialized = false;
        $reflector = new \ReflectionClass(PersistentEntity::class);
        $lazyGhost = $reflector->newLazyGhost(function (PersistentEntity $object) use (&$initialized) {
            $initialized = true;
            $object->setName('LazyGhostInitialized');
        });

        self::assertFalse($initialized, 'Lazy ghost should not be initialized before persist.');
        $this->_em->persist($lazyGhost);
        $this->_em->flush();
        self::assertTrue($initialized, 'Lazy ghost should be initialized during flush.');
        $this->_em->clear();
        $retrievedEntity = $this->_em->find(PersistentEntity::class, $lazyGhost->getId());
        self::assertNotNull($retrievedEntity);
        self::assertEquals('LazyGhostInitialized', $retrievedEntity->name);
    }

soyuka avatar Nov 29 '25 10:11 soyuka

@beberlei @greg0ire

I've spoken to @soyuka about this issue. The issue is that app tries to persist lazy objects as new records. Those are lazy objects that are not Doctrine proxies. Because we read the object's values through reflection, we don't initialize the objects and basically read null everywhere.

How shall we handle this? Should we attempt to detect lazy objects and initialize them?

derrabus avatar Dec 01 '25 15:12 derrabus

I'm sorry but I don't think I get it.

the JsonStreamer deserializes a JSON into a Lazy Ghost as while its not consumed (streaming) we don't need to initialize this object

So you're receiving JSON from an http request and stream that data into several entities, not all of which you persist, hence why you can't afford to initialize them?

greg0ire avatar Dec 02 '25 08:12 greg0ire

I can initialize it but I was wondering if Doctrine should initialize it itself if its persisted, as right now if you attempt to persist a lazy ghost entity (that wasn't created by Doctrine) then it fails and never initializes it (as doctrine uses reflection).

soyuka avatar Dec 02 '25 08:12 soyuka

I think there are at least 2 point of views:

a) If we pass an entity to the ORM, it should be able to deal with it regardless of whether it's lazy or not. It shouldn't matter that the object is lazy. b) If we pass a lazy object to the ORM, and it becomes non-lazy, that's a side effect, and it's unclear whether that side effect is OK, so maybe an exception should be thrown forbidding to force users to make their objects non-lazy explicitly, outside Doctrine.

Regardless of the answer, it seems that we should attempt to detect whether objects are lazy or not. Do we have a way to do that that is reliable and where performance won't be a concern?

greg0ire avatar Dec 02 '25 19:12 greg0ire

Regardless of the answer, it seems that we should attempt to detect whether objects are lazy or not. Do we have a way to do that that is reliable and where performance won't be a concern?

Yes, we can use reflection to detect a lazy object. And since we're already accessing the object's properties through reflection, one more check won't hurt, I guess.

derrabus avatar Dec 02 '25 21:12 derrabus

I think I lean more towards scenario b. What about you?

greg0ire avatar Dec 21 '25 09:12 greg0ire

If I pass an object to the ORM via the persist() method, I believe I do that with the intention of having it read by the ORM. So maybe initializing the object is okay or even expected?

And usually, the laziness would be broken by reading the properties. It's more or less an accident that it does not happen in our case.

derrabus avatar Dec 21 '25 14:12 derrabus

True. So how do we trigger the initialization then?

greg0ire avatar Dec 21 '25 15:12 greg0ire

Something like this?

if (\PHP_VERSION_ID > 80400) {
    $r = new \ReflectionClass($value);
    if ($r->isUninitializedLazyObject($value)) {
        $r->initializeLazyObject($value);
    }
}

https://github.com/api-platform/core/blob/45831a93c9d256d9ebecd3db13ac7db34e3778f1/src/Doctrine/Common/State/PersistProcessor.php#L116-L121

soyuka avatar Dec 22 '25 07:12 soyuka

Looks good. Please push a commit with that.

greg0ire avatar Dec 22 '25 07:12 greg0ire