mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
Bug: UnitOfWork attempts to update readonly property (object IDs don't match) #6930
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @zanbaldwin on GitHub (Feb 12, 2022).
Bug Report
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
\LogicExceptionis 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. WhenReflectionReadonlyProperty::setValue()does an identical comparison===the object IDs don't match and aLogicExceptionis thrown.How to reproduce
Set an entity column property to be both
readonlyand 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).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.I also thought about using the
updatablevalue 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.@derrabus commented on GitHub (Feb 15, 2022):
This is kind of tricky because we suddenly need a way to tell if two arbitrary objects are equal. We can add a special case for datetime objects, but as you already mentioned, this will again fail for custom userland types.
@zanbaldwin commented on GitHub (Feb 15, 2022):
Off the top of my head the only way I can think of doing this in PHP without building some kind of object comparison library (which seems overkill) is something along the lines of using
serialize.I haven't checked the performance of this and I'm sure there'll be a whole host of problems I haven't foreseen, but what's your opinion on something like this? Once execution has got to this point we know we're dealing with a read-only, initialized property that's a value object.
I can't imagine a use-case where a value object has been constructed from a Doctrine Type, and would result in serialized data that's different to the actual property.
@garak commented on GitHub (Feb 16, 2022):
What about doing a non-strict comparison for objects? I.e., using
==instead of===.@derrabus commented on GitHub (Feb 16, 2022):
Depending on the actual custom types, both approaches might or might not work. But they boil down to the UoW having an opionion on whether two arbitrary objects are equal or not. I don't think it's a good idea having that kind of guesswork in the ORM.
Maybe we should just skip initialized readonly fields when refreshing an entity after insert and document this as a known limitation?
@garak commented on GitHub (Feb 16, 2022):
What's the problem with non-strict comparison? According to the manual, it should work comparing objects values, and I think it can fit both the cases of DateTime and of unique identifiers.
@zanbaldwin commented on GitHub (Feb 16, 2022):
I've been playing around with both methods, and I'm beginning to agree with @derrabus - perhaps we should just skip initialized readonly fields?
We don't know how complex the object might be (for example, in the past I've created a custom Doctrine Type to construct
Opis\JsonSchema\Schemaobjects from ajsoncolumn). Problems with comparison arise whenever the objects being compared contain either a DateTime (those pesky milliseconds) or a closure, so while they may be functionally the same we just won't be able to detect it:While we could make a special case for
DateTimeInterfaceobjects, we wouldn't be able to do anything about them if they're nested deep in the objects properties. And closures are a big no-go:@RSickenberg commented on GitHub (Mar 21, 2022):
Same issue with
$doctrine->remove($object)with areadonly $id@nesl247 commented on GitHub (Jun 29, 2022):
@zanbaldwin your patch fixed it for me. I do hope there can be some official progress on this but until then, thanks!
@nCrazed commented on GitHub (Aug 18, 2022):
Would exposing a configuration hook for providing custom equality checks be acceptable here?
@Invis1ble commented on GitHub (Aug 8, 2024):
This needs to be fixed as soon as possible because, at the moment, we can't use readonly Value Objects as properties.
@bigfoot90 commented on GitHub (Oct 24, 2024):
Any news on this? ♥
@Arkemlar commented on GitHub (Oct 24, 2025):
The problem easaly arises when you try to lock something. Internally it leads to refresh() that attempts to update readonly properties like
uuid,createdAt, and so on.Example:
See! Both values are the same. But doctrine tries to set and fails. This forces developers to not use readonly for mapped properties, that is kind of code-uglifying IMHO.
@Arkemlar commented on GitHub (Nov 2, 2025):
@beberlei @greg0ire @derrabus I kindly ask you to react on this.
I suppose readonly property should be handled by doctrine correctly: once set - either by first hydration OR after the object has been created in userland code - it SHOULD NOT be changed ever. No matter either it value object, scalar or whatever else.
At this moment doctrine punches with LogicException and text like
Attempting to change readonly property App\...\SaleOrderLine::$uuid. This requires to sadly removereadonlyfor mapped properties of every entity that might be refreshed or used infind($id, LockMode::PESSIMISTIC_*). In latter case pessimistic locking on already managed entity leads to refreshing it (and refresh failing on readonly property).The problem resides in this line https://github.com/doctrine/orm/blob/3.5.x/src/UnitOfWork.php#L2420
Are you agree with this change?