Problem/Motivation

The root problem

Steps to reproduce:

  1. Create an entity with two translations
        $node = Node::create('langcode' => 'en');
        $node->addTranslation('fr');
        $node->save();
        
  2. Reload the entity
        $node = Node::load($node->id());
        
  3. Initialize the translation
        $entity->getTranslation('fr');
        
  4. Clone the entity
        $clone = clone $entity;
        

At this point $entity->field_name === $clone->field_name will yield TRUE. This means changing field values in $clone now also changes the values in $entity which precisely breaks the contract of the clone operation.

How this affects content entity forms

This affects content entity forms because during validation a clone of the entity is populated with the user submitted values to perform validation of those values via $cloned_entity->validate(). Due to this bug this in fact changes the field values in of the form entity in $this->entity. This is not a problem for normal form submissions because the form state is not cached, thus the form entity is rebuilt on each request.

On Ajax operations, however, the form state does get cached for subsequent requests. This means that at this point the values in $this->entity are the updated values from the user input and not the original entity values when the form is being (re-)built. The fact that possibly unvalidated values end up in $this->entity and that the behavior is different for non-Ajax and Ajax forms is a corollary problem of the original problem.

Why this furthermore affects changing the entity language

When there are at least three languages and a node has been created and translated into one language (but not the other(s)), it is possible to change the language of the original node to the language(s) in which no translation exist(s) yet.

ContentEntityForm::updateFormLangcode() is an #entity_builder which updates the form language (accessible via $form_state->getLangcode()) when the language is changed in this way. The fact that this #entity_builder has side effects is problematic, as discussed in #2799637: Document that #entity_builders and overrides of EntityForm::copyFormValuesToEntity() must be idempotent. This will be discussed - and hopefully fixed - in #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls, however, so for the scope of this issue, we will accept this as fact.

Content entity forms also store the entity's default language in form state (accessible via $form_state->get('entity_default_langcode')) which is compared with the form langcode to determine whether the current form is a translation form. This information can be retrieved via $form_state->isDefaultFormLangcode().

Due to the bug described above $form_state->getFormObject()->getEntity()->getUntranslated()->language()->getId() and $form_state->getFormLangcode() always return the same thing. Initially they are the same thing but also after the entity language has been changed and an Ajax operation has been performed they are again the same thing as not only the form langcode but by accident also the entity's default language has been changed.

As modules (Content Translation in core, Paragraphs and Inline Entity Form in contrib) use this information - and in particular the equality of the two values - to determine whether they are acting on a translation form or not fixing this bug leads to the problem that after changing the language and performing an Ajax operation they think that they are operating on a translation form.

Proposed resolution

The original fix

Ensure the $fields array of content entities is actually cloned in ContentEntityBase::__clone() by overwriting the original reference with one pointing to a copy of the array.

Updating the entity's default langcode in form state

Due to the above we explicitly update the stored entity default language in the form state in ContentEntityForm::updateFormLangcode(). This means that modules can now safely rely on $form_state->isDefaultFormLangcode() again to determine whether they are on a translation form or not. This fix has been verified both with Paragraphs and Inline Entity Form.

As said above, disabling the change of langcode(s) altogether will be discussed in #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls and is out of scope here. This has been discussed and agreed upon by @Berdir, @hchonov, and @tstoeckler.

Fix ContentTranslationHandler::entityFormAlter()

ContentTranslationHandler::entityFormAlter() is updated to (consistently) use $form_state->isDefaultFormLangcode(). This could be a considered a clean-up or consistency fix in its own right, but needs to be fixed in the scope of this issue due to the above.

Remaining tasks

None.

User interface changes

None.

API changes

In the case described above $form_state->get('entity_default_langcode') is now updated to the changed language where it would not have been before. This is an acceptable change as it returned something that was not sensible before. In fact both Content Translation and Inline Entity Form expect the new value, so this change in fact fixes bugs in both of those modules rather than causing any disruption. Paragraphs contains crazy workarounds because it is aware of this bug, but it can be simplified a lot after this goes in (and as stated above it does not break with this change in any way).

Data model changes

None.

