Bug: UnitOfWork attempts to update readonly property (object IDs don't match)
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).
#[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,
) {}
}$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.
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.
Source: doctrine/orm