Problem/Motivation
Steps to reproduce:
- Create an entity with two translations.
- Reload the entity.
- Execute
$entity->getTranslation("second_langcode");
- Execute
$clone = clone $entity
- Test that
$entity->field_name === $clone->field_name
. Changing field values in $clone
now also changes the values in $entity
which precisely breaks the contract of the clone
operation.
This affects content entity forms because during validation a clone of the entity is populated with the user submitted values to perform $cloned_entity->validate()
. Due to this bug, after Ajax operations in content entity forms, the values in $this->entity
during subsequent form building are actually the updated values from the user input and not the original entity values.
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. Due to the above problem this means that $this->entity->language()
returns the previously selected language after Ajax requests - which may differ from the original entity language.
ContentEntityForm::updateFormLangcode()
is run as an #entity_builder
, so that $form_state->getLangcode()
will return the previously selected language after Ajax requests. The fact that an #entity_builder
has side effects is problematic, as discussed in #2799637: Document that #entity_builders and overrides of EntityForm::copyFormValuesToEntity() must be idempotent. In this concrete case, however, the problem is masked, because due to the bug described above, $form_state->getFormObject()->getEntity()->language()->getId()
and $form_state->getFormLangcode()
always return the same thing.
Thus, fixing the clone bug leads to the case where the language of the entity and the form langcode are different: The language of the entity is no longer (inadvertently) updated, but the form langcode is still updated by the #entity_builder
. This breaks an expectation in the Paragraphs module.
Proposed resolution
Ensure the fields array is actually cloned by overwriting the original reference with one pointing to a copy of the array.
The original solution of only fixing the cloning discovered another bug and it is that when cloning properly then the form will be rebuild with the original entity instead of the modified one (which has been caused by the wrongly cloning). The problem relies in the entity builder ContentEntityForm::updateFormLangcode which is not idempotent and will change the langcode stored in the form state storage each time an ajax call is triggered and the user has changed the language in the language widget when ContentEntityForm::validate is executed and the entity build for validation with the user submitted values. So what we have now is that we are not setting $this->entity generated based on the user input on ajax calls but we are updating the form state langcode based on this intermediate entity which we are throwing away. Setting $this->entity is impossible as #2811841: Add test coverage ensuring user input is mapped on the correct form elements when elements are reordered currently shows.
We have currently two approaches which we are discussing:
1. Do not use the entity builder ContentEntityForm::updateFormLangcode as it is not idempotent, leave the function there and mark it as deprecated as of Drupal 9.0.0. This approach assumes as we are not setting $this->entity and rebuilding the form with the original entity that the form language is still the original one and if some code used to rely on changing the language in the form state the new language from the language widget has to be retrieved using $form_state->getValue($entity->getEntityType()->getKey("langcode")). This approach solves all the provided tests.
2. Always update the form state language code even if not setting $this->entity and continue using the entity builder ContentEntityForm::updateFormLangcode. This approach still has the problem that the title of the page will suddenly be changed by the content_translation module, as on form rebuild the decision would be made that a translation is being edited and [%translatio% translation] will be added to the page title. Additionally during form rebuild the form alter hooks which generated the first form send to the user now will be called with a form state indicating a different language than the one of the entity used now for rebuilding the form.
Remaining tasks
Review.
User interface changes
none.
API changes
none.
Data model changes
none.