CommentFileSizeAuthor
#79 cloned_entity_will-2675010-79.patch15.11 KBBerdir
#72 cloned_entity_will-2675010-72-interdiff.txt4.08 KBBerdir
#72 cloned_entity_will-2675010-72.patch15.38 KBBerdir
#61 cloned_entity_will-2675010-61-interdiff.txt3.37 KBBerdir
#61 cloned_entity_will-2675010-61.patch12.28 KBBerdir
#57 cloned_entity_will-2675010-57-interdiff.txt1001 bytesBerdir
#57 cloned_entity_will-2675010-57.patch14.84 KBBerdir
#49 2675010-49-solution-from-comment-36-with-tests.patch16.49 KBhchonov
#49 2675010-49-solution-from-comment-27-with-tests.patch16.51 KBhchonov
#49 interdiff-41-49-tests.txt3.06 KBhchonov
#49 2675010-49-tests-only.patch14.13 KBhchonov
#41 interdiff-41-solution-from-comment-27-solution-from-comment-36.txt2.27 KBhchonov
#41 2675010-41-solution-from-comment-36-with-tests.patch13.87 KBhchonov
#41 2675010-41-solution-from-comment-27-with-tests.patch14.21 KBhchonov
#41 2675010-41-tests-only.patch11.51 KBhchonov
#36 2675010-36-interdiff.txt2.52 KBBerdir
#36 2675010-36.patch8.07 KBBerdir
#27 2675010-27.patch8.37 KBhchonov
#22 cloned_entity_will-2675010-22.patch3.62 KBjohnchque
#22 cloned_entity_will-2675010-22-test-fail.patch4.41 KBjohnchque
#6 interdiff-3-6.patch3.7 KBhchonov
#6 2675010-6.patch2.83 KBhchonov
#3 2675010-3.patch2.81 KBhchonov
#2 2675010-2-failing-test.patch2.02 KBhchonov
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hchonov created an issue. See original summary.

hchonov’s picture

hchonov’s picture

Issue summary: View changes
FileSize
2.81 KB

The problem was that the cloned entity and the original pointed to the same fields array...

The last submitted patch, 2: 2675010-2-failing-test.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.

hchonov’s picture

Status: Needs review » Needs work

The last submitted patch, 6: interdiff-3-6.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review

oups sorry for by mistake giving the interdiff the extension .patch....

hchonov’s picture

Issue tags: +D8MI, +DevDaysMilan
hchonov’s picture

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +language-content, +sprint

Looks like a very strange bug, but a straightforward solution. The code is already doint the same for translations:

      // Ensure the translations array is actually cloned by overwriting the
      // original reference with one pointing to a copy of the array.
      $this->clearTranslationCache();
      $translations = $this->translations;
      $this->translations = &$translations;
alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 5e942cf and pushed to 8.1.x and 8.2.x. Thanks!

  • alexpott committed fe08d93 on 8.2.x
    Issue #2675010 by hchonov: Cloned entity will point to the same field...

  • alexpott committed 5e942cf on 8.1.x
    Issue #2675010 by hchonov: Cloned entity will point to the same field...
johnchque’s picture

After this commit the tests for Paragraphs got broken. https://www.drupal.org/pift-ci-job/345975
Is there any change we should make after this commit?

johnchque’s picture

Testing a bit found the steps to reproduce.
1. Enable translation for article
2. Create an English article
3. Translate it to German and save
4. Edit the English translation, first change the language to french and add an image
5. Node supposes to be in French but remains in English
I think it can be something related with ajax, because in the step 4 we add the image first and then change the language to french, the change is saved.

Should we open a followup issue or revert this commit?

Gábor Hojtsy’s picture

@yongt9412: sounds like a pre-extisting bug that only existed due to this bug obscuring it to me?!

Berdir’s picture

It might or might not have been a pre-existing problem, but it did *not* happen like that before this commit.

Given that that is IMHO a more severe problem than what was fixed here (I can't see any immediate user-facing bugs here, if there are, they should possibly be listed), I'm wondering if reverting and making sure we have test coverage for that scenario isn't the better approach.

alexpott’s picture

Status: Fixed » Needs work
Issue tags: +Needs tests

That bug does seem to be worse than what we've fixed. It needs investigation and resolution.

  • alexpott committed ffd027b on 8.2.x
    Revert "Issue #2675010 by hchonov: Cloned entity will point to the same...

  • alexpott committed f300d0e on 8.1.x
    Revert "Issue #2675010 by hchonov: Cloned entity will point to the same...
johnchque’s picture

I added some tests. Let's see what test bot thinks.

The last submitted patch, 22: cloned_entity_will-2675010-22-test-fail.patch, failed testing.

hchonov’s picture

@yongt9412 in the failing test you create the node in english and afterwards you translate it to french but the failing assertion is that the german translation should exist, which is impossible. Would you please verify your test?

Berdir’s picture

The node is created in english, then translated to french, then the english translation is edited and the language is changed to german. As a result, german and french has to exist, not english.

Not impossible, possible and passing on HEAD :)

