EntityReferenceFieldItemList::referencedEntities might not return up-to-date entity objects

Created on 11 November 2016, about 8 years ago
Updated 8 April 2023, almost 2 years ago

Problem/Motivation

EntityReferenceFieldItemList::referencedEntities in its current implementation will collect all the target ids from the field items and then collect the referenced entity objects by calling loadMultiple, but this is faulty in the following use cases:

  1. An entity cloned object has been set on some of the field items -> load will not return this one but a different one.
  2. resetCache is called after the entity objects have been initialized on some of the field items.

Proposed resolution

EntityReferenceFieldItemList::referencedEntities should first collect the entities which are already set on the field items and load the remaining ones through loadMultiple.

Remaining tasks

  • Add test coverage
  • Review

User interface changes

None.

API changes

None.

Data model changes

None.

πŸ› Bug report
Status

Needs work

Version

10.1 ✨

Component
EntityΒ  β†’

Last updated 16 minutes ago

Created by

πŸ‡©πŸ‡ͺGermany hchonov πŸ‡ͺπŸ‡ΊπŸ‡©πŸ‡ͺπŸ‡§πŸ‡¬

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels

    EntityReferenceFieldItemList::referencedEntities should just call $this->getValue($include_computed=TRUE) and then from the result leave only the entity objects.

    Drupal\Core\TypedData\Plugin\DataType\ItemList::getValue() doesn't have an $include_computed parameter, so that option is off the table.

    I'll create a MR implementing the first option.

  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels
  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    There are 2 options it appears in the issue summary proposed solution. Can it be highlighted which was chosen?

    Also could use a test case showing the issue.

  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels
  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels

    It seems like the current implementation of the MR causes a performance problem. $item->entity !== NULL triggers DataReferenceBase::getValue(), which triggers EntityReference::getTarget(), which triggers EntityStorageInterface::load(). This means that every entity is now loaded separately. I'll look for a different solution.

  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels

    I found a fix. In a custom API endpoint with a bunch of referencedEntities() calls this makes a huge performance difference: -5.62 s (-75%) and -2 219 database queries. Hopefully anyone using a patch from this issue sees this.

  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Yes, that's the problem with accessing the property, it auto-initializes, so that condition is always true if it can be loaded.

    I don't think your change is correct either. getValue() will never return computed values, so the result is the same as HEAD.

    I don't think it is possible at all to do this, because even accessing the EntityReference property directly with get('entity') there simply is no API to detect whether it has been set or not. There is no public method to access $this->target and \Drupal\Core\Entity\Plugin\DataType\EntityReference::getTarget() always loads it if not set, that's the same as ->entity.

    That's why \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase::prepareView + \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase::getEntitiesToView() use the magic _loaded property to track that.

    So to change this, we'd first need to extend the underlying entity reference property to add a method to check if the refernce is loaded or not.

  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels

    I don't think your change is correct either. getValue() will never return computed values, so the result is the same as HEAD.

    No, that does seem to work. Because EntityReferenceItem::getValue() looks like this:

    /**
     * {@inheritdoc}
     */
    public function getValue() {
      $values = parent::getValue();
      // If there is an unsaved entity, return it as part of the field item values
      // to ensure idempotency of getValue() / setValue().
      if ($this->hasNewEntity()) {
        $values['entity'] = $this->entity;
      }
      return $values;
    }
    

    $this->entity also triggers EntityReference::getTarget(), but since isset($this->target) it doesn't trigger a new entity load and returns the existing entity object instead.

    I think that means that the previous approach would have also worked, if we had just replaced $item->entity !== NULL with $item->hasNewEntity().

  • πŸ‡§πŸ‡ͺBelgium dieterholvoet Brussels

    By the way, I just tested this with code like this:

    $this->set('field_event_type', [EventType::create()]);
    $types = $this->get('field_event_type')->referencedEntities();
    

    $types contained the newly created, unsaved entity.

Production build 0.71.5 2024