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.

#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.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Status: Needs review » Needs work

The last submitted patch, d8_set_notify.patch, failed testing.

yched’s picture

A 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 ?

yched’s picture

TextItemBase.php:
public function onChange($property_name, $notify_parent = TRUE) {
  // ...
  parent::onChange($property_name, $notify_parent);
}

This gets a bit hair pulling - so the $notify_parent param is about notifying the parent's parent ?

fago’s picture

Title: Improve onChange() code to avoid code duplication » Clean-up typed data set() and onChange() logic
Issue summary: View changes
Issue tags: +API change
FileSize
5.34 KB
16.71 KB

>Other than that - is "@return self" a thing now ?

Afaik, yes.

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 :-)

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).

fago’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 4: d8_set_notify.tests-only.patch, failed testing.

fago’s picture

Status: Needs work » Needs review

4: d8_set_notify.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 4: d8_set_notify.patch, failed testing.

fago’s picture

Priority: Normal » Major
Status: Needs work » Needs review
FileSize
2.32 KB

Ran 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.

fago’s picture

Updated patch to include previous test coverage and account for set() improvements.

fago’s picture

Title: Clean-up typed data set() and onChange() logic » Typed data does not handle set() and onChange() consistently
Category: Task » Bug report
Issue summary: View changes

Updated issue summary.

Status: Needs review » Needs work

The last submitted patch, 10: d8_typed_onchange_unify.patch, failed testing.

fago’s picture

Status: Needs work » Needs review
FileSize
10.56 KB

Turns 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.

fago’s picture

Issue tags: +DrupalCamp Ghent 2014

Forgot to attach the interdiff.

fago’s picture

No, did not forget it - d.o. was silently eating it :)

yched’s picture

Looks good - minor remarks :

  1. +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php
    @@ -190,23 +191,26 @@ public function __isset($name) {
    +    // Explicitly unset the property in $this->values if a non-defined
    +    // property is unset, such that its key is removed from $this->values.
    +    if (!$this->definition->getPropertyDefinition($name)) {
    +      unset($this->values[$name]);
    +    }
    +    else {
    +      $this->set($name, NULL);
    +    }
    

    I think in other similar places we tend to order the cases the other way :

    if (official field/property) {
      do this
    }
    else {
      do that
    }
    
  2. +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
    @@ -148,6 +148,34 @@ protected function doTestReadWrite($entity_type) {
    +    $entity->user_id->first()->get('entity')->setValue($new_user2);
    ...
    +    $entity->user_id->first()->get('target_id')->setValue($new_user2->id());
    ...
    +    $entity->name->first()->get('value')->setValue(NULL);
    +    $entity->user_id->first()->get('target_id')->setValue(NULL);
    

    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 :-)

  3. +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
    @@ -148,6 +148,34 @@ protected function doTestReadWrite($entity_type) {
    +    $entity2 = entity_create($entity_type, array(
    +      'user_id' => array('target_id' => $new_user2->id()),
    +    ));
    +    // Access the property object, and set a value.
    +    $entity2->user_id->first()->get('target_id')->setValue($new_user2->id());
    

    Not sure I get this - this assigns a target_id in cerate(), and the assigns the same target_id with setValue() ? why ?

fago’s picture

1. 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.

fago’s picture

Further 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.

Status: Needs review » Needs work

The last submitted patch, 18: d8_typed_onchange_unify.patch, failed testing.

fago’s picture

Looks like it was one unset() too much.

Also opened issues for the things we ran into while working on this.

yched’s picture

yched’s picture

Partial review :

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -375,9 +375,9 @@ protected function getTranslatedField($name, $langcode) {
       public function set($name, $value, $notify = TRUE) {
    -    // If default language or an entity key changes we need to react to that.
    -    $notify = $name == 'langcode' || in_array($name, $this->getEntityType()->getKeys());
    -    $this->get($name)->setValue($value, $notify);
    +    $this->get($name)->setValue($value, FALSE);
    +    // Directly notify ourselves, so we can take changes into account.
    +    $this->onChange($name);
    

    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 :

    // Assign the value on the child, making sure this does not notify ourselves or our parents yet.
    $this->get($name)->setValue($value, FALSE);
    // Then notify ourselves, bubbling up to our parents if specified by the $notify parameter.
    $this->onChange($name, $notify);
    
  2. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
    @@ -140,14 +140,10 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
    +    // Treat the values as property value of the entity property, if no array
    +    // is given as this handles entity ids and objects.
         if (isset($values) && !is_array($values)) {
    ...
    +      $this->set('entity', $values, $notify);
         }
    

    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 " ?

  3. Maybe you have already explained this to me live, but :
    $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 ?

yched’s picture

+++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
@@ -59,12 +59,7 @@ public function isEmpty() {
+  public function onChange($property_name, $notify = TRUE) {

That onChange() implementation calls $this->set($property, $value, FALSE) - shouldn't it call $this->writePropertyValue() instead ?

fago’s picture

ad 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

    // Assign the value on the child and overrule notify such that we get
    // notified to handle changes afterwards. We can ignore notify as there is
    // no parent to notify anyway.
    $this->get($name)->setValue($value, TRUE);
$map->setValue($values_for_properties) doesn't notify $map->onChange() for the various properties that were changed

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.

That onChange() implementation calls $this->set($property, $value, FALSE) - shouldn't it call $this->writePropertyValue() instead ?

indeed - missed that, thanks.

yched’s picture

ContentEntityBase - "there is no parent to notify anyway" : doh, of course. Self-slap time.

yched’s picture

  1. +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php
    @@ -139,32 +123,32 @@ public function setValue($values, $notify = TRUE) {
    +   * Writes the value of a property without handling changes.
    +   *
    +   * @param string $property_name
    +   *   The name of the property to be written.
    +   * @param $value
    +   *   The value to set.
        */
    -  public function set($property_name, $value, $notify = TRUE) {
    +  protected function writePropertyValue($property_name, $value) {
    

    The 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() ?

  2. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
    @@ -134,19 +134,31 @@ public function get($property_name) {
    +   * Writes the value of a property without handling changes.
    +   *
    +   * @param string $property_name
    +   *   The name of the property to be written.
    +   * @param $value
    +   *   The value to set.
    +   */
    +  protected function writePropertyValue($property_name, $value) {
    

    And here we could document that this is the method that should be used by onChange() implementations instead of set(), because set() calls onChange().

  3. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
    @@ -212,11 +224,16 @@ public function __clone() {
    +   *   (optional) Whether to forward the notification to the parent. Defaults to
    +   *   TRUE. By passing FALSE overriding implementations can re-use the logic
    +   *   of parent classes without triggering the parent notification.
    

    "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 ?

  4. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
    @@ -212,11 +224,16 @@ public function __clone() {
    -    if (isset($this->parent)) {
    +    if (isset($this->parent) && $notify) {
    

    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..."

fago’s picture

Thanks, addressed the remarks.

yched’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @fago.

+++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php
@@ -228,12 +231,12 @@ public function __clone() {
+   *   TRUE. By passing FALSE, overrides of this method can re-use the logic
+   *   of parent classes without triggering notification.

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 ? :-)

plach’s picture

Priority: Major » Critical

This is blocking a critical issue.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Given

However, the previous return value was not implemented at all, so no can be making use of that API right now.

(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!

  • alexpott committed 9f04e89 on 8.0.x
    Issue #2137309 by fago: Typed data does not handle set() and onChange()...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.