hchonov’s picture

Ok, I get it. So it is basically a problem when using ajax calls, which means the problem is reproducible also if you make the body field multiple and change the language and then click on "Add new item". Crazy... looking further into this...

hchonov’s picture

Sooooo the problem is ContentEntityForm::updateFormLangcode, which acts as an entity builder, but what it does is that it changes the form lang code in the form state. After changing the form lang code and executing an ajax callback the form will be rebuild and validated and after it has been validated the form lang code will be changed in the form state before the form is rebuild again before sending a response. Removing this entity builder makes both tests pass but probably now other ones will fail.

This entity builder was introduced in #2230637: Create a Language field widget and the related formatter.

Please correct me if I am wrong but the form lang code should remain still as the original language for which the form has been built and ajax calls should not change it, as the form is still shown for the original value and it remains so until the whole form is submitted.

hchonov’s picture

So I've just made sure the problem exists as well in the current head and made an issue for fixing it outside this one. When the other one is fixed we could revert the revert here.

#2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls

Gábor Hojtsy’s picture

Issue tags: -Needs tests

Yay, thanks @hchonov, you are again amazing :)

hchonov’s picture

Just to explain what happens in core without this fix :
When you execute an ajax call the form will be first validated and the entity fields will be updated with the submitted user values. As the fields are not properly cloned the entity which will now be used to rebuild the form will have as values the ones submitted by the user, which means now when the form is cached after the first ajax call it will be cached with the user values at each place where we have for example #value or #default_value instead with the original entity values!

The problem @yongt9412 described happens however because of the ContentTranslationHandler::entityFormAlter, which if we are editing an translation (detected with $is_translation = !$form_object->isDefaultFormLangcode($form_state);) will set the #access for the language widget to FALSE. And now when the form language code is changed because of the entity builder ContentTranslationHandler::entityFormAlter will set the #access to FALSE for the language widget. This means that the form_state value of the language widget will be retrieved from the #default_value of the language widget instead of the user_input, because the user_input will not be applied because of the #access being set to FALSE.

Your tests have failed because the entity used for generating the form, which will be cached after the ajax call, will be the original entity and not the entity build by the user submitted values and then the default_value used later to set the form_state value of the language widget will be the entity original langcode instead of the one submitted by the user. That is why it is false to change the form language during ajax calls.

With the patch we have the correct behavior and there is the referenced issue which will take care of the form language not being changed.

  • alexpott committed fe08d93 on 8.3.x
    Issue #2675010 by hchonov: Cloned entity will point to the same field...
  • alexpott committed ffd027b on 8.3.x
    Revert "Issue #2675010 by hchonov: Cloned entity will point to the same...

  • alexpott committed fe08d93 on 8.3.x
    Issue #2675010 by hchonov: Cloned entity will point to the same field...
  • alexpott committed ffd027b on 8.3.x
    Revert "Issue #2675010 by hchonov: Cloned entity will point to the same...

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.

tstoeckler’s picture

Marking this as postponed on #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls. As far as I can tell, the latest patch here contains the patch from the other issue, also, so...

tstoeckler’s picture

Issue tags: +Dublin2016
Berdir’s picture

Status: Postponed » Needs review
FileSize
8.07 KB
2.52 KB

Ok, here we go. This is not completely right yet either, but I think this is on a better way. Both ParagraphsTranslationTest and the new test here are passing with this.

Note that the current behavior in the UI is like this:
1. For a entity without translations, we allow setting language to all values.
2. For the original entity language with translations, we allow to set to all languages that do not exist yet.
3. Existing translations can't have their language changed.

I think 2 vs. 3 is a bit strange, they should possibly work in the same way but they don't.

The basic change that I'm making is to fix updateFormLangcode() instead of removing it (based on how I understand it should work). I'm always updating the langcode and I'm also updating the default langcode if that changed. The logic there is a bit strange, I guess I could just unconditionally set it based on untranslated.

