Problem/Motivation

  1. Create an entity with two translation A and B
  2. Initialize all the fields for each translation through e.g. $entity->getFields()
  3. Clone the entity in translation B - $clone = clone $entity->getTranslation("B");
  4. Make changes on the cloned entity object as e.g. "$clone->enforceIsNew();"
  5. Now the fields of translation A of the cloned entity still point to the original entity and they get a false result by checking $this->getEntity()->isNew(), which causes e.g. causes php error and breaking the execution in ChangedItem::preSave when calling $entity->original->hasTranslationChanges() assuming $entity->original is set, but it is not as the clone is the entity that is being saved and not the original reference.

Proposed resolution

In ContentEntityBase::__clone clear the translation cache before cloning the fields in order to ensure the cloned fields will get an entity translation reference of the cloned entity instead of the original entity.

The decoupling of the object properties has to be done as well before cloning the fields as this will cause cloning of the clone when initializing the translations of the clone and we want to prevent that ::initializeTranslation will create references to the properties of the original entity.

Remaining tasks

Review & Commit.

User interface changes

None.

API changes

None.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hchonov created an issue. See original summary.

hchonov’s picture

hchonov’s picture

The decoupling of the object properties has to be done as well before cloning the fields as this will cause cloning of the clone when initializing the translations of the clone and we want to prevent that ::initializeTranslation will create references to the properties of the original entity.

hchonov’s picture

Issue summary: View changes

The last submitted patch, 2: 2841311-2-test-with-fix.patch, failed testing.

The last submitted patch, 2: 2841311-2-test-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 3: 2841311-3-test-with-fix.patch, failed testing.

hchonov’s picture

Issue tags: +blocker

The fails here look really strange however the issue here appears to be a blocker for #2839195: Add a method to access the original property .

hchonov’s picture

Is the HEAD failing or what is happening? I've reverted all the changes locally and still getting those fails...

Berdir’s picture

> Class 'Drupal\migrate\Event\MigrateEvents' not found

I've seen that before, some fun stuff about class autoloading... #2776235: Cached autoloader misses cause failures when missed class becomes available and possibly other issues. but that was fixed?

Berdir’s picture

And cached autoloader might also explain why you still get fails even after reverting the changes, as something is still cached in apcu, a restart might help?

hchonov’s picture

Status: Needs work » Needs review

Unfortunately APCu is always disabled on my system as somehow it has issues with OSX. Even though I've restarted, but nothing changed.

When running the ConfigImportAllTest I get only the fails from ConfigImportAllTest:: testInstallUninstall, but none fails such as "Class 'Drupal\migrate\Event\MigrateEvents' not found".

I've run "composer install"

composer install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Nothing to install or update
Generating autoload files
> Drupal\Core\Composer\Composer::preAutoloadDump
> Drupal\Core\Composer\Composer::ensureHtaccess

I even deleted the vendor folder and reinstalled all the dependencies again, but this did not help as well, so I have no idea why my system is still broken.

However the tests are now green here probably because of the revert of #2840596: Update Symfony components to ~2.8.16.

hchonov’s picture

Issue summary: View changes
tstoeckler’s picture

Wow, another beautiful find!

Code-wise it looks perfect. I think we can improve the comments a bit, though.

  1. I think one detail that is missing is pinpointing that the getTranslation() call is the culprit and that the cache needs to be cleared so that new objects get created through initializeTranslation()
  2. The second chunk about the property references is hard to parse, especially the "cloning of the clone" part. It's factually correct, as far as I can tell, I'm not saying it's wrong in any way, I just think it's hard to understand what you mean.

I had a go at trying to improve this:

  1. The translation cache has to be cleared before cloning the fields below so that the call to getTranslation() does not re-use the translation objects of the old entity but instead creates new translation objects from the newly cloned entity. Otherwise the newly cloned field item lists would hold references to the old translation objects in their $parent property after the call to setContext().
  2. Because the new translation objects that are created below are themselves created by *cloning* the newly cloned entity we need to make sure that the references to property values are properly cloned before cloning the fields. Otherwise calling $items->getEntity()->isNew(), for example, would return the $enforceIsNew value of the old entity.

Also one minor note:

+++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php
@@ -53,10 +61,12 @@ public function testFieldEntityReferenceAfterClone() {
+          $this->assertSame($translation, $field->getEntity(), new FormattableMarkup('Translatable field %field_name on translation %langcode has correct entity reference to the cloned entity object.', $args));
...
+          $this->assertSame($translation->getUntranslated(), $field->getEntity(), new FormattableMarkup('Non translatable field %field_name on translation %langcode has correct entity reference to the cloned entity object in the default translation %default_langcode.', $args));

Let's use just "reference" instead of "entity reference" here, please, to avoid confusions with the field.

tstoeckler’s picture

Status: Needs review » Needs work
hchonov’s picture

Status: Needs work » Needs review
FileSize
5.87 KB
3.13 KB

Thanks for the review, @tstoeckler. I've addressed all your remarks.

hchonov’s picture

FileSize
5.93 KB
3.92 KB

Oups I've forgot to update the second comment chunk .. Sorry about this.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community
Grayside’s picture

@hchonov pointed me here as closely related to a migration stubbing issue I'm having with taxonomy entities (and specifically not with any other entity type). In a non-multilingual site.

  1. Migration step 1 stubs a term
  2. Migration step 2 wants to create that term
  3. ChangedItem::preSave() ends up seeing the entity from step (2) as a not-new entity, however it does not have the original property on the entity. Also, the entity ID does not match the one generated for the stub in step (1), instead it has a tid I do not see in the database.
  4. Process crashes on the error below Fatal error: Call to a member function hasTranslation() on null in core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedItem.php on line 52

Re-posting as a consolidated version of what I shared on #2543726: Make $term->parent behave like any other entity reference field, to fix REST and Migrate support and de-customize its Views integration for discovery/solution review purposes.

hchonov’s picture

@Grayside does your problem still exists with the latest patch from this issue applied?

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 17: 2841311-17.patch, failed testing.

hchonov’s picture

Status: Needs work » Reviewed & tested by the community

It looks like it has been just a random failure, therefore putting back to RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 17: 2841311-17.patch, failed testing.

Berdir’s picture

Now it needs a reroll :)

tstoeckler’s picture

tstoeckler’s picture

hchonov’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -1071,35 +1071,29 @@ public function __clone() {
+      // them: enforceIsNew, newRevision and loadedRevisionId, fields.

Just one nitpick about the re-roll.

"and" is left at the false place :)

Now it needs RTBC back again :P.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Oops, yeah I think I'm allowed to RTBC that interdiff then. ;-)

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

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now 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.

Grayside’s picture

Here is a broad backport of the patch to Drupal 8.2/8.1. There were enough changes in ContentEntityBase::__clone() that I ended up copying that method entirely over what I found in Drupal 8.2, but preliminary testing shows it's working fine.

This does appear to solve the issue I outlined in #19.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 30: 2841311-30-drupal-8-2-do-not-test.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
6.51 KB

Re-uploading latest patch.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed 151050c to 8.4.x and 6245eeb to 8.3.x. Thanks!

This is really just a move of code around so that the translation cache is cleared first. Test coverage looks good.

  • alexpott committed 151050c on 8.4.x
    Issue #2841311 by hchonov, tstoeckler: Initialized fields of an entity...

  • alexpott committed 6245eeb on 8.3.x
    Issue #2841311 by hchonov, tstoeckler: Initialized fields of an entity...

Status: Fixed » Closed (fixed)

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