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 ofhook_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:
- 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.
- Implement
hook_entity_update()in the custom module. - 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
| Comment | File | Size | Author |
|---|
Issue fork drupal-2974156
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
Comment #2
john.nie commentedComment #3
john.nie commentedComment #4
john.nie commentedComment #5
mario steinitzI 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
elseifinstead of wrapping the contents of theelseblock in anotherifcondition?Comment #6
john.nie commented@Mario Steinitz
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.
Comment #7
m.lebedev commentedThanks. It works
Comment #9
jian he commentedJust needs skip process if $entity has no original.
Comment #10
martin_klimaI 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!
Comment #11
alexpottIn 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:
Comment #12
alexpottAlso 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
Comment #14
megachrizThis error can happen when you try to save an entity when it is already in the process of a save.
For example:
\Drupal\Core\Entity\EntityStorageBase::doPostSave()ends with unsetting$entitiy->original. So following the code example above, before$entity->save(),$entitiy->originalexists, 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.
Comment #15
megachrizOne 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.
Comment #16
megachrizSo 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.
Comment #17
megachrizThere 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?
Comment #18
mattjones86I 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 originalsave()call save that data?Comment #19
adam clarey commentedI 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.
Comment #21
k4v commentedI 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.
Comment #22
k4v commentedhttps://www.drupal.org/node/2892654 seems to be relevant.
Comment #23
k4v commentedI 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.
Comment #24
k4v commentedComment #25
k4v commentedStill needs tests.
Comment #26
darrenwh commentedAdding this extra check as I found it can pass a null value into the function _editor_record_file_usage
Comment #27
k4v commentedNice, 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.
Comment #28
k4v commentedComment #30
ocastle commentedPatch 27 solved my issue.
Comment #31
santimg commentedPatch #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
Comment #32
duneblpatch #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 adestruct()function (implementingDestructableInterface) like explained by https://www.drupal.org/project/commerce/issues/3044681#comment-13220754 without luckI end up by tweaking my code at the form level, which is not very nice
Comment #33
jungleNW for adding test(s)
Comment #34
duneblI 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!!
Comment #36
abhijith s commentedComment #37
abhijith s commentedGot this same error on Drupal 9 when programmatically updating translated nodes.
Hopefully after applying patch #27 the issue is fixed for me. Thankyou
Comment #38
spokjeAs per #33:
Comment #40
phenaproximaCould 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.
Comment #41
ornagy commentedHello, here you can see an example:
https://www.drupal.org/project/commerce/issues/2961440
Comment #43
megachriz@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.
Comment #44
daffie commented@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.
Comment #45
daffie commentedI think the fix should be something like:
$original_uuids_by_field = !empty($entity->original) ? _editor_get_file_uuids_by_field($entity->original) : [];Comment #46
megachriz@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:
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 timeeditor_entity_update()gets called, it is triggered by the second invocation ofhook_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->originalno 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).
Comment #48
daffie commentedThe 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.
Comment #49
alexpottCommitted and pushed cd735be4dd to 9.3.x and b5cfa7cfa4 to 9.2.x. Thanks!
Comment #52
alexpott