This means that changing the language doesn't break isDefaultFormLangcode(), the current behavior there why it was so complicated for us in paragraphs to make it work.

What's weird is that something is wrong with the entity that we build and the entity that is available in hook_form_alter(). Because validate builds a new entity, uses that for validation but doesn't update $this->entity. That means a form_alter that is then called during form rebuild is actually working with the old object. That means content translation still incorrectly changes the title when you switch the language. That seems like an issue that is should be addressed in its own issue. I believe we could actually fix the fact that we build the entity twice (once in validate, once in submit).

The reason why removing the form_state langcode update worked is because of the bug above, when it no longer changed, then it was the same as the old wrong language on the entity, so content_translation actually didn't see a change and considered it to be still the original untranslated language.

What I don't get is the difference between form language and entity language. @plach said in IRC that in 7.x, form language must always be the entity language. The same is now true for 8.x and I don't understand why it should be any different. Of course, then the question is why the form langcode concept exists in the first place. There is one case in \Drupal\content_translation\ContentTranslationHandler::entityFormAlter() where the two are compared, but IMHO, as the comment says, that should simply do a isDefaultFormLangcode() like it does for the language field access.

I didn't run any other content_translation tests, lets see what happens.

The interdiff won't be very useful as it is against the previous patch that removed the updateFormLangcode() method.

Berdir’s picture

I'v confirmed that just doing $this->entity = $entity in ContentEntityForm::validateForm() fixes the page title. I'll experiment with that in #2799637: Document that #entity_builders and overrides of EntityForm::copyFormValuesToEntity() must be idempotent.

hchonov’s picture

Status: Needs review » Needs work

What's weird is that something is wrong with the entity that we build and the entity that is available in hook_form_alter(). Because validate builds a new entity, uses that for validation but doesn't update $this->entity. That means a form_alter that is then called during form rebuild is actually working with the old object. That means content translation still incorrectly changes the title when you switch the language. That seems like an issue that is should be addressed in its own issue. I believe we could actually fix the fact that we build the entity twice (once in validate, once in submit).

That is exactly why $this->entity is not set in the validate function. The form has to be rebuild with the original entity and with its original field values, and the second is why we need to solve the problem with the cloning.
We want to recreate the form as it was originally send to the user during form rebuild, that is why the form alter hooks have to be called with the form array generated the same way and also have the original requested entity in the originally requested translation!

Changing this behaviour is wrong and would cause really a lot more problems in custom and in contrib than by changing the behaviour of altering the form state langcode during ajax calls and form rebuild.

Berdir’s picture

Status: Needs work » Needs review

This patch is actually not yet changing that and I disagreee with that behavior being expected. We are not rebuilding the original form, we are always rebuilding the form based on whatever changes we made.

You're welcome to provide an example as to why rebuilding with the changed entity is wrong and what would fail, so far you only said it is wrong, not why.

The problem is not that we change the langcode, it's that we make inconsistent changes that then confuse content_translation and the helper methods we have on content forms.

Config forms already work like this, only content entity forms are different (and IMHO broken).

hchonov’s picture

You've said that we have to make this change in order for everything to be working properly?

I do not think we are going to get a consensus on this, so we need someone to make the final decision. But still I think setting $this->entity in validate and rebuilding the form with this entity is not correct and changing it might cause really a lot of problems and we cannot guarantee everything will be fine in custom!

hchonov’s picture

So I've added additional test coverage. Now we have the following tests:

  1. ContentEntityCloneTest::testClonedEntityFields - tests the proper cloning of entity fields.
  2. ContentTranslationLanguageChangeTest::testLanguageChange - tests that after changing the language in the language widget, triggering an ajax call and submitting the form, the new language will be set on the entity.
  3. ContentTranslationLanguageChangeTest::testTitleDoesNotChangesOnChangingLanguageWidgetAndTriggeringAjaxCall - tests that after changing the language in the language widget and trigger an ajax call the current page title will not change.
  4. ContentTranslationLanguageChangeTest::testUserInputIsNotLostOnLanguageChange - tests that after changing the language in the language widget and triggering an ajax request under the circumstance that the form is altered for specific languages the user input will not be lost.

So here I am uploading only the tests, the tests with the solution from #27 and the tests with the solution from #36 and an interdiff between both solutions.

