#9505·orm

Bug: UnitOfWork attempts to update readonly property (object IDs don't match)

Author: zanbaldwinCreated Feb 12, 2022Updated Sep 9, 2026
LabelsBug

Bug Report

Q A
BC Break no
ORM Version 2.11.x
PHP Version 8.1+

Summary

Found a bug where UnitOfWork attempts to update a readonly property of a type that is an object not managed by Doctrine (eg, value object). a \LogicException is thrown.

Current behavior

Even though the data has not changed, the value object inside the managed entity (held inside UnitOfWork::$identityMap) is a different instance to the value object constructed from the data/row fetched from the database. When ReflectionReadonlyProperty::setValue() does an identical comparison === the object IDs don't match and a LogicException is thrown.

How to reproduce

Set an entity column property to be both readonly and type-hinted to be an object that is not an entity managed by the object manager (eg, Symfony\Component\Uid\Ulid, App\ValueObject\Email, \DateTimeImmutable, etc).

php
#[ORM\Entity]
class Test1
{
    #[ORM\Id]
    #[ORM\Column(name: 'id', type: 'integer', nullable: false)]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    public readonly int $id;

    public function __construct(
        #[ORM\Column(name: 'date', type: 'datetime', nullable: false)]
        public readonly \DateTimeImmutable $date = new \DateTime,
    ) {}
}
php
$entityManager->persist($entity = new TestEntity);
$entityManager->flush();
$entityManager->refresh($entity);

Expected behavior

Read-only properties should never be written to after initialization.

However, it's logical to assume that someone is calling ObjectManager::refresh() because they expect the data to have changed in the database. In this case, the only way I can think to fetch that updated data would be to detach the managed entity first before loading again.

Workaround

I've temporarily avoided this issue by not resetting readonly properties that have already been initialized, but it's not a solution since it purposefully avoids performing that value comparison inside ReflectionReadonlyProperty.

diff
index cd5d72716..aadc82d32 100644
--- a/lib/Doctrine/ORM/UnitOfWork.php
+++ b/lib/Doctrine/ORM/UnitOfWork.php
@@ -2740,7 +2740,11 @@ class UnitOfWork implements PropertyChangedListener
         }
 
         foreach ($data as $field => $value) {
-            if (isset($class->fieldMappings[$field])) {
+            if (isset($class->fieldMappings[$field]) && (
+                !method_exists($class->reflFields[$field], 'isReadOnly')
+                || !$class->reflFields[$field]->isReadOnly()
+                || !$class->reflFields[$field]->isInitialized($entity)
+            )) {
                 $class->reflFields[$field]->setValue($entity, $value);
             }
         }

I also thought about using the updatable value from the column field mapping data, or using $this->fieldMappings[$field]['notUpdatable'], but I'm unsure how this would affect setting the value of generated columns.