Problem/Motivation

When an entity gets saved when it is already in the process of being saved, the following error occurs:

TypeError: Argument 1 passed to _editor_get_file_uuids_by_field() must implement interface Drupal\Core\Entity\EntityInterface, null given, called in drupal/core/modules/editor/editor.module on line 399

The resave of an entity can happen in other modules by:

  • executing $entity->save(); in an implementation of hook_entity_update().
  • saving an entity in an event subscriber whose event gets dispatched while the entity was already in the process of being saved. Maybe the event got dispatched in a postSave() call. Apparently, this can happen with the Commerce project, as reported in #2961440: Error _editor_get_file_uuids_by_field() .

Steps to reproduce

The simplest way to reproduce the issue is as follows:

  1. Create a custom module whose hook implementations are called earlier than those from the editor module. This can either be done by having a module with a lower weight than the editor module or by having a module whose name comes earlier in the alphabet.
  2. Implement hook_entity_update() in the custom module.
  3. Resave the entity in that function.

Example implementation:

/**
 * Implements hook_entity_update().
 */
function mymodule_entity_update(EntityInterface $entity) {
  // Avoid infinite loop by only going through our post save logic once.
  if (!empty($entity->mymodule_updating)) {
    return;
  }

  // Set flag for whether or not the entity needs to be resaved.
  $needs_update = FALSE;

  // Perform our post save logic.
  if (1) {
    // (...)
    $needs_update = TRUE;
  }

  if ($needs_update) {
    // Set flag on entity that our logic was already executed.
    $entity->mymodule_updating = TRUE;
    // And resave entity.
    $entity->save();
  }
}

Result:

TypeError: Argument 1 passed to _editor_get_file_uuids_by_field() must implement interface Drupal\Core\Entity\EntityInterface, null given, called in drupal/core/modules/editor/editor.module on line 399

Traceback

TypeError: Argument 1 passed to _editor_get_file_uuids_by_field() must implement interface Drupal\Core\Entity\EntityInterface, null given, called in drupal/core/modules/editor/editor.module on line 399

drupal/core/modules/editor/editor.module:569
drupal/core/modules/editor/editor.module:399
drupal/core/lib/Drupal/Core/Extension/ModuleHandler.php:403
drupal/core/lib/Drupal/Core/Entity/EntityStorageBase.php:201
drupal/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php:800
drupal/core/lib/Drupal/Core/Entity/EntityStorageBase.php:530
drupal/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php:685
drupal/core/lib/Drupal/Core/Entity/EntityStorageBase.php:455
drupal/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php:801
drupal/core/lib/Drupal/Core/Entity/EntityBase.php:339

Original report by John.nie

user edit page: when add a custom field named person, when I save my user information, it reports this error like below:

