Found out in #2164601-48: Stop auto-creating FieldItems on mere reading of $entity->field[N] :

On an EntityRefItem :
- $item->entity = $existing_entity populates $item->target_id with $existing_entity->id() :
\Drupal\Core\Entity\Plugin\DataType\EntityReference::setValue() calls \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onChange(), which assigns the target_id property

- but $item->setValue(array('entity' => $existing_entity)) does not assign the $item->target_id,
which triggers lots of interesting fails.

Not sure exactly where this needs to be fixed yet.
Major, because code will work or break depending on the syntax you use to populate your item.

Comments

yched’s picture

Title: ERItem::setValue(array('entity' => $entity) does not assign the target_id property » ERItem::setValue(array('entity' => $entity) produces broken Items
Issue summary: View changes
yched’s picture

Since the assignment of the target_id is taken care of by EntityReferenceItem::onChange(), which isn't triggered by Item::setValue(), maybe there should also be some special code in EntityReferenceItem::setValue() to populate the 'target_id' if the 'entity' property was part of the passed value ?

yched’s picture

As a proof-of-concept fail in "drush php" :

$term = \Drupal\taxonomy\Entity\Term::create(['vid' => 'tags', 'name' => 'foo']);
$term->save();
$node = \Drupal\node\Entity\Node::create(['type' => 'article']);
// Assign the term by 'entity' in setValue():
$node->field_tags = [ [ 'entity' => $term ] ]; // or $node->field_tags[0] = [ 'entity' => $term ];
$node->field_tags->entity;  // OK, returns the term
$node->field_tags->target_id; // NOK, returns null
// Assign the 'entity' property directly:
$node->field_tags->entity = $term;
$node->field_tags->target_id; // OK, returns the term id.
yched’s picture

Assigned: Unassigned » fago
Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new981 bytes

Attached patch seems to fix this - this would really need to be approved by @fago though, because the sync between 'target_id' and 'entity' goes through a notify dance that loses me a bit :-).

Also, this will need tests, once the fix is agreed.

amateescu’s picture

StatusFileSize
new1.7 KB

Would this be enough for testing?

yched’s picture

Yep, the tests look great, thanks @amateescu :-)

Status: Needs review » Needs work

The last submitted patch, 5: 2386559-tests-only.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new2.65 KB

Patch from #4 + tests from #5

amateescu’s picture

+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
@@ -150,10 +150,13 @@ public function setValue($values, $notify = TRUE) {
-      // Make sure that the 'entity' property gets set as 'target_id'.
+      // Make sure that the 'entity' and 'target_id' properties stay in sync.
       if (isset($values['target_id']) && !isset($values['entity'])) {
         $values['entity'] = $values['target_id'];
       }
+      elseif (isset($values['entity']) && !isset($values['target_id']) && $values['entity'] instanceof EntityInterface) {
+        $values['target_id'] = $values['entity']->id();
+      }

I felt like we're missing something here (and in the test coverage too) but it took me a few good minutes to put my finger on it:

What happens if we assign both 'target_id' and 'entity' at the same time, but with "unsynced" values (e.g. 'target_id' => 1 and 'entity' => $entity_for_which_the_id_is_not_1)?

My guess is that we should also take care of this case and throw an InvalidArgumentException. This might even be a potential security issue I suppose :)

yched’s picture

Indeed - even with current patch :

$term1 = \Drupal\taxonomy\Entity\Term::create(['vid' => 'tags', 'name' => 'foo']);
$term1->save();
$term2 = \Drupal\taxonomy\Entity\Term::create(['vid' => 'tags', 'name' => 'bar']);
$term2->save();
$node = \Drupal\node\Entity\Node::create(['type' => 'article']);
$node->field_tags = [ [ 'entity' => $term1, 'target_id' => $term2->id() ] ];
$node->field_tags->entity->id() == $term1->id(); // TRUE, 'entity' contains $term1
$node->field_tags->target_id == $term2->id(); // TRUE, 'target_id' points to $term2
amateescu’s picture

That's hilarious :) Good thing we found it while just poking around this code and not after 8.0.0.

yched’s picture

Also, looks like LanguageItem will need the same fixes, it is likely to have the same issues for its 'value' and 'language' properties.

fago’s picture

Assigned: fago » Unassigned
StatusFileSize
new5.41 KB

Yeah, in general you have to pass a valid $item array in setValue() if you pass an array. However, the case for the synced reference items is a bit special and I agree that it should take care of both cases with only one of both properties being passed in.

Yep, language reference item has the same flow and should be fixed as well. However, I'd propose a fix that re-uses existing onChange() logic instead of re-inventing in setValue(). So instead is another patch, which keeps tests of #8 only.