As a summary I want to say that my opinion is that the form should always be rebuild the way it has been send to the user initially and afterwards the form builder sets on top of it the user submitted values. It is how it works currently.
If the form had to be built with the entity built based on the new user submitted values then we would had $this->entity set in the validate function so that the form is intentionally rebuilt with the new generated entity based on the user input, but I am pretty sure it has been intentionally left unmodified.

Additionally if we set $this->entity in validate then the form_alter hooks could not access the original entity anymore and retrieve the original entity values! This would be a really big change and all modules relying on retrieving the original entity field values in the form_alter hook would break!

As shown in the new tests changing the form language during ajax requests might even lead to a user input data loss.

The last submitted patch, 41: 2675010-41-tests-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 41: 2675010-41-solution-from-comment-36-with-tests.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
hchonov’s picture

So here I have an another argument if we rebuilt the form with the entity generated with the new values and have reordered multiple fields, then the form array will really differ at the deltas from the page form so addressing delta-0 from the code and sending e.g. an ajax response which wants to do something with this delta-0 it will not be the same element in the code and on the page.

hchonov’s picture

@berdir I just got a test case proving why a content entity form should not be rebuilt with the modified entity, but with the original unmodified one - #2811841: Add test coverage ensuring user input is mapped on the correct form elements when elements are reordered.

tstoeckler’s picture

Opened #2813073: Don't update $this->entity in EntityForm::afterBuild() for the #after_build thing. The short answer is that we have to live with this inconsistency for now.

Will look at #2811841: Add test coverage ensuring user input is mapped on the correct form elements when elements are reordered now...

hchonov’s picture

Issue summary: View changes
hchonov’s picture

As berdir said in #36 :

What I don't get is the difference between form language and entity language. @plach said in IRC that in 7.x, form language must always be the entity language. The same is now true for 8.x and I don't understand why it should be any different. Of course, then the question is why the form langcode concept exists in the first place. There is one case in \Drupal\content_translation\ContentTranslationHandler::entityFormAlter() where the two are compared, but IMHO, as the comment says, that should simply do a isDefaultFormLangcode() like it does for the language field access.

I agree as well that the entity langcode and the form state langcode should be identical, which means that in Drupal 9 we could remove the form state langcode and ContentEntityFormInterface::getFormLangcode could retrieve the language from the entity directly. But changing this behaviour could be left for Drupal 9.

I am extending the tests from #41 to include a test checking that the entity language code and the form state language code remain identical as everything else would create a pure inconsistency.

I also think that unfortunately we could not solve this in 8.2.x anymore.

The last submitted patch, 49: 2675010-49-tests-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 49: 2675010-49-solution-from-comment-36-with-tests.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
hchonov’s picture

We agree that the form and entity language should be identical. There are only two possible ways of achieving this -

1. Do not use the entity builder ContentEntityForm::updateFormLangcode.
2. Continue using the entity builder ContentEntityForm::updateFormLangcode and additionally exchange $this->entity with the entity built based on the user input each time an ajax request is triggered or the form is rebuilt.

However as #2811841: Add test coverage ensuring user input is mapped on the correct form elements when elements are reordered shows content entity forms have to be rebuild with the original unmodified entity, which means 2. is not possible.

Currently only the first proposed resolution handles this correctly.

tstoeckler’s picture

Issue summary: View changes

First stab at expanding the issue summary, will continue later.

hchonov’s picture

Issue summary: View changes
xjm’s picture

Issue tags: +Triaged core major

We also agreed at Dublin that this is a major bug, because of the potential impact on data integrity.

Berdir’s picture

Ok, slowly getting back to this after #2811841: Add test coverage ensuring user input is mapped on the correct form elements when elements are reordered is in. Yay, one small step forward and many things learned :)

Looking at #41 now. My approach has two test fails. The first one about the wrong title is something that I already pointed out in #36:

What I don't get is the difference between form language and entity language. @plach said in IRC that in 7.x, form language must always be the entity language. The same is now true for 8.x and I don't understand why it should be any different. Of course, then the question is why the form langcode concept exists in the first place. There is one case in \Drupal\content_translation\ContentTranslationHandler::entityFormAlter() where the two are compared, but IMHO, as the comment says, that should simply do a isDefaultFormLangcode() like it does for the language field access.