[Mon May 21 01:21:49.224782 2018] [:error] [pid 15769] [client 172.17.0.1:48758] TypeError: Argument 1 passed to _editor_get_file_uuids_by_field() must implement interface Drupal\\Core\\Entity\\EntityInterface, null given, called in /var/www/html/core/modules/editor/editor.module on line 384 in /var/www/html/core/modules/editor/editor.module on line 554 #0 /var/www/html/core/modules/editor/editor.module(384): _editor_get_file_uuids_by_field(NULL)\n#1 [internal function]: editor_entity_update(Object(Drupal\\user\\Entity\\User))\n#2 /var/www/html/core/lib/Drupal/Core/Extension/ModuleHandler.php(402): call_user_func_array('editor_entity_u...', Array)\n#3 /var/www/html/core/lib/Drupal/Core/Entity/EntityStorageBase.php(169): Drupal\\Core\\Extension\\ModuleHandler->invokeAll('entity_update', Array)\n#4 /var/www/html/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php(730): Drupal\\Core\\Entity\\EntityStorageBase->invokeHook('update', Object(Drupal\\user\\Entity\\User))\n#5 /var/www/html/core/lib/Drupal/Core/Entity/EntityStorageBase.php(470): Drupal\\Core\\Entity\\ContentEntityStorageBase->invokeHook('update', Object(Drupal\\user\\Entity\\User))\n#6 /var/www/html/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php(615): Drupal\\Core\\Entity\\EntityStorageBase->doPostSave(Object(Drupal\\user\\Entity\\User), true)\n#7 /var/www/html/core/lib/Drupal/Core/Entity/EntityStorageBase.php(395): Drupal\\Core\\Entity\\ContentEntityStorageBase->doPostSave(Object(Drupal\\user\\Entity\\User), true)\n#8 /var/www/html/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php(820): Drupal\\Core\\Entity\\EntityStorageBase->save(Object(Drupal\\user\\Entity\\User))\n#9 /var/www/html/core/lib/Drupal/Core/Entity/Entity.php(387): Drupal\\Core\\Entity\\Sql\\SqlContentEntityStorage->save(Object(Drupal\\user\\Entity\\User))\n#10 /var/www/html/core/modules/user/src/ProfileForm.php(38): Drupal\\Core\\Entity\\Entity->save()\n#11 [internal function]: Drupal\\user\\ProfileForm->save(Array, Object(Drupal\\Core\\Form\\FormState))\n#12 /var/www/html/core/lib/Drupal/Core/Form/FormSubmitter.php(111): call_user_func_array(Array, Array)\n#13 /var/www/html/core/lib/Drupal/Core/Form/FormSubmitter.php(51): Drupal\\Core\\Form\\FormSubmitter->executeSubmitHandlers(Array, Object(Drupal\\Core\\Form\\FormState))\n#14 /var/www/html/core/lib/Drupal/Core/Form/FormBuilder.php(585): Drupal\\Core\\Form\\FormSubmitter->doSubmitForm(Array, Object(Drupal\\Core\\Form\\FormState))\n#15 /var/www/html/core/lib/Drupal/Core/Form/FormBuilder.php(314): Drupal\\Core\\Form\\FormBuilder->processForm('user_form', Array, Object(Drupal\\Core\\Form\\FormState))\n#16 /var/www/html/core/lib/Drupal/Core/Controller/FormController.php(74): Drupal\\Core\\Form\\FormBuilder->buildForm('user_form', Object(Drupal\\Core\\Form\\FormState))\n#17 [internal function]: Drupal\\Core\\Controller\\FormController->getContentResult(Object(Symfony\\Component\\HttpFoundation\\Request), Object(Drupal\\Core\\Routing\\RouteMatch))\n#18 /var/www/html/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)\n#19 /var/www/html/core/lib/Drupal/Core/Render/Renderer.php(582): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->Drupal\\Core\\EventSubscriber\\{closure}()\n#20 /var/www/html/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\\Core\\Render\\Renderer->executeInRenderContext(Object(Drupal\\Core\\Render\\RenderContext), Object(Closure))\n#21 /var/www/html/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array)\n#22 /var/www/html/vendor/symfony/http-kernel/HttpKernel.php(151): Drupal\\Core\\EventSubscriber\\EarlyRenderingControllerWrapperSubscriber->Drupal\\Core\\EventSubscriber\\{closure}()\n#23 /var/www/html/vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\\Component\\HttpKernel\\HttpKernel->handleRaw(Object(Symfony\\Component\\HttpFoundation\\Request), 1)\n#24 /var/www/html/modules/contrib/simple_oauth/src/HttpMiddleware/BasicAuthSwap.php(67): Symfony\\Component\\HttpKernel\\HttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#25 /var/www/html/core/lib/Drupal/Core/StackMiddleware/Session.php(57): Drupal\\simple_oauth\\HttpMiddleware\\BasicAuthSwap->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#26 /var/www/html/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(47): Drupal\\Core\\StackMiddleware\\Session->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#27 /var/www/html/core/modules/page_cache/src/StackMiddleware/PageCache.php(99): Drupal\\Core\\StackMiddleware\\KernelPreHandle->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#28 /var/www/html/core/modules/page_cache/src/StackMiddleware/PageCache.php(78): Drupal\\page_cache\\StackMiddleware\\PageCache->pass(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#29 /var/www/html/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(47): Drupal\\page_cache\\StackMiddleware\\PageCache->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#30 /var/www/html/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(50): Drupal\\Core\\StackMiddleware\\ReverseProxyMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#31 /var/www/html/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\\Core\\StackMiddleware\\NegotiationMiddleware->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#32 /var/www/html/core/lib/Drupal/Core/DrupalKernel.php(657): Stack\\StackedHttpKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request), 1, true)\n#33 /var/www/html/index.php(19): Drupal\\Core\\DrupalKernel->handle(Object(Symfony\\Component\\HttpFoundation\\Request))\n#34 {main}, referer: http://localhost:8181/user/2/edit?destination=/admin/people

