Problem/Motivation

In contrib it is common for workflows to be implemented by resaving the entity in a hook_entity_update() or a matching Rule.
So if my commerce order needs to go through 4 different statuses, and can't skip them by just doing hook_entity_presave(), it is going to do do a status change and a save as the last hook_entity_update() operation / rule.

The code that initializes $entity->original is contained in EntityStorageBase::doPreSave(), the method called before hook_entity_update() implementations are invoked, which contains the following code.

// Load the original entity, if any.
if ($id_exists && !isset($entity->original)) {
  $entity->original = $this->loadUnchanged($id);
}

This prevents $entity->original from being reloaded, so when I react on a fourth status change, $entity->original->status is still pointing at the first one. Ideally workflows like this would be handled in events / hooks outside of the save pipeline, but Drupal is not currently discouraging this practice.

Steps to reproduce

use Drupal\node\Entity\Node;

$node = Node::create(['type' => 'page']);
$node->set('title', 'Drupal 9 rocks!');
$node->save();
$node->set('title', 'Drupal 10 rocks!');
$node->save();

/**
 * Implements hook_node_update().
 */
function mymodule_node_update(Node $node): void {
  // The node has been created with the title 'Drupal 9 rocks!'.
  // Then we update it to say "Drupal 10 rocks!".
  // This hook catches that update, and changes it to "Drupal 11 rocks!".
  // This is a simple example that could be replaced with a presave() hook,
  // but in the case of workflow changes, every status needs to be cycled
  // through, no skipping allowed.
  if ($node->title == 'Drupal 10 rocks!') {
    $node->set('title', 'Drupal 11 rocks!');
    $node->save();
    return;
  }

  // On second update, check the value of $node->original.
  if ($node->title == 'Drupal 11 rocks!') {
    if ($node->original->title == 'Drupal 10 rocks!') {
      dump('The node has the correct original value');
    }
    if ($node->original->title == 'Drupal 9 rocks!') {
      dump('The node has an incorrect original value');
    }
  }
}

Proposed resolution

Stop doing the isset check (it's premature optimization anyway), and then the concrete bug is fixed.

// Load the original entity, if any.
if ($id_exists) {
  $entity->original = $this->loadUnchanged($id);
}

Remaining tasks

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

Issue fork drupal-2140179

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

bojanz’s picture

Status: Active » Needs review
StatusFileSize
new2.61 KB

Here's the initial patch. We'll probably want some tests as well, and a D7 backport when this is done.

Added the Commerce and Entity API (D7 contrib) issues as related.

bojanz’s picture

fago’s picture

I don't think we can remove this "premature" optimization, as it's need to allow mass updates to function efficiently; i.e. it allows you to (at least) multiple load the entities (while there is no multiple update).

The module issuing that change could already take care of updating $entity->original, what would be the most efficient anyway.

A generic, proper solution would be doing #1480696: Move $entity->original to a separate hook argument, i.e. instead of pre-populating $entity->original it could become an optional argument to save(). That would be an API change, but imo this would still justify doing it right *now*.

Ideally workflows like this would be handled in events / hooks outside of the save pipeline, but Drupal is not currently discouraging this practice.

See #1729812: Separate storage operations from reactions.

bojanz’s picture

I don't think we can remove this "premature" optimization, as it's need to allow mass updates to function efficiently; i.e. it allows you to (at least) multiple load the entities (while there is no multiple update).

How does that work? I don't see any other place (such as the load method) populating original, it seems to be loaded one by one?

fago’s picture

check e.g. _node_mass_update_helper()

amateescu’s picture

_menu_navigation_links_rebuild() also uses this (line 2763 in menu.inc) to not execute hundreds of queries on a menu rebuild, so it's not premature optimisation at all.

Status: Needs review » Needs work

The last submitted patch, 1: 2140179-1-always-reload-entity-original.patch, failed testing.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

avpaderno’s picture

Version: 8.6.x-dev » 8.8.x-dev

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Needs work » Postponed (maintainer needs more info)

Is this still relevant? I can't seem to find the code in the description. And the other example seems to be centered around how D7 did it.

If still an issue can we get an updated issue summary please

avpaderno’s picture

Drupal code has been changed, since this issue has been open. Now, the code that initializes $entity->original is contained in EntityStorageBase::doPreSave(), the method called before hook_entity_update() implementations are invoked, which contains the following code.

// A new entity should not already exist.
if ($id_exists && $entity->isNew()) {
  throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '{$id}' already exists.");
}

// Load the original entity, if any.
if ($id_exists && !isset($entity->original)) {
  $entity->original = $this->loadUnchanged($id);
}

I take the issue is suggesting that $this->loadUnchanged($id) should always be called for entities that are updated. The quoted code used by EntityStorageBase::doPreSave() I showed should therefore be changed to the following one.

// A new entity should not already exist.
if ($id_exists && $entity->isNew()) {
  throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '{$id}' already exists.");
}

// Load the original entity, if any.
if ($id_exists) {
  $entity->original = $this->loadUnchanged($id);
}

The issue summary should be updated: What the suggested change is should be made clearer and the Drupal 7 example code should be replaced by Drupal 9 code, or removed.

avpaderno’s picture

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

DieterHolvoet made their first commit to this issue’s fork.

dieterholvoet’s picture

Issue summary: View changes

I updated the issue description and the code example based on the info provided by @apaderno and my own experience. I'll create a MR with the suggested fix since it actually fixed the issue for me.

dieterholvoet’s picture

Issue tags: -Needs issue summary update

dieterholvoet’s picture

Status: Postponed (maintainer needs more info) » Needs review
smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs tests

Failure seems like it could be legit for this issue.

Also would need a test of it's own.

Good job on the issue summary!

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

vidorado made their first commit to this issue’s fork.

vidorado changed the visibility of the branch 11.x to hidden.

vidorado changed the visibility of the branch 2140179-entity-original-gets-stale to hidden.

vidorado’s picture

I've added tests, but I can't get them to fail even after undoing the fix... 🤔

It seems the original entity is already fine in D11.

Could you take a look, @dieterholvoet?

Edit: Now that the complete test suite has run in GitLab, I see the fix is breaking some tests. A deeper investigation of the side effects over entity revisions is needed.

dieterholvoet’s picture

I can confirm the issue is still present in Drupal 11.3.0 and merge request !10763 fixes it.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.