So doing exactly that makes that test pass, also with my approach. What we need for that to work is consistency. In your approach, it is consistently the old original langcode, in my case, it is consistently the new langcode, both cases result in it correctly detecting a translation vs editing the original.

Attaching a patch with that fix to demonstrate that. *This is not a final patch.*

The second is more interesting. It fees a bit like that is specifically written to fail with my approach. Which might be OK if that is an actual valid use case, but I'm not convinced about that yet. Lets look a bit at that test:

1. It adds a custom field that is only visible when the language is either EN or FR.
2. On editing an existing entity in the default translation (EN), we change the language to german.***
3. Then we save.
4. And now we ensure that the value, which is only supposed be there for EN/FR was saved when saving the entity as a german translation?

Yes, that fails with my code, but a) It would be possible to write code that wouldn't fail (for example currently, by getting the language of the entity, not the form language) and b) That logically doesn't make sense to me. Yes, when using ajax, that field is still visible but that's a bug in that specific logic and it would have to find a way to update it. For example with #states on the langcode field, or trying to find a way to attach another ajax command to the response. It also seems more like a a test case for #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls than for this issue.

*** As also commented in #36, the fact that you can change the language of the original language but not of a translation seems very strange to me and I think we should consider to remove that feature. Or allow to change the language of a translation too. The current logic just doesn't make too much sense there.

In #41, @hchronov wrote this:

If the form had to be built with the entity built based on the new user submitted values then we would had $this->entity set in the validate function so that the form is intentionally rebuilt with the new generated entity based on the user input, but I am pretty sure it has been intentionally left unmodified.

You might be putting too much trust in the people who wrote and reviewed that code (which includes me, so I'm allowed to say that I think :)). There clearly are obvious bugs with form langcode stuff, whether or not it should exist at all or not. So what exactly is intentional and what isn't really isn't clear.

Here is a suggestion for a compromise to continue moving forward in small steps:

Lets leave it for #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls to decide if we should do that or not. What is clear is that a) right now the call exists and there are contrib modules like paragraphs relying on that (@tstoeckler is working on code that doesn't need that anymore, which is great but it's not quite finished yet). b) Its implementation is broken and doesn't handle repeated builds at all.

My patch keeps the status quo in keeping that call and it fixes bugs in the current implementation which allows us to fix the clone bug here without introducing new regressions in core or risking them in contrib/custom code.

I also never proposed to do "I believe we could actually fix the fact that we build the entity twice (once in validate, once in submit)." in this issue, I was just thinking out loud and #2799637: Document that #entity_builders and overrides of EntityForm::copyFormValuesToEntity() must be idempotent already proved that we can't do that, and we have #2813073: Don't update $this->entity in EntityForm::afterBuild() to investigate that part.

Given that, my suggestion would be to do this issue by fixing updateFormLangcode() and leave out the test coverage from #41 and #49 and move that to the mentioned issue instead. 2675010-49-tests-only.patch AFAIK proves that the things tested by it are failing in HEAD as well and are *not* caused by my suggested (maybe temporary fix) fix for updateFormLangcode.

Status: Needs review » Needs work

The last submitted patch, 57: cloned_entity_will-2675010-57.patch, failed testing.

tstoeckler’s picture

Thank you for the update!!

I'm not yet sure about the validity of the test, so I will not (yet) comment on it in either direction.

I am in general fine with the plan. It is based on the assumption, though, that the remaining test failure is invalid, which at least in my brain has yet to be determined ;-).

I would like to fix the form langcode handling and the content translation stuff in a separate issue, but as far as I can tell that's not actually possible, as that is directly influenced by the bug at hand. :-/ So (again under the above assumption) I think your patch is fine.

I am fine with the rest happening in #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls. I am by now fairly certain that we want to remove usages of and deprecate getFormLangcode() but I guess that's something to be discussed over there.

hchonov’s picture

I would be fine with moving the discussion to the other issue what are we going to do with the form state langcode and the currently failing test as well.

However about Paragraphs and the fails there:

After investigating further what actually happens I see that the paragraphs module relies on that the form state langcode is changed when the entity is first built in the validation phase and uses it later in submit to build the entities with the new langcode which means the entity generated in validate and in submit differs which means you are saving an entity which has not been validated. This is why I am against updating the langcode in the form state as it leads to such inconsistencies. The paragraph entities build in the validation phase have different langcode than the ones in the submit phase and I would say this is major if not critical for the module because you are saving an entity/entities which has/have not been validated.