Issue fork drupal-2974156

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

john.lee created an issue. See original summary.

john.nie’s picture

Component: media system » editor.module
john.nie’s picture

StatusFileSize
new1.85 KB
john.nie’s picture

Status: Active » Needs review
mario steinitz’s picture

I experienced the same error when programmatically updating a revisionable entity within it's update hook and not creating a new revision. Your patch fixes the issue.

However, I'm wondering, why not using an elseif instead of wrapping the contents of the else block in another if condition?

john.nie’s picture

@Mario Steinitz

I experienced the same error when programmatically updating a revisionable entity within it's update hook and not creating a new revision. Your patch fixes the issue.

However, I'm wondering, why not using an elseif instead of wrapping the contents of the else block in another if condition?

maybe it will support other condition, so I solve this for my scene.

if something else condition, please commit a patch for this.

I'll appreciate for this.

m.lebedev’s picture

Thanks. It works

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

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now 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.

jian he’s picture

StatusFileSize
new838 bytes

Just needs skip process if $entity has no original.

martin_klima’s picture

Status: Needs review » Reviewed & tested by the community

I experienced the same error when programmatically updating a Commerce order due to change the order state transition in OrderEventSubscriber. Your patch fixes the problem. Many thanks!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

In order to commit a bug fix we need an automated to test to prove that we've fixed the bug and ensure that we don't break it again in the future. For more information about writing tests in Drupal 8 see the following links:

  1. https://www.drupal.org/docs/8/testing
  2. https://api.drupal.org/api/drupal/core%21core.api.php/group/testing/8.7.x
alexpott’s picture

Also I'm not entirely sure the change is correct. I think we need to get to the second part of the if and record file usage and do something like

    $original_uuids_by_field = empty($entity->original) ? [] : _editor_get_file_uuids_by_field($entity->original);

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

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

megachriz’s picture

This error can happen when you try to save an entity when it is already in the process of a save.

For example:

/**
 * Implements hook_entity_update().
 */
function mymodule_entity_update(EntityInterface $entity) {
  // Avoid infinite loop by only going through our post save logic once.
  if (!empty($entity->mymodule_updating)) {
    return;
  }

  // Set flag for whether or not the entity needs to be resaved.
  $needs_update = FALSE;

  // Perform our post save logic.
  // (...)

  if ($needs_update) {
    // Set flag on entity that our logic was already executed.
    $entity->mymodule_updating = TRUE;
    // And resave entity.
    $entity->save();
  }
}

\Drupal\Core\Entity\EntityStorageBase::doPostSave() ends with unsetting $entitiy->original. So following the code example above, before $entity->save(), $entitiy->original exists, but afterwards it doesn't.

After the function above is finished, Drupal continues invoking the entity update hook that was started after the first entity save call.

In my opinion it is bad practice to save an entity when it is already in the process of being saved. In theory, this can even lead to infinite loops: an entity save call could result into another save call for the same entity, which could result into yet another save call for the same entity, and so on.

megachriz’s picture

One way to avoid the bad practice could be to enforce disallowing saving an entity in presave, insert and update hooks: throw an exception in \Drupal\Core\Entity\EntityStorageBase::save() when the entity is already in the process of being saved.