Status: Needs review » Needs work

The last submitted patch, 13: d8_field_ref_item_set.patch, failed testing.

fago’s picture

Status: Needs work » Needs review
Issue tags: +Ghent DA sprint
StatusFileSize
new7.63 KB

Discussed the best code-flow with yched. Attached patch implements an idea to improve it for ERI, but doesn't update LanguageItem yet. Looked through the onChange() methods to unify them, but that's not really related to this issue. (didn't look at test fails yet either).

fago’s picture

Moved the unrelated changes to its existing issue: #2137309: Typed data does not handle set() and onChange() consistently

Status: Needs review » Needs work

The last submitted patch, 15: d8_field_ref_item_set.patch, failed testing.

yched’s picture

Yay, that approach looks way easier to follow :-)

  1. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
    @@ -150,12 +150,19 @@ public function setValue($values, $notify = TRUE) {
    +      parent::setValue($values, FALSE);
    +      // Make sure values stay in sync if only one is passed.
           if (isset($values['target_id']) && !isset($values['entity'])) {
    -        $values['entity'] = $values['target_id'];
    +        $this->synchronizePropertyValues('target_id');
    +      }
    +      elseif (!isset($values['target_id']) && isset($values['entity'])) {
    +        $this->synchronizePropertyValues('entity');
    +      }
    +      if ($notify) {
    +        $this->notifyParent();
           }
    

    We should comment this better IMO :

    // Apply the specified values without notifying the parent yet. 
    ...
    // Make sure the 'target_id' and 'entity' properties stay in sync.
    ...
    // Notify the parent if needed.
    ...
    

    Also, we should raise an exception if $values contain both 'entity' and 'target_id' that contradict each other. That could go in that if / elsif group ? We'd still call parent::setValue() for nothing, but that's not too important ?

  2. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
    @@ -173,16 +180,26 @@ public function getValue($include_computed = FALSE) {
    +   * Synchronizes the property values of the target_id and entity properties.
    

    Nitpick : "Synchronizes the values of the 'target_id' and 'entity' properties" ?

  3. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
    @@ -173,16 +180,26 @@ public function getValue($include_computed = FALSE) {
    +   * @param  string $changed_property
    

    nitpick : double space

plach’s picture

Priority: Major » Critical

This is blocking a critical issue.

plach’s picture

Title: ERItem::setValue(array('entity' => $entity) produces broken Items » [PP-1] ERItem::setValue(array('entity' => $entity) produces broken Items
Priority: Critical » Major
Status: Needs work » Postponed
fago’s picture

Status: Postponed » Needs review
StatusFileSize
new3.9 KB

Updated the patch based on #2137309: Typed data does not handle set() and onChange() consistently / HEAD. This shuold be fine for the ER item, but does not account for the language item yet. We should add test coverage for any language item changes as well though imo.

Setting for needs review so the bot can give it a try.

fago’s picture

Status: Needs review » Postponed
plach’s picture

Title: [PP-1] ERItem::setValue(array('entity' => $entity) produces broken Items » ERItem::setValue(array('entity' => $entity) produces broken Items
Priority: Major » Critical
Status: Postponed » Needs review
plach’s picture

+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php
@@ -146,12 +145,27 @@ public function setValue($values, $notify = TRUE) {
+      elseif (isset($values['target_id']) && isset($values['entity'])) {
+        // If both properties are passed, verify the passed values match.
+        if ($this->get('entity')->getTargetIdentifier() != $values['target_id']) {
+          throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.');
+        }
+      }
+      // Notify the parent if necessary.

It seems in this case we are setting nothing.

jibran’s picture

Issue tags: -Needs tests
StatusFileSize
new2.05 KB

Patch looks good to me over all. Here is a test only patch.

plach’s picture

Forget #24, now I get it.

Status: Needs review » Needs work

The last submitted patch, 25: 2386559-test-only.patch, failed testing.

plach’s picture

Status: Needs work » Reviewed & tested by the community

Awesome, patch to commit is in #21.

jibran’s picture

RTBC +1

effulgentsia’s picture

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed #21 to 8.0.x. Thanks!

Looks like we need a child issue for LanguageItem. Can someone take care of that quickly?

  • webchick committed 15debbd on 8.0.x
    Issue #2386559 by fago, yched, amateescu, jibran, plach: ERItem::...

Status: Fixed » Closed (fixed)

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

jibran’s picture

Do we still need a follow up for LanguageItem?

plach’s picture

AFAIK, yes, at least I didn't create one