When a configuration entity that acts as a bundle for another entity is first saved, it does not have an "original ID" yet. If that configuration entity is then re-saved during Entity::postSave() or hook_entity_insert(), the site shows a blank screen (WSOD).
This is because ConfigEntityBundleBase::preSave() throws an exception if the entity ID differs from the original ID. Of course this is always the case for new bundle entities, as they have NULL for their original ID. See below for the relevant code:
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Only handle renames, not creations.
if (!$this->isNew() && $this->getOriginalId() !== $this->id()) {
$bundle_type = $this->getEntityType();
$bundle_of = $bundle_type->getBundleOf();
if (!empty($bundle_of)) {
throw new ConfigNameException("The machine name of the '{$bundle_type->getLabel()}' bundle cannot be changed.");
}
}
}
However, EntityStorageBase::doPostSave has a fix for this, but it's being called too late:
protected function doPostSave(EntityInterface $entity, $update) {
$this->resetCache(array($entity->id()));
// The entity is no longer new.
$entity->enforceIsNew(FALSE);
// Allow code to run after saving.
$entity->postSave($this, $update);
$this->invokeHook($update ? 'update' : 'insert', $entity);
// After saving, this is now the "original entity", and subsequent saves
// will be updates instead of inserts, and updates must always be able to
// correctly identify the original entity.
$entity->setOriginalId($entity->id());
unset($entity->original);
}
Proposed resolution:
- Move the relevant piece of code above the postSave and invokeHook for new entities and leave it below the hooks for updated entities.
- Alternatively, do the above in a brand new ConfigEntityStorageBase class override of ::postSave().
Example use case:
Config entities can have plugin collections by implementing EntityWithPluginCollectionInterface. After saving, one could let other modules try to install "default" plugins on the config entity. Doing so would require a re-save of the config entity, leading to the exception thrown.
Comments
Comment #2
kristiaanvandeneyndeFirst stab at this, let's see how the test bot likes it.
Comment #3
penyaskitoFacing the same issue here. We need to add tests reproducing the issue.
Comment #4
penyaskitoProbably obvious, but just in case, a workaround for contrib in the meanwhile: you can load the entity being saved again, and it will have the proper original id values.
Comment #5
kristiaanvandeneyndeI did this as a workaround:
Comment #6
dawehnerMoving to needs work as tests are kinda clearly missing ...
Good catch. This could be the case for config entities theoretically.
Doesn't that make parts of the documentation redundant now?
Comment #7
kristiaanvandeneynde@dawehner: It's actually still very applicable to the line that unsets the 'original' property, isn't it? We are telling people why we're unsetting that property: Because the entity itself is now the original.
I'd look into tests, but is there any way you can test a postSave() method or hook invocation easily? Haven't really written any D8 tests yet and a push in the right direction would help. If you have to create a whole config entity for the sake of one test, that seems like a lot of code..
Comment #8
kristiaanvandeneynde@dawehner Looking into \Drupal\entity_test, I cannot find a single test for bundle entities. It seems as though the entire concept of bundle entities is not covered by any test. If there were any, I could have built this issue's test on top them, but there aren't any...
Comment #9
dawehnerI guess we indeed just have implicit test coverage via nodes, comments as well as block_content entities.
Comment #10
kristiaanvandeneyndeAttached is a patch with just the tests and one with tests and the fix from #2.
Mind that these are my first D8 tests I've ever written so they made break, be in the wrong place or set your house on fire.
Comment #13
kristiaanvandeneyndeLet's try again with a config schema for the bundle entity.
Still firing shots in the dark here with regard to the tests. Is there documentation on how to provide them for D8 somewhere?
Comment #14
kristiaanvandeneyndeRight, so that only seems to have run the setUp() method on BundleEntityTest and not testResaveNewBundleEntity(). I'm not getting any wiser from https://api.drupal.org/api/drupal/core!core.api.php/group/testing/8 or https://www.drupal.org/phpunit either...
Comment #15
kristiaanvandeneyndeRight, final test attempt before I go throwing stuff out the window.
Comment #16
kristiaanvandeneyndeTest-only patch should be failing, I give up. I've got other fish to fry.
Comment #17
kristiaanvandeneyndeI know why it's passing now! When manually creating an entity like I did in my test, the originalId gets set in ConfigEntityBase::__construct().
However, when creating an entity through its entity form, the originalId remains NULL throughout because it receives an almost completely empty entity from HtmlEntityFormController::getFormObject() and EntityForm::buildEntity() will not find originalId in the form values and thus not set it.
So my fix in #2 will definitely fix the bug, but the test need to unset the originalId property or find another way to fake a form submission.
Comment #18
swentel CreditAttribution: swentel commentedWouldn't it be better than to actually create a webtestbased test ? Sounds more honest then as a test instead of manually unsetting it.
Comment #19
kristiaanvandeneyndeRight, so this should finally work.
Comment #20
kristiaanvandeneyndeI actually found that creating an almost empty entity and then setting the ID manually is exactly what the entity form submission does. So no need for a web test, as we can mimic what the form submission does.
Comment #21
kristiaanvandeneyndeI borked the testing by immediately hiding the TEST_ONLY patch :/
Comment #22
kristiaanvandeneyndeSorry about the spam, let's try the TEST_ONLY version now.
Comment #24
kristiaanvandeneyndeYay! So we have a test which should fail and does fail in #22 and a test+patch which should go green and goes green in #19. Seeing as it's still the same fix from #2 and others have taken a look at this issue: RTBC?
Comment #25
swentel CreditAttribution: swentel commentedthese docs should be removed as they are pointless no ?
Are we really cloning here ?
Comment #27
kristiaanvandeneynde@dawehner #6 and swentel #25: The resulting part is as shown below and still applies to the unset() call: It makes sure that next time a function requests the original entity, it will be loaded again with the newly saved data.
Fixed the documentation as pointed out by swentel.
Comment #28
kristiaanvandeneyndeIs there anything else that needs to be addressed or can this go in?
Comment #29
kristiaanvandeneyndeDoes this need a reroll because of #2666792: Provide a route provider for add-page of entities? Technically it's still a bug in 8.0.x but it could benefit from the tests added to 8.1.x in that issue.
Comment #30
bojanz CreditAttribution: bojanz at Centarro commented@kristiaanvandeneynde
No reason not to provide different patches for 8.1 and 8.0
Comment #32
xjmThe core committers discussed this issue with the Entity and Field maintainers today. We agreed to downgrade this issue because it is only developer-facing and it is possible to work around it by setting the original ID before re-saving the entity. The error is also not unrecoverable as far as we can tell (so it doesn't entirely crash the site). An exception is a reasonable result to inform the developer that the API does not yet support this (even though that is unexpected and we probably want to fix it).
The summary could use some clarification. The first paragraph seems much broader in scope than the title suggests. Rather than "an entity", should it be "a configuration entity that is used for a bundle of another entity (like a content type/vocabulary)"? It would also be helpful to explain a usecase/scenario where this needs to be done in
postSave()
.Thanks for reporting this!
Comment #33
dawehnerInteresting enough this sounds really similar to #2718839: hook_ENTITY_update() and hook_ENTITY_insert() don't allow to resave entities with a new revision, but just the for entity ID instead of the revision ID
Comment #34
kristiaanvandeneynde@xjm is the issue summary better like this?
Comment #35
xjmThanks @kristiaanvandeneynde, that helps!
Comment #38
kristiaanvandeneyndeSo what does this issue need to go in?
Comment #39
dawehnerOMG you have no idea how much pain I had with that particular bit of code.
I don't see an example of a save inside a test. Ideally this should be tested as well.
Comment #40
kristiaanvandeneyndeRe-rolled the patch and adjusted it to match the current tests. Please note that I am still using EntityUnitTestBase, even though that class now reads:
@dahwener: The save is happening inside BundleEntityTest
Comment #41
BerdirYou shouldn't, just switch to EntityKernelTestBase and move the test to core/tests/Kernel/Entity or something like that, where the other entity tests are.
Comment #42
kristiaanvandeneyndeMoved it.
Comment #43
dawehner@kristiaanvandeneynde
I think having a save in a update hook would be nice as a test, as this is also a really common usecase.
Comment #44
kristiaanvandeneynde@dawehner You mean in a test module and then a test checking for what that hook did? Much like what we already have for postSave()? Also, don't you mean an insert hook? Re-saving a bundle entity in an update works fine as it is.
Comment #45
dawehnerWell for me its mostly about ensuring resaving actually works. I ran into issue inside editor module for example, so ideally try to enable that module in the test, just as an experiment.
Comment #46
Pavan B S CreditAttribution: Pavan B S at Valuebound commentedRerolled the patch.
Comment #48
kristiaanvandeneyndeThe re-roll seems a bit off:
Could you please re-roll the patch with just the changes from the patch in #42?
Comment #49
jofitz CreditAttribution: jofitz at ComputerMinds commentedI'm not convinced the patch in #42 requires a re-roll, it applies perfectly well for me. Perhaps @PavanBS was trying to apply it to 8.4.x? Retesting #42 to be sure, setting back to Needs Review in expectation of a test pass.
Comment #51
kristiaanvandeneyndeYeah, don't think it needed a reroll either :/
Seems like a testbot fail, judging by https://www.drupal.org/pift-ci-job/633085?
Comment #52
kristiaanvandeneyndeAdded an insert hook test as well, as requested by Daniel in #45.
Comment #54
kristiaanvandeneyndeComment #55
xjmThanks @Pavan B S for your interest in contributing! Note that I've saved this issue to remove any default credit for your attempted reroll, because the reroll was incorrect and not needed. (Adding the "Needs reroll" tag to a patch that actually applies is also not correct.) You can see https://www.drupal.org/patch/reroll#introduction for more information on how to reroll a patch, including how to tell from the automated testing system when a reroll is actually needed. Maybe you could look into some of the other contributor tasks for other issues, since this issue currently just needs peer code review.
@kristiaanvandeneynde, note that if you attach the test-only patch as the first attachment, rather than the second, that ensures that testbot will not mark the issue needs work (since the issue status is based on the last file attachment).
Comment #56
kristiaanvandeneyndeI had no idea the order of patches was of importance. Thanks @xjm!
Comment #57
Pavan B S CreditAttribution: Pavan B S at Valuebound commented@xjm, thanks for the link, i will go through that link, and i will make sure that i will not do that mistake in future.
Comment #61
alphawebgroupany chance to get this committed?
Comment #62
hchonovThis comment references the updating of the original ID, so it should be moved to the new description.
The type of the parameter is missing.
This could become a single line.
--------
Only when it is RTBC. You could help with the patch or the review of it.
Comment #63
kristiaanvandeneyndeI'm a bit (actually, really) busy right now. Maybe a novice could come in and take care of #62?
Comment #64
gnunes CreditAttribution: gnunes at bio.logis Genetic Information Management GmbH commentedI've implemented the changes suggested at #62 and re-rolled for 8.9.x and 9.0.x.
It needs review, please
Comment #65
kristiaanvandeneyndeNot sure I'm the right person to RTBC as the original patch was my doing, but the rerolls by @gnunes are perfect. Please set back to "needs review" if it's not my call to RTBC this :)
Thanks for the rerolls!
Comment #66
oriol_e9gTest only are passing and should fail.
Comment #67
kristiaanvandeneyndeIt seems like the test only patches didn't really change and yet they go green where they used to go red. This needs further review indeed.
Comment #74
smustgrave CreditAttribution: smustgrave at Mobomo commentedThis issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.
The tests-only patch should definitely fails so needs work for those.
Comment #75
Akhil Yadav CreditAttribution: Akhil Yadav commentedAdded patch against #64 in 10.1 version
Comment #76
BramDriesenHiding patch #75 as it's incomplete and omitting important changes and/or files.
Adding to the list [#https://www.drupal.org/project/site_moderators/issues/3339883]