However, this may be too disruptive. Would there be any legit reason to resave an entity in these hooks? The patch attached isn't necessarily meant to be committed, but an attempt to find out if such legit reasons actually exist in core.

megachriz’s picture

So resaving an entity is a valid use case in an entity insert hook. Good to know. Is that also the case for an entity update hook? Let's find out.

megachriz’s picture

There is no example in a core (test) module that resaves an entity in an update hook. The test failure from #16 is caused by not having the "is_updating" property reset on an exception during saving.

So the question now is: should resaving an entity in an update hook be supported?

If not, then this issue can either be closed as "won't fix" or we could decide to trigger a warning whenever an entity gets resaved in an update hook. By using a warning instead of throwing an exception we would keep backwards compatibility.

I would say it should not be supported to resave an entity in an update hook because of the risk to cause an infinite loop (entity save > invoke update hook > entity save > invoke update hook > entity save, etc.).

Opinions?

mattjones86’s picture

I tend to agree with you MegaChriz, if a developer is attempting to call $entity->save() within an presave/update/insert then I think that's asking for trouble.

Surely in most cases it's enough to use hook_entity_presave() to set the properties required and let the original save() call save that data?

adam clarey’s picture

I have a use case for re-saving an entity in _update()

I have a parent entity, which has a field with references to child entities.

The parent has a status field.

When ever a child is updated, I need the parent to check all children and depending on the state of the children, this will alter the status of the parent.

So when a child is updated, in the _update() method saves the parent: $parent->save()

In the parents _update() method, it loops over all children and if the status of the children hit a different criteria, the parent will its status field and then save itself to take effect.

My logic is sound as to avoid infinite loops.

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.

k4v’s picture

I also have several use cases in my Drupal application to call $entity->save from an update hook.

I don't know any other event or hook to use to trigger my business logic... It would be very good to have a hook that works after an entity is safely stored and I can do some changes on it, e.g. setting a custom field to "processed" when several other states are matched.

k4v’s picture

k4v’s picture

StatusFileSize
new760 bytes

I think this issue is clearly a bug that should be fixed, as the original code is not very clear and this error caught an edge case.

It built a new patch with Alex' suggestion from #12.

k4v’s picture

Status: Needs work » Needs review
k4v’s picture

Status: Needs review » Needs work

Still needs tests.

darrenwh’s picture

StatusFileSize
new1.17 KB

Adding this extra check as I found it can pass a null value into the function _editor_record_file_usage

k4v’s picture

StatusFileSize
new1.26 KB

Nice, I ran into the same problem, and have an additional catch: In my case I got a PHP notice that some fields were not present in the original entity.

k4v’s picture

Status: Needs work » Needs review

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.

ocastle’s picture

Patch 27 solved my issue.

santimg’s picture

Patch #27 solved my issue too. Thanks a lot.

In my case I was saving an order in a post_transition event subscriber. I came to this issue from https://www.drupal.org/project/commerce/issues/2961440

dunebl’s picture

patch #27 remove the error but doesn't solve my problem.
Like @samtimg in #31, I was trying to save an entity in a state_machine post_transition event subscriber; but with the added complicatie that I was trying to perform another transition inside the event subscriber.
In other words:

1-[Button click] $stateItem->applyTransitionById('id1')
2-[Inside state_machine post_transition event subscriber] if ($something){$stateItem->applyTransitionById(id2);$entity->save();}

After patch #27, the entity was well saved, (with the transition 'id2' applied) BUT, the post_transition event of the 'id2' transition was never called.

FYI: I have tried to put my code (containing the transition'id2') in a destruct() function (implementing DestructableInterface) like explained by https://www.drupal.org/project/commerce/issues/3044681#comment-13220754 without luck

I end up by tweaking my code at the form level, which is not very nice

jungle’s picture

Status: Needs review » Needs work
Issue tags: +Bug Smash Initiative

