Follow-up of #2382493: Population of default field values in entity translation is incorrect

Problem/Motivation

The current handling of default values in ContentEntityBase::addTranslation() feels very inefficient: it creates a new fresh entity that gets default values, only to be able to copy those values over to the new translation. Additionally hook_entity_create() is called when creating an entity translation which is incorrect.

Proposed resolution

  • Make sure ContentEntityStorageBase::doCreate($values) and ContentEntityBase::addTranslation($values) reuse the same code for the "merge default values into the passed in $values".
  • Introduce a new hook_entity_field_values_init() hook to allow to alter these values regardless of whether we are initializing a new entity or a new entity translation.
  • Introduce a new hook_entity_translation_create() hook to allow to react to entity translation creation.

Remaining tasks

  • Validate the proposed solution
  • Reviews
  • Evaluate whether it can be committed during the RC phase

User interface changes

None

API changes

Mostly additive:

  • ContentEntityStorageBase now implements a new ContentEntityStorageInterface.
  • ContentEntityType::setStorageClass() throws an \InvalidArgumentException if the entity storage handler does not implement ContentEntityStorageInterface.
  • The TranslatableInterface::isNewTranslation() method was added.

Data model changes

None

CommentFileSizeAuthor
#67 diagram.pdf33.73 KBalexpott
#63 2382675-2-63.patch24.16 KBalexpott
#63 31-63-interdiff.txt7.54 KBalexpott
#61 drupalci-vagrant.log_.txt13.42 KBplach
#56 mysql_error.txt58.33 KBhchonov
#56 test_verbose.txt30.68 KBhchonov
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.patch20.84 KBplach
#50 et-translation_create-2382675-50.interdiff.txt1.02 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#40 et-translation_create-2382675-40.patch20.53 KBplach
#31 et-translation_create-2382675-31.patch20.53 KBplach
#31 et-translation_create-2382675-31.interdiff.txt3.2 KBplach
#27 et-translation_create-2382675-27.patch17.33 KBplach
#27 et-translation_create-2382675-27.interdiff.txt4.89 KBplach
#23 et-translation_create-2382675-23.patch13.86 KBplach
#21 et-translation_create-2382675-21.interdiff.txt588 bytesplach
#21 et-translation_create-2382675-21.patch13.86 KBplach
#20 et-translation_create-2382675-20.patch14.13 KBplach
#20 et-translation_create-2382675-20.interdiff.txt9.51 KBplach
#19 et-translation_create-2382675-19.patch6.37 KBplach
#6 2382675-Entity_applyDefaultValues-6.patch1.77 KByched
#3 2382675-Entity_applyDefaultValues-3.patch3.33 KByched
#1 2382675-Entity_applyDefaultValues-1.patch1 KByched

Comments

yched’s picture

Status: Active » Needs review
StatusFileSize
new1 KB

Maybe something like this ?

- ContentEntityStorageBase::doCreate($values) only takes care of applying the values that were passed in.
- default field values are applied by ContentEntityBase::postCreate() and ContentEntityBase::addTranslation()

Status: Needs review » Needs work

The last submitted patch, 1: 2382675-Entity_applyDefaultValues-1.patch, failed testing.

yched’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new3.33 KB

Oops, wrong git flow, don't mind the patch in #1.

Here is the correct patch.

Status: Needs review » Needs work

The last submitted patch, 3: 2382675-Entity_applyDefaultValues-3.patch, failed testing.

yched’s picture

Damn - the test fail is because $field->isEmpty() cannot be trusted until we fix #2164601: Stop auto-creating FieldItems on mere reading of $entity->field[N] :-/

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new1.77 KB

Well, so much for reusing then (we don't receive $values in Entity::postCreate()).

Different approach, 4 lines of code are duplicated between ContenetEntityBase::addTranslation() and ContentEntityStorageBase::doCreate(), no biggie.

plach’s picture

plach’s picture

Status: Needs review » Needs work

During the last #drupal-entity IRC meeting we agreed to introduce a new hook to be called in addition to hook_entity_create() to allow to manipulate field default values without having to differentiate between entity creation and translation creation. The rationale is that there is no difference in initializing an entity object or a translation object wrt initial/default values. Suggested signatures:

hook_entity_field_default_values_alter($values, $langcode)
hook_entity_field_values_init($values, $langcode)
(others?)

yched’s picture

Title: Optimize ContentEntityBase::addTranslation() » ContentEntityBase::addTranslation() should not fire hook_entity_create()

Updating title to more accurately reflect the issue at hand

fago’s picture

Priority: Normal » Major

Yep - the wrong hook being fired is the main issue that we have to fix here imo. Bumping priority for that.

plach’s picture

Issue tags: +rc deadline

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 6: 2382675-Entity_applyDefaultValues-6.patch, failed testing.

catch’s picture

Issue tags: -rc deadline +rc target triage
catch’s picture

We need to decide if simply not invoking a hook, from the wrong place, is an API break.

If you rely on that behaviour, your module might break, but the behaviour is not supposed to happen at all in the first place.

plach’s picture

Issue tags: +lanaguage-content, +API addition

We need to decide if simply not invoking a hook, from the wrong place, is an API break.
If you rely on that behaviour, your module might break, but the behaviour is not supposed to happen at all in the first place.

Yep, form my POV it is not an API break, it's a plain bug fix since the intended behavior would be preserved by introducing a new hook and the buggy behavior would be removed.

plach’s picture

Assigned: Unassigned » plach
Issue tags: +sprint

Working on this

plach’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new6.37 KB

First attempt at implementing #8.

plach’s picture

StatusFileSize
new9.51 KB
new14.13 KB

Spoke with @Berdir, this patch addresses also the related issue pointed out in https://github.com/md-systems/pathauto/issues/90#issuecomment-154173677. We agreed that a single issue makes sense in this case, since we are basically completing the Entity Translation API wrt entity translation creation.

Reviews welcome :)

plach’s picture

StatusFileSize
new13.86 KB
new588 bytes
+++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php
@@ -302,6 +302,7 @@ function testEntityTranslationAPI() {
+    $this->doTestEntityTranslationAPI($entity_type);

Unrelated

penyaskito’s picture

Issue tags: -lanaguage-content +language-content

Fixed typo in tags

plach’s picture

StatusFileSize
new13.86 KB

Rerolled, reviews welcome...

hchonov’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -822,37 +829,15 @@ public function addTranslation($langcode, array $values = array()) {
+    if ($storage instanceof ContentEntityStorageInterface) {
+      $this->translations[$langcode]['status'] = static::TRANSLATION_CREATED;
+      return $storage->createTranslation($this, $langcode, $values);
     }
...
+    else {
+      throw new \LogicException('The storage handler cannot create entity translations.');
     }

Why do we have to check here if the storage for the content entity implements the ContentEntityStorageInterace and do not instead enforce that content entity types define a storage class implementing the ContentEntityStorageInterace? We do exactly this in ConfigEntityType::__construct, where we enforce the defined storage class to be an instance of ConfigEntityStorage.

plach’s picture

That's a good suggestion, but I'm not sure we can still do that.

hchonov’s picture

I guess your concerns are that if we do the check in the ContentEntityType::__construct then there will be a greater impact, but actually no matter where we do it there is going to be an impact. Doing it in the ContentEntityBase::addTranslation would mean we are going anyway to force anyone having a custom storage not extending from ContentEntityStorageBase to implement the new interface as soon as they wann create new translations. And actually doing this check in ContentEntityBase::addTranslation will hide the change for a longer time.

So I think that if we are going to do this change then we have to do it the right way even if there is a disruption.

plach’s picture

StatusFileSize
new4.89 KB
new17.33 KB

Implemented #24, let's see whether the bot is happy.

catch’s picture

This looks great to me, and the sort of change we can't make in a patch release, so I'd support trying to get it in before 8.0.0.

I think we could make this change in 8.1.x, but there's more chance of modules relying on the current, buggy, behaviour by then than if we do it now.

Status: Needs review » Needs work

The last submitted patch, 27: et-translation_create-2382675-27.patch, failed testing.

hchonov’s picture

So, I guess we need a separate key value storage for content entities as well...

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new3.2 KB
new20.53 KB

Rrright

plach’s picture

Issue summary: View changes
hchonov’s picture

Assigned: plach » Unassigned
Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs change record

Mostly additive changes, a dedicated interface for content entity storage and a new hook_entity_field_values_init() hook to unify entity_create and translation_create. Nice work.

Everything looks good to me.

plach’s picture

  • catch committed 58332b2 on 8.0.x
    Issue #2382675 by plach, yched: ContentEntityBase::addTranslation()...
catch’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -rc target triage +rc target

Discussed with xjm and alexpott. Did't quite finish conversation with xjm, but alexpott agreed with getting this in before 8.0.0l on the basis the chance of disruption gets higher the longer core is doing the wrong thing,

Committed/pushed to 8.0.x, thanks!

plach’s picture

Thanks, published the change records!

alexpott’s picture

  • alexpott committed 4f9265c on 8.0.x
    Revert "Issue #2382675 by plach, yched: ContentEntityBase::...
plach’s picture

The last submitted patch, 40: et-translation_create-2382675-40.patch, failed testing.

The last submitted patch, 40: et-translation_create-2382675-40.patch, failed testing.

The last submitted patch, 40: et-translation_create-2382675-40.patch, failed testing.

The last submitted patch, 40: et-translation_create-2382675-40.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 40: et-translation_create-2382675-40.patch, failed testing.

hchonov’s picture

I've just executed locally 15 times the following without a single failure:
php core/scripts/run-tests.sh --url "http://dev-d8.nauts.eu/" --class "\Drupal\system\Tests\Update\AutomatedCronUpdateWithAutomatedCronTest"

And this is the test, which is failing more than once on the test bot.....

hchonov’s picture

An interessting fact as well is that all the tests that have failed on the test bot are using the dump drupal-8.bare.standard.php.gz.

berdir’s picture

One thing that has caused problems in the past on testbot is that it runs many tests in parallel. If you want to replicate that behavior, try something like --repeat 20 --concurrency 5 or even higher numbers.

But something like this, which smells like a segfault is also always very system/version specific. Any chance that we can get at the apache/php logs of the failing containers somehow?

plach’s picture

Making some experiments in #2496337-78: [plach] Testing issue.

The last submitted patch, 50: et-translation_create-2382675-50.patch, failed testing.

The last submitted patch, 50: et-translation_create-2382675-50.patch, failed testing.

The last submitted patch, 50: et-translation_create-2382675-50.patch, failed testing.

The last submitted patch, 50: et-translation_create-2382675-50.patch, failed testing.

hchonov’s picture

Just atm I've tested locally but I gave by mistake the wrong --url and suprise... I got exactly the same errors like on the testbot...

Any clue?

hchonov’s picture

StatusFileSize
new30.68 KB
new58.33 KB

as requested by @plach in IRC providing logs when submitting wrong --url

alexpott’s picture

I thought I was on to something but I was wrong. However looking at this patch I think think interface inheritance is wrong. ContentEntityStorageInterface extends DynamicallyFieldableEntityStorageInterface? I don't see why that should be the case. Shouldn't ContentEntityStorageInterface extends EntityStorageInterface and then ContentEntityStorageBase implement both ContentEntityStorageInterface and DynamicallyFieldableEntityStorageInterface?

plach’s picture

I modeled it after ContentEntityInterface...

plach’s picture

I set up a local DrupalCI environment with Vagrant but no luck, the tests pass even with the exact same PHP version (5.5.23) on Ubuntu Trusty. Attached you can find the log...

plach’s picture

plach’s picture

StatusFileSize
new13.42 KB

The log...

alexpott’s picture

@plach so what this might be is something to do with APC and concurrency - we did have random fails before to do with Entity storage classes - see #731724-527: Convert comment settings into a field to make them work with CMI and non-node entities

alexpott’s picture

StatusFileSize
new7.54 KB
new24.16 KB

Here's a patch that shuffles the interfaces as suggested by #57 and doesn't suffer from the obscure random fail - see #2496337-122: [plach] Testing issue

plach’s picture

The interdiff looks good to me, RTBC +1.

plach’s picture

Status: Needs review » Reviewed & tested by the community

Changes are quite simple so I think it's ok for me to move this back to RTBC.

hchonov’s picture

+1 RTBC

alexpott’s picture

StatusFileSize
new33.73 KB

Here's the class diagram of everything that implements EntityStorageInterface on applying the patch. Note how the ContentEntityStorageInterface and ConfigEntityStorageInterface nicely imitate each other now. It looks correct to me.

effulgentsia’s picture

Title: ContentEntityBase::addTranslation() should not fire hook_entity_create() » hook_entity_create() affects the data of new translations of existing entities in unexpected and undocumented ways
Category: Task » Bug report
Issue tags: -rc target +8.0.0 target

I discussed this with the other committers, and we decided to allow this into 8.0.0, because:

  • It was already committed and only reverted for a tangential reason that has since been resolved.
  • We couldn't come up with an idea for how to do this in a minor release in a way that wouldn't introduce a slew of other problems with respect to BC.
  • Leaving this unsolved for all of 8.x is problematic due to the likelihood based on past experience of modules (contrib and custom) being written without getting sufficiently tested in multilingual contexts, but then used on multilingual sites. And this WTF of hook_entity_create() is particularly likely for such modules to overlook (even if we document it) and as a result corrupt translations of those sites.

So, tagging, and also recategorizing as a bug and retitling accordingly.

  • effulgentsia committed 2090a96 on 8.0.x
    Issue #2382675 by plach, yched, alexpott: hook_entity_create() affects...
effulgentsia’s picture

Status: Reviewed & tested by the community » Fixed

I pushed #63 to 8.0.x. However, the following nits could use a follow-up issue/patch:

  1. +++ b/core/lib/Drupal/Core/Entity/EntityType.php
    @@ -459,10 +462,21 @@ public function getStorageClass() {
    +   * Checks that the provided class is an instance of ConfigEntityStorage.
    ...
    +  protected function checkStorageClass($class) {
    +    // Nothing to check by default.
    +  }
    

    Doc doesn't match implementation.

  2. +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php
    @@ -0,0 +1,25 @@
    +    // @todo
    

    What should be to-done?

effulgentsia’s picture

I also think it would be great to have a follow-up issue to add an integration test for what happens when hook_entity_create(), hook_entity_field_values_init(), and hook_entity_translation_create() are all implemented: i.e., that hook_entity_field_values_init() affects both entities and translations, and that the other two hooks only affect the corresponding one. Such a test should also test the expected order in which those hooks fire (i.e., hook_entity_field_values_init() before *_create()).

plach’s picture

Assigned: Unassigned » plach

Thanks!

I'm creating the follow-ups.

  • effulgentsia committed 2090a96 on 8.1.x
    Issue #2382675 by plach, yched, alexpott: hook_entity_create() affects...
  • alexpott committed 4f9265c on 8.1.x
    Revert "Issue #2382675 by plach, yched: ContentEntityBase::...
  • catch committed 58332b2 on 8.1.x
    Issue #2382675 by plach, yched: ContentEntityBase::addTranslation()...
gábor hojtsy’s picture

Issue tags: -sprint

Status: Fixed » Closed (fixed)

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