I have also another issue which would cause the same failures as this one here because of this fact - #2833682: In submit use the entity built during the validation instead of building it again - for performance and consistency ensuring we are saving the entity that has been validated.

So I think we should fine a solution for what Paragraphs is doing sooner than later.

Otherwise you could remove/move the failing test to the other issue.

I still have one question regarding the current patch:

+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -166,7 +166,6 @@ protected function flagViolations(EntityConstraintViolationListInterface $violat
-    $this->initFormLangcodes($form_state);

This looks like out of scope of the current issue or why are you removing it?

Berdir’s picture

Ok, removed the failing test method. And restored that method call. I did that because the next thing we do is calling getFormLangcode(), which itself calls initFormLangcodes() again, so it really is pointless. But yeah, not related/required, so removed that changed.

Not sure about keeping the the en_fr_only field in the test, it is still used in testTitleDoesNotChangesOnChangingLanguageWidgetAndTriggeringAjaxCall() but only to check the initial value, which I think isn't too useful but kept it for now.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Thank you!

Today is a good day.

hchonov’s picture

After looking at this again I have one more question.

+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -266,10 +266,13 @@ public function setFormDisplay(EntityFormDisplayInterface $form_display, FormSta
+    // If this is the original entity language, also update the default
+    // langcode.
+    if ($langcode == $entity->getUntranslated()->language()->getId()) {
+      $form_state->set('entity_default_langcode', $langcode);
     }

Why are we doing this here? I mean we do set the entity_default_langcode in the form state through ContentEntityForm::init->ContentEntityForm::initFormLangcodes, so I would say we do not have to do it here as well, right?

Not sure but maybe I've figured it out. It is needed because after the langcode in the LanguageWidget is changed then maybe the language selected there is the new default language code? I have now really my concerns what we do here again .... :( What if at some place I do
$storage->loadUnchanged($entity->id())->getTranslation($form_state->get('entity_default_langcode'));?
This would cause exceptions now... I am actually for seeing this RTBC and landing in core but I have the feeling now we create another problem and break actually BC.

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update, +Needs change record

That is a fair point. I still think this should go in the way it currently is, because the behavior change this introduces is less of an API-change than what it otherwise would be. Instead of explaining that in detail in this comment, though, this should be explained in the issue summary and in a change record, so marking needs work for that. Will try to get on that today/tomorrow.

I also just found that Inline Entity Form uses $form_state->get('entity_default_langcode') as well, so we need to check whether we haven't broken anything. It seems that Inline Entity Form suffers from a similar problem as core's Content Translation in that it currently sometimes thinks that the form is a translation form although it actually isn't and that this issue would (not so) magically fix that. But that needs to be verified before this can go in.

hchonov’s picture

\Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormComplex::formElement has the following;

$row['actions']['ief_entity_remove']['#access'] = !$element['#translating']

and they define #translating through \Drupal\inline_entity_form\TranslationHelper::isTranslating with

$form_state->get('langcode') != $form_state->get('entity_default_langcode')

There also other places where element['#translating'] is being used in the module.

And as we used to change only the form state langcode and not the form state entity_default_langcode until now they would have considered that the form is translating but with the change that we would make they would not consider anymore the form being translating.

So what currently happens is in order not to break Paragraphs we break Inline Entity Form. No matter how we look at this, one of the modules has to adapt with the one or with the other approach. I think this only proves how wrong changing the language code is no matter if it is the form state langcode or the form state entity_default_langcode and I would simply stop changing it then both modules have to find another ways of determining correctly if the language code in the language widget has been changed.

tstoeckler’s picture

So what currently happens is in order not to break Paragraphs we break Inline Entity Form.

That's not true, at least not necessarily. As I've explained it might also be that this actually fixes a bug in Inline Entity Form. But we definitely need to find out which it is before proceeding here.

Berdir’s picture

I am updating the default langcode exactly to make $form_state->get('langcode') != $form_state->get('entity_default_langcode') work as expected. That's exactly what \Drupal\Core\Entity\ContentEntityForm::isDefaultFormLangcode() does and it now works as expected.

We might be changing/affecting IEF but IMHO in a positive way. Changing the language does *not* mean we are translating, it means we are changing the language, we are still editing the default language.

That remove access is the same thing as paragraphs does. You're only allowed to remove if you are not editing a tranlsation. You are still allowed access to delete when you change the language.

The fact that isDefaultFormLangcode() was broken and inconsistent before is why paragraphs did not use that and is exactly the bug that I am fixing in updateFormLangcode().

hchonov’s picture

Well if you are sure we are not breaking IEF then I am fine with the change. At least now it is clear that we have two approaches for fixing the bug with changing the language in the LanguageWidget:

1. Stop updating the form state langcode in the entity build ContentEntityForm::updateFormLangcode
or
2. Continue updating the form state langcode in ContentEntityForm::updateFormLangcode but also update the form state entity_default_langcode to the new langcode if we have changed the langcode of the default translation.

1. actually should be the correct decision, plach agreed on it as well, but it is breaking Paragraphs this is why we have decided to go with 2. for now and continue the discussion about 1. in #2757003: Entity builder ContentEntityForm::updateFormLangcode changes the form language code during ajax calls.

plach’s picture

It seems the latest patch is now failing badly...

Berdir’s picture

Had a look at the fails but didn't figure out the problem yet.

Looks like it saves a new revision, but the $entity->getUntranslated() still has a revision_id. Why I have no idea, it is an untranslatable field, so shouldn't matter. Maybe something weird happen with cloning and the reference to the translation object?

According to git bisect, it is #2669802: Add a content entity form which allows to set revisions that broke this.

hchonov’s picture

I haven't looked at it yet, but what @berdir is saying looks like #2828073: Cloning an entity with initialized translations leaves $entityKeys and $translatableEntityKeys pointing to the old entity object. Probably we should merge both patches and see if the tests pass again.

Berdir’s picture

Indeed, thanks! This fixes at least one of those test fails. Lets see about the others.

This is the combined patch, had a small conflict on the test.

tstoeckler’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
tstoeckler’s picture

Updated the issue summary and added a change notice.

I also verified that the Inline Entity Form tests pass with this applied.

-> RTBC!!

I haven't provided any patches here, and this patch has been approved by both @Berdir and @hchonov so I am comfortable pulling the trigger on this.

Thanks to the two of you for all your time on this and your persistence and of course to everyone else involved with this, as well.

alexpott’s picture

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

Committed 4b65a2b and pushed to 8.3.x. Thanks!

I'm not committing to 8.2.x just yet because of all the earlier issues.

diff --git a/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module b/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module
index 7093947..d07549d 100644
--- a/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module
+++ b/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module
@@ -1,5 +1,10 @@
 <?php
 
+/**
+ * @file
+ * Helper module for the Content Translation tests.
+ */
+
 use \Drupal\Core\Form\FormStateInterface;
 
 /**

Fixed missing docblock on commit.

  • alexpott committed 4b65a2b on 8.3.x
    Issue #2675010 by hchonov, Berdir, yongt9412, tstoeckler: Cloned entity...

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 72: cloned_entity_will-2675010-72.patch, failed testing.

Berdir’s picture

Status: Needs work » Reviewed & tested by the community

Testbot tried to re-test on 8.3.x. Submitted a test run for 8.2.x to see if it passes there, not 100% sure if we should commit it to the stable branch..

Berdir’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
15.11 KB

Ok, conflicted with the loadedRevisionId property.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Patch looks good.

  • alexpott committed fe08d93 on 8.4.x
    Issue #2675010 by hchonov: Cloned entity will point to the same field...
  • alexpott committed ffd027b on 8.4.x
    Revert "Issue #2675010 by hchonov: Cloned entity will point to the same...
  • alexpott committed 4b65a2b on 8.4.x
    Issue #2675010 by hchonov, Berdir, yongt9412, tstoeckler: Cloned entity...

  • alexpott committed fe08d93 on 8.4.x
    Issue #2675010 by hchonov: Cloned entity will point to the same field...
  • alexpott committed ffd027b on 8.4.x
    Revert "Issue #2675010 by hchonov: Cloned entity will point to the same...
  • alexpott committed 4b65a2b on 8.4.x
    Issue #2675010 by hchonov, Berdir, yongt9412, tstoeckler: Cloned entity...

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.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

There is not going to be another 8.2.x release. This is in 8.3.x and 8.4.x

Status: Fixed » Closed (fixed)

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