bug in readonly properties & native lazy objects, triggers immedtiately #7552

Closed
opened 2026-01-22 15:53:17 +01:00 by admin · 8 comments
Owner

Originally created by @beberlei on GitHub (Sep 15, 2025).

Just found another bug with the readonly IDs and new PHP 8.4 ghost objects - if the ID is readonly of a related entity - when it tries to create a proxy for that related entity - the proxy is immediately initialized from the ReadonlyAccessor - it calls the reflection property->isInitialized on the ID and this triggers the initialization in the foreach right after the proxy generation. Here is the stacktrace from inside of the ghost object function to visualize which flow triggered the initialization.

image

This cascades to a very ugly and hard to trace bug - it sets the originalEntityData to empty array, which then behind the scenes ignores all changes made on that entity on flush. Just spent like 3 hours debugging to find that out. Also couldn't use debugger to trace that issue, because the xdebug itself triggers the initialization of entities so manual logs until my head spins..

Originally posted by @vuryss in https://github.com/doctrine/doctrine-website/pull/479#issuecomment-3292973817

Originally created by @beberlei on GitHub (Sep 15, 2025). Just found another bug with the readonly IDs and new PHP 8.4 ghost objects - if the ID is readonly of a related entity - when it tries to create a proxy for that related entity - the proxy is immediately initialized from the ReadonlyAccessor - it calls the reflection property->isInitialized on the ID and this triggers the initialization in the foreach right after the proxy generation. Here is the stacktrace from inside of the ghost object function to visualize which flow triggered the initialization. <img width="765" height="708" alt="image" src="https://github.com/user-attachments/assets/3cbdade7-c034-44a0-9e66-d1737013bda8" /> This cascades to a very ugly and hard to trace bug - it sets the `originalEntityData` to empty array, which then behind the scenes ignores all changes made on that entity on flush. Just spent like 3 hours debugging to find that out. Also couldn't use debugger to trace that issue, because the xdebug itself triggers the initialization of entities so manual logs until my head spins.. _Originally posted by @vuryss in https://github.com/doctrine/doctrine-website/pull/479#issuecomment-3292973817_
admin closed this issue 2026-01-22 15:53:18 +01:00
Author
Owner

@vuryss commented on GitHub (Sep 15, 2025):

Sorry I wanna correct the line numbers, because of my manual log entries they have been shifted.
The line numbers are as follows:

@vuryss commented on GitHub (Sep 15, 2025): Sorry I wanna correct the line numbers, because of my manual log entries they have been shifted. The line numbers are as follows: - ProxyFactory:211 https://github.com/doctrine/orm/blob/2ca63df90cae8e203f6f1a2b12eb3e394e3d1f21/src/Proxy/ProxyFactory.php#L211 - ReadonlyAccessor:29 https://github.com/doctrine/orm/blob/2ca63df90cae8e203f6f1a2b12eb3e394e3d1f21/src/Mapping/PropertyAccessors/ReadonlyAccessor.php#L29 - ProxyFactory:227 https://github.com/doctrine/orm/blob/2ca63df90cae8e203f6f1a2b12eb3e394e3d1f21/src/Proxy/ProxyFactory.php#L227 - UnitOfWork:2539 https://github.com/doctrine/orm/blob/2ca63df90cae8e203f6f1a2b12eb3e394e3d1f21/src/UnitOfWork.php#L2539 - SimpleObjectHydrator:176 https://github.com/doctrine/orm/blob/2ca63df90cae8e203f6f1a2b12eb3e394e3d1f21/src/Internal/Hydration/SimpleObjectHydrator.php#L176
Author
Owner

@adrav commented on GitHub (Jan 1, 2026):

Maybe related: https://github.com/doctrine/orm/issues/12334

@adrav commented on GitHub (Jan 1, 2026): Maybe related: https://github.com/doctrine/orm/issues/12334
Author
Owner

@greg0ire commented on GitHub (Jan 4, 2026):

@adrav @vuryss I've asked my LLM, it's suggesting this fix in src/Mapping/PropertyAccessors/ReadonlyAccessor.php:

   public function setValue(object $object, mixed $value): void
   {
       // Check if this is a PHP 8.4 uninitialized lazy object
       // If so, we can safely set the value without checking isInitialized
       // because calling isInitialized() would trigger initialization
       if (PHP_VERSION_ID >= 80400 
           && $this->reflectionProperty->getDeclaringClass()->isUninitializedLazyObject($object)
       ) {
           $this->parent->setValue($object, $value);
           return;
       }
       
       // For regular objects or initialized proxies, use existing logic
       if (! $this->reflectionProperty->isInitialized($object)) {
           $this->parent->setValue($object, $value);
           return;
       }
       // Rest of validation logic remains the same...
   }

I think it is fine because if $object is an initialized lazy object, then I think calling setValue() is always fine. Can you please test it and let me know if it does address the issue?

@greg0ire commented on GitHub (Jan 4, 2026): @adrav @vuryss I've asked my LLM, it's suggesting this fix in `src/Mapping/PropertyAccessors/ReadonlyAccessor.php`: ```php public function setValue(object $object, mixed $value): void { // Check if this is a PHP 8.4 uninitialized lazy object // If so, we can safely set the value without checking isInitialized // because calling isInitialized() would trigger initialization if (PHP_VERSION_ID >= 80400 && $this->reflectionProperty->getDeclaringClass()->isUninitializedLazyObject($object) ) { $this->parent->setValue($object, $value); return; } // For regular objects or initialized proxies, use existing logic if (! $this->reflectionProperty->isInitialized($object)) { $this->parent->setValue($object, $value); return; } // Rest of validation logic remains the same... } ``` I think it is fine because if `$object` is an initialized lazy object, then I think calling `setValue()` is always fine. Can you please test it and let me know if it does address the issue?
Author
Owner

@greg0ire commented on GitHub (Jan 4, 2026):

But maybe this is going to be fixed at the PHP level: https://github.com/php/php-src/pull/20831

@greg0ire commented on GitHub (Jan 4, 2026): But maybe this is going to be fixed at the PHP level: https://github.com/php/php-src/pull/20831
Author
Owner

@greg0ire commented on GitHub (Jan 6, 2026):

Todo: consider the fix described in https://github.com/php/php-src/issues/20830#issuecomment-3714381830

@greg0ire commented on GitHub (Jan 6, 2026): Todo: consider the fix described in https://github.com/php/php-src/issues/20830#issuecomment-3714381830
Author
Owner

@greg0ire commented on GitHub (Jan 6, 2026):

The fix my LLM suggested above is probably bad because

ReadonlyAccessor uses ReflectionProperty::isInitialized() and ReflectionProperty::setValue(), both of which would trigger initialization of the object.

I will try to reproduce it and fix it.

EDIT: oh but $this->parent->setValue() should result in 8cbd34c666/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php (L42), so this is probably fine.

@greg0ire commented on GitHub (Jan 6, 2026): The fix my LLM suggested above is probably bad because > ReadonlyAccessor uses ReflectionProperty::isInitialized() and ReflectionProperty::setValue(), both of which would trigger initialization of the object. I will try to reproduce it and fix it. EDIT: oh but `$this->parent->setValue()` should result in https://github.com/doctrine/orm/blob/8cbd34c6668fea94ed5cdb2d19a7aa7a405b30aa/src/Mapping/PropertyAccessors/RawValuePropertyAccessor.php#L42, so this is probably fine.
Author
Owner

@greg0ire commented on GitHub (Jan 6, 2026):

If somebody here could please test https://github.com/doctrine/orm/pull/12335 on their application, that would be great.

@greg0ire commented on GitHub (Jan 6, 2026): If somebody here could please test https://github.com/doctrine/orm/pull/12335 on their application, that would be great.
Author
Owner

@adrav commented on GitHub (Jan 9, 2026):

If somebody here could please test #12335 on their application, that would be great.

I have just tested it with my code described here: https://github.com/doctrine/orm/issues/12334

It works fine with the fix: https://github.com/doctrine/orm/pull/12335.

@adrav commented on GitHub (Jan 9, 2026): > If somebody here could please test [#12335](https://github.com/doctrine/orm/pull/12335) on their application, that would be great. I have just tested it with my code described here: https://github.com/doctrine/orm/issues/12334 It works fine with the fix: https://github.com/doctrine/orm/pull/12335.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7552