-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: UnitOfWork attempts to update readonly property (object IDs don't match) #9505
Comments
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. |
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 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. diff --git i/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php w/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php
index fba314be1..7cfe2b545 100644
--- i/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php
+++ w/lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
+use Exception;
use InvalidArgumentException;
use LogicException;
use ReflectionProperty;
@@ -12,6 +13,7 @@ use function assert;
use function func_get_args;
use function func_num_args;
use function is_object;
+use function serialize;
use function sprintf;
/**
@@ -45,6 +47,13 @@ final class ReflectionReadonlyProperty extends ReflectionProperty
assert(is_object($objectOrValue));
if (parent::getValue($objectOrValue) !== $value) {
+ try {
+ if (is_object($value) && serialize(parent::getValue($objectOrValue)) === serialize($value)) {
+ return;
+ }
+ } catch (Exception $e) {
+ }
+
throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));
}
} 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. |
What about doing a non-strict comparison for objects? I.e., using |
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? |
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. |
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 $mysqlFormatString = 'Y-m-d H:i:s';
// Constructed by userland-code.
$userlandDate = new DateTime;
// Constructed by DateTimeType on MySQL platform.
$databaseDate = DateTime::createFromFormat($mysqlFormatString, $userlandDate->format($mysqlFormatString));
var_dump($userlandDate == $databaseDate); // bool(false)
var_dump(serialize($userlandDate) === serialize($databaseDate)); // bool(false) While we could make a special case for class ContainsClosure {
private \Closure $closure;
public function __construct() {
$this->closure = function (string $bar) {
return $bar;
};
}
}
var_dump(new ContainsClosure == new ContainsClosure); // bool(false)
var_dump(serialize(new ContainsClosure) === serialize(new ContainsClosure)); // throws \Exception |
Same issue with |
@zanbaldwin your patch fixed it for me. I do hope there can be some official progress on this but until then, thanks! |
Would exposing a configuration hook for providing custom equality checks be acceptable here? |
This needs to be fixed as soon as possible because, at the moment, we can't use readonly Value Objects as properties. |
Any news on this? ♥ |
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
\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. WhenReflectionReadonlyProperty::setValue()
does an identical comparison===
the object IDs don't match and aLogicException
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).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
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.The text was updated successfully, but these errors were encountered: