Updated: Comment #N
Problem
- Current set() and onChange() implementations are inconsistent in how they deal with notifications.
- Current onChange() implementations do not consistently make updates first and forward notifications to the parent afterwards.
- As #2050201: Unsetting field item properties via FieldItemBase::set() does not always work shows __unset() currently has to work around a problem of set(), which shouldn't be necessary
- set() implementations do not honour the interfaces return value
- In certain circumstances current entity reference items may become un-synced and return *wrong* values, due to a bug in the existing onChange logic. It happens when you do $entity2->user_id->first()->get('target_id')->setValue($new_user2->id());
on a fresh entity. This needs to be fixed and have test coverage.
Let's clean this up by implementing the methods consistently, such that contrib has an established pattern to follow.
Proposed resolution
- Clean-up onChange() logic to consistently account for changes first and forward notifications second.
- Improve set() return value to be consistent with others and match the docs (small API change, but it was not following the documented API previously anyway)
- Fix the entityreference-item onChange logic bug and add test coverage for it.
Remaining tasks
-
User interface changes
-
API changes
$field_item->set('name', 'value') has now $field_item as return value, instead of its typed data name property. However, the previous return value was not implemented at all, so no can be making use of that API right now.
Related Issues
#2050201: Unsetting field item properties via FieldItemBase::set() does not always work
#2019055: Switch from field-level language fallback to entity-level language fallback
Original report by fago
As the recent patch in #2019055: Switch from field-level language fallback to entity-level language fallback has shown the current way of handling onChange() code leads to duplicated code. Right now onChange() is inlined when set() is called, but reactions need to be run in the same class.
To avoid maintenance problems I'd suggest we always call onChange instead and remove the micro-optimization of lining that in set(). That way we don't duplicated that code, what's bad even if it's just one line as it's easy to miss - what is way more important than the probably unnecessary micro-optimization (on write).
Besides that, the patch fixed different set() and onChange implementations to work in a consistent manner. Also, the documented return of set() is weird, but wasn't respected at all - so I improved that and fixed it everywhere.
Comment | File | Size | Author |
---|---|---|---|
#27 | d8_typed_onchange_unify.interdiff.txt | 3.32 KB | fago |
#27 | d8_typed_onchange_unify.patch | 20.93 KB | fago |
#24 | d8_typed_onchange_unify.interdiff.txt | 2.48 KB | fago |
#24 | d8_typed_onchange_unify.patch | 20.56 KB | fago |
#20 | d8_typed_onchange_unify.patch | 20.35 KB | fago |
Comments
Comment #2
yched CreditAttribution: yched commentedA bit above my head ;-), but looks like it makes sense.
Just wondering: some param / var names are $notify, others $notify_parent - and the distinction is really welcome to help make sense of the code flow :-)
I does seem like some (most ?) $notify vars are indeed about "notify self" rather that "notify parent", but I'm not sure that's the case for all.
Mapping & FieldItemBase::set($notify) seems to be about notifying the parent ?
So I'm wondering if the other $notify vars would need double checking too ?
Other than that - is "@return self" a thing now ?
Comment #3
yched CreditAttribution: yched commentedThis gets a bit hair pulling - so the $notify_parent param is about notifying the parent's parent ?
Comment #4
fago>Other than that - is "@return self" a thing now ?
Afaik, yes.
Good point. $notify is always about notifying the parent, so that should be just $notify.
Howsoever, I realized that this approach started by ContentEntityBase::set() is wrong. The intention of notify == FALSE is exactly to be able to skip that synchronization stuff, so should do ContentEntityBase::set(). I.e., the patch unifies that set() methods to properly follow $notify.
Then, I looked at why the EntityTranslation tests failed. Turns out there were some subtle situations in which the FieldItemBase logic of retrieving the value from $this->values vs $this->properties was buggy - as shown by #2050201: Unsetting field item properties via FieldItemBase::set() does not always work. So attached patch attempts to clean set() and notify() logic up + fix that bugs by a more robust onChange() implementation.
Problem with onChange() has been that parents should be notified at the end of the method (i.e. when the property has reacted properly), while re-using parent methods should happen at the beginning such that we can rely on parent reactions already and $this->values and $this->properties are synched. Thus, attached patch makes use of an optional $notify parameter for onChange(), which can be set to FALSE by child classes when calling the parent. This allows re-using parent onChange() implementations without having them to notify parent objects too early.
Updated patch adds in the fix from #2050201: Unsetting field item properties via FieldItemBase::set() does not always work and comes with improved test coverage. I've also added a test-case specifically for the EntityTranslationTest fails, which are already present in HEAD when using the typed data API to set a value (see test case).
I'm attached the complete patch + the test only (should fail).
Comment #5
fagoComment #7
fago4: d8_set_notify.patch queued for re-testing.
Comment #9
fagoRan into this while working on #2386559: ERItem::setValue(array('entity' => $entity) produces broken Items. This still needs a clean-up. Bumping to major as there should be a clear pattern for contrib to follow.
Not sure about the API change, we need to re-evaluate whether it still makes sense. Quicky uploading my clean-up changes from #2386559: ERItem::setValue(array('entity' => $entity) produces broken Items to see whether this has problems.
Comment #10
fagoUpdated patch to include previous test coverage and account for set() improvements.
Comment #11
fagoUpdated issue summary.
Comment #13
fagoTurns out schema checks are really good in catching left-over keys now, which the removed unset() "created". Updated patch to account for unsetting non-defined properties.
Comment #14
fagoForgot to attach the interdiff.
Comment #15
fagoNo, did not forget it - d.o. was silently eating it :)
Comment #16
yched CreditAttribution: yched commentedLooks good - minor remarks :
I think in other similar places we tend to order the cases the other way :
Right, first() auto-creating the item is something I'd really want to get rid of in #2164601: Stop auto-creating FieldItems on mere reading of $entity->field[N], but this is not a blocker here :-)
Not sure I get this - this assigns a target_id in cerate(), and the assigns the same target_id with setValue() ? why ?
Comment #17
fago1. ok, moved it around.
3. Right - I got it wrong when taking it over from the old patch. Thanks a lot for catching this, because it was the actual coverage for the bug which lead to the creation of this issue. For fixing it, I had to re-apply more of the original patch which adds the optional $notify parameter to onChange() to allow re-using it. I also re-applied to hunk to remove the micro-optimization in ContentEntityBase::set() to ensure it won't become accidentially unsynced with future changes.
Comment #18
fagoFurther looked into all the change logic flow together with yched, to make sure it's sound and easier to follow. First off, we can save some headache by consistently always respecting a property object first, if there is one. Then, we figured that you easily run into endless loops with calling set() from onChange now and fixed that by introducing an internal writePropertyValues() helper that you may use in onChange.
Comment #20
fagoLooks like it was one unset() too much.
Also opened issues for the things we ran into while working on this.
Comment #21
yched CreditAttribution: yched commented:-)
Comment #22
yched CreditAttribution: yched commentedPartial review :
We do nothing with the $notify param, and never notify our parents ?
Shouldn't this be $this->onChange($name, $notify) ?
+ For clarity, I'd suggest the following commenting :
Comment is a bit unclear - suggestion : "If either a scalar or an object was passed as the value for the item, assign it to the 'entity' property since that works for both cases " ?
$map->set($property, $value) notifies $map->onChange() for the property that was changed,
but yet $map->setValue($values_for_properties) doesn't notify $map->onChange() for the various properties that were changed ?
What's the reasoning ? setValue() operates on the scope of all properties that are being written, and can thus do whatever reaction is needed without needing separate onChange() reactions on each individual property ?
Comment #23
yched CreditAttribution: yched commentedThat onChange() implementation calls $this->set($property, $value, FALSE) - shouldn't it call $this->writePropertyValue() instead ?
Comment #24
fagoad 1. $notify is useless in this scenario and is about to be removed by #2388501: $notify in FieldableEntityInterface::set() is useless, so updated it to be
setValue() is supposed to receive a $values / $item which is already in a consistent state, so there should be no need for handling changes to individual properties. We opt to do so for the special case of entity references where we want to support setting it with a $item that does not contain all properties though - so it's required for that special case only.
indeed - missed that, thanks.
Comment #25
yched CreditAttribution: yched commentedContentEntityBase - "there is no parent to notify anyway" : doh, of course. Self-slap time.
Comment #26
yched CreditAttribution: yched commentedThe phpdoc is already in Map, it should be @inheritdoc here.
What we should document here, on the other hand, is how FieldItemBase's logic for $this->values & $this->properties is different from the one in Map - which is the reason why we override writePropertyValue().
Also, minor : code order is weird - could we put this override below the override of setValue(), instead of between __get() and __set() ?
And here we could document that this is the method that should be used by onChange() implementations instead of set(), because set() calls onChange().
"By passing FALSE overriding implementations can re-use the logic of parent classes without triggering the parent notification."
A bit cryptic :-)
- would be easier to parse with a comma after "By pasing FALSE, ..."
- "overriding implementations can ..." : implementations of what ?
Nitpick : logically, would make a bit more sense as
if ($notify && isset($this->parent))
?"If we're asked to notify and there is actually something to notify..."
Comment #27
fagoThanks, addressed the remarks.
Comment #28
yched CreditAttribution: yched commentedThanks @fago.
Sorry for being thick, I can't honestly say I really understand it better.
Not a blocker for RTBC, but maybe we can discuss live see if we can clarify it a bit more ? :-)
Comment #29
plachThis is blocking a critical issue.
Comment #30
alexpottGiven
(from issue summary) I do not think we need a CR here. This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 9f04e89 and pushed to 8.0.x. Thanks!