Allow custom type objects to trigger value updates - #3017
Conversation
UnitOfWork::doMerge() iterates over all reflection properties of a class, including static ones, which are never mapped fields. Since RuntimeReflectionProperty::getValue($document) returns null for static properties, the merge assigned null back and crashed with a TypeError on non-nullable typed static properties. Fixes doctrine#3013
GromNaN
left a comment
There was a problem hiding this comment.
Could you have a look at the failing tests?
|
You've been faster light for review! Thanks. Is it considered to add a |
|
Thanks to SPL there is now a way to detect the object content update which should do the trick. |
GromNaN
left a comment
There was a problem hiding this comment.
Thanks for the contribution and the test that clearly reproduces the issue! I have a few comments below.
| use function method_exists; | ||
| use function preg_match; | ||
| use function serialize; | ||
| use function spl_object_hash; |
There was a problem hiding this comment.
spl_object_hash($a) === spl_object_hash($b) is functionally identical to $a === $b for two simultaneously live objects (PHP docs: the hash can only be reused after the object is destroyed). This import can be dropped.
| if ($actualValue instanceof PersistentCollectionInterface && (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue))) { | ||
| // consider dirty collections as changed as well | ||
| continue; | ||
| } | ||
|
|
||
| if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) { | ||
| // consider dirty collections as changed as well | ||
| // If object content has not been modified, nothing to do. | ||
| if (is_object($orgValue) && spl_object_hash($actualValue) === spl_object_hash($orgValue)) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
This restructuring introduces a regression for dirty ReferenceMany collections. When a PersistentCollection is dirty but its instance is the same object stored in $originalDocumentData ($orgValue === $actualValue — the normal case), the new code falls through to line 777 and executes continue, silently skipping the dirty collection instead of detecting it as changed.
EmbedMany collections are unaffected by luck — they go through the association changeset loop (~line 870). ReferenceMany collections are explicitly excluded there (if (isset($mapping['reference'])) { continue; }), so this block is their only detection path.
PR #3021 adds a test that demonstrates this: it passes on 2.17.x and fails with this patch applied.
Also, the comment // consider dirty collections as changed as well is now on the continue of the clean collection branch — the opposite of what it says.
Since spl_object_hash is equivalent to === for live objects, it does not help with value comparison either. I'd suggest keeping the original structure and adding a == check as a new elseif. PHP's == compares objects by value: same class and all properties (public and private) equal — https://3v4l.org/sBNOH:
// skip if value has not changed
if ($orgValue === $actualValue) {
if (! $actualValue instanceof PersistentCollectionInterface) {
continue;
}
if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
// consider dirty collections as changed as well
continue;
}
} elseif (is_object($orgValue) && is_object($actualValue) && $orgValue == $actualValue) {
continue;
}This preserves the dirty-collection behaviour and fixes the value object case without any new dependency. The TODO on Comparable remains valid for edge cases where == is not sufficient, but is not a blocker.
| { | ||
| } | ||
|
|
||
| class CustomValueObjectChild extends Type |
There was a problem hiding this comment.
CustomValueObjectChild implements no convertToPHPValue/convertToDatabaseValue, so the test doesn't quite cover a realistic custom type scenario where the type actually converts values between PHP and MongoDB.
| $changeSet = $this->uow->getDocumentChangeSet($root); | ||
|
|
||
| self::assertArrayHasKey('child', $changeSet); | ||
| self::assertEquals('12', $changeSet['child'][1]->prop1); |
There was a problem hiding this comment.
prop1 is declared int, so this should compare against 12 (no quotes). The assertion should use assertSame.
Custom types are not correctly supported due to the fact that
UnitOfWork::computeOrRecomputeChangeSet()uses===operations without checking the objects inner values.This leads to missed update values.
The added test
testValueObjectChangeSetsis red if the changeset detection fragment is not altered.There is also a
@TODOadded to track the suggestion to use\Doctrine\Common\Comparableinterface. The problem is that this interface is part of doctrine/commons which is not required by the package at the moment.