NW for adding test(s)

dunebl’s picture

I end up here again:

First time (#32) : When I was trying to save an entity in a state_machine post_transition event subscriber; but with the added complicatie that I was trying to perform another transition inside the event subscriber.
=>As I could not get rid of the error, I decided to handle my logic at the form level... and I unpatched #27

Now : Like #5, I am updating (and saving) a node in hook_node_update (could not do it in hook_presave due to the fact that I must update the node itself and its' translations which isn't supported by core)

#27 save my day: thank you!!

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.

abhijith s’s picture

Status: Needs work » Needs review
abhijith s’s picture

StatusFileSize
new324.23 KB

Got this same error on Drupal 9 when programmatically updating translated nodes.

before

Hopefully after applying patch #27 the issue is fixed for me. Thankyou

spokje’s picture

Status: Needs review » Needs work

As per #33:

NW for adding test(s)

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.

phenaproxima’s picture

Could someone post steps to reproduce this in the issue summary, starting from a clean installation of Drupal core? If we had those, it would be a lot easier to puzzle out how to write a test.

ornagy’s picture

Hello, here you can see an example:

https://www.drupal.org/project/commerce/issues/2961440

megachriz’s picture

Issue summary: View changes
Issue tags: -Needs tests, -Needs steps to reproduce

@phenaproxima
I updated the issue summary to include the steps to reproduce. I also added a test to the issue branch "2974156-type-error-tests_only" to demontrate the issue.

daffie’s picture

@MegaChriz: The testing in the MR looks good. What we are missing is the fix. Usually we do 2 patches with the first one having the fix and the testing and the second patch only has the testing. The second one should fail the testbot, like your MR is doing. The first patch should pass the testbot. Writing the testing is almost always the most work and that is done. Almost there.

daffie’s picture

I think the fix should be something like:
$original_uuids_by_field = !empty($entity->original) ? _editor_get_file_uuids_by_field($entity->original) : [];

megachriz’s picture

@daffie
There is a possible fix in #27. To me it looks like it is fixing a symptom of the problem, the problem being that resaving an entity in an update hook results in a process like the following:

  1. \Drupal\Core\Entity\EntityStorageBase::doPostSave() gets called.
  2. editor_test_entity_update() gets called.
  3. A resave of the updated entity gets triggered (second save call).
  4. \Drupal\Core\Entity\EntityStorageBase::doPostSave() gets called.
  5. editor_test_entity_update() gets called.
  6. editor_entity_update() gets called (caused by the second save call).
  7. editor_entity_update() gets called (caused by the first save call).

In the process above you see that two save calls on the same entity happened. And also two times hook_entity_update() gets invoked. The perhaps weird thing is that the first time editor_entity_update() gets called, it is triggered by the second invocation of hook_entity_update(). The second time it gets called, it is triggered by the first invocation of the same hook. It is at this point that $entity->original no longer exists, because it gets unset in \Drupal\Core\Entity\EntityStorageBase::doPostSave() after the second hook invocation is done (and the first hook invocation has yet to complete). See https://git.drupalcode.org/project/drupal/-/blob/9.3.x/core/lib/Drupal/C...

But perhaps we're okay with that a process like above can exist. In that case the fix in #27 is probably the way to go (I haven't tested that one yet).

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

daffie’s picture

Status: Needs work » Reviewed & tested by the community

The fix and the added testing look good to me.
The added testing will fail the testbot when the fix is removed. With the same error as the one from the IS.
For me it is RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed cd735be4dd to 9.3.x and b5cfa7cfa4 to 9.2.x. Thanks!

  • alexpott committed cd735be on 9.3.x
    Issue #2974156 by MegaChriz, k4v, John.nie, jian he, darrenwh,...

  • alexpott committed b5cfa7c on 9.2.x
    Issue #2974156 by MegaChriz, k4v, John.nie, jian he, darrenwh,...
alexpott’s picture

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

Status: Fixed » Closed (fixed)

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