From 0d06b25026e5bb340c63017ac4942778d0a02f38 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Sun, 26 Jul 2015 16:49:58 +0100 Subject: [PATCH] Issue #2513094 by hchonov, pfrenssen, mkalkbrenner, yched, Berdir: ContentEntityBase::getTranslatedField and ContentEntityBase::__clone break field reference to parent entity --- .../Drupal/Core/Entity/ContentEntityBase.php | 17 ++++- .../Core/Entity/ContentEntityStorageBase.php | 7 +- .../Core/Entity/FieldableEntityInterface.php | 13 +++- ...erenceFieldTranslatedReferenceViewTest.php | 4 +- .../Tests/Entity/ContentEntityCloneTest.php | 70 +++++++++++++++++++ .../Tests/Entity/EntityTranslationTest.php | 29 ++++++++ .../Tests/TaxonomyTranslationTestTrait.php | 8 ++- .../Tests/TermTranslationFieldViewTest.php | 4 +- 8 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index aae28963b119..27d130fd9fb6 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -489,7 +489,7 @@ protected function getTranslatedField($name, $langcode) { if (isset($this->values[$name][$langcode])) { $value = $this->values[$name][$langcode]; } - $field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this, $name, $value); + $field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this->getTranslation($langcode), $name, $value); if ($default) { // $this->defaultLangcode might not be set if we are initializing the // default language code cache, in which case there is no valid @@ -530,6 +530,19 @@ public function getFields($include_computed = TRUE) { return $fields; } + /** + * {@inheritdoc} + */ + public function getTranslatableFields($include_computed = TRUE) { + $fields = []; + foreach ($this->getFieldDefinitions() as $name => $definition) { + if (($include_computed || !$definition->isComputed()) && $definition->isTranslatable()) { + $fields[$name] = $this->get($name); + } + } + return $fields; + } + /** * {@inheritdoc} */ @@ -1041,7 +1054,7 @@ public function __clone() { } foreach ($values as $langcode => $items) { $this->fields[$name][$langcode] = clone $items; - $this->fields[$name][$langcode]->setContext($name, $this->getTypedData()); + $this->fields[$name][$langcode]->setContext($name, $this->getTranslation($langcode)->getTypedData()); } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index c3ae6a304b3b..9240827f96b7 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -405,7 +405,12 @@ protected function invokeFieldMethod($method, ContentEntityInterface $entity) { $args = array_slice(func_get_args(), 2); foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { $translation = $entity->getTranslation($langcode); - foreach ($translation->getFields() as $name => $items) { + // For non translatable fields, there is only one field object instance + // across all translations and it has as parent entity the entity in the + // default entity translation. Therefore field methods on non translatable + // fields should be invoked only on the default entity translation. + $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields(); + foreach ($fields as $name => $items) { // call_user_func_array() is way slower than a direct call so we avoid // using it if have no parameters. $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}(); diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php index 2e5cce76fb67..a83548b530fe 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php @@ -175,7 +175,7 @@ public function get($field_name); public function set($field_name, $value, $notify = TRUE); /** - * Gets an array of field item lists. + * Gets an array of all field item lists. * * @param bool $include_computed * If set to TRUE, computed fields are included. Defaults to TRUE. @@ -185,6 +185,17 @@ public function set($field_name, $value, $notify = TRUE); */ public function getFields($include_computed = TRUE); + /** + * Gets an array of field item lists for translatable fields. + * + * @param bool $include_computed + * If set to TRUE, computed fields are included. Defaults to TRUE. + * + * @return \Drupal\Core\Field\FieldItemListInterface[] + * An array of field item lists implementing, keyed by field name. + */ + public function getTranslatableFields($include_computed = TRUE); + /** * Reacts to changes to a field. * diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php index 648920b7ece8..f72a4de4fc28 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceFieldTranslatedReferenceViewTest.php @@ -188,7 +188,7 @@ protected function setUpEntityReferenceField() { 'entity_type' => $this->testEntityTypeName, 'type' => 'entity_reference', 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, - 'translatable' => FALSE, + 'translatable' => TRUE, 'settings' => array( 'allowed_values' => array( array( @@ -292,7 +292,7 @@ protected function createReferrerEntity() { 'langcode' => $this->baseLangcode, )); $node->save(); - $node->addTranslation($this->translateToLangcode, array()); + $node->addTranslation($this->translateToLangcode, $node->toArray()); $node->save(); return $node; diff --git a/core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php b/core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php new file mode 100644 index 000000000000..77d1603985ef --- /dev/null +++ b/core/modules/system/src/Tests/Entity/ContentEntityCloneTest.php @@ -0,0 +1,70 @@ +<?php + +/** + * @file + * Contains \Drupal\system\Tests\Entity\ContentEntityCloneTest. + */ + +namespace Drupal\system\Tests\Entity; + +use Drupal\entity_test\Entity\EntityTestMul; +use Drupal\language\Entity\ConfigurableLanguage; + +/** + * Tests proper cloning of content entities. + * + * @group Entity + */ +class ContentEntityCloneTest extends EntityUnitTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['language', 'entity_test']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Enable an additional language. + ConfigurableLanguage::createFromLangcode('de')->save(); + + $this->installEntitySchema('entity_test_mul'); + } + + /** + * Tests if entity references on fields are still correct after cloning. + */ + public function testFieldEntityReferenceAfterClone() { + $user = $this->createUser(); + + // Create a test entity. + $entity = EntityTestMul::create([ + 'name' => $this->randomString(), + 'user_id' => $user->id(), + 'language' => 'en', + ]); + + $clone = clone $entity->addTranslation('de'); + + $this->assertEqual($entity->getTranslationLanguages(), $clone->getTranslationLanguages(), 'The entity and its clone have the same translation languages.'); + + $default_langcode = $entity->getUntranslated()->language()->getId(); + foreach (array_keys($clone->getTranslationLanguages()) as $langcode) { + $translation = $clone->getTranslation($langcode); + foreach ($translation->getFields() as $field_name => $field) { + if ($field->getFieldDefinition()->isTranslatable()) { + $args = ['%field_name' => $field_name, '%langcode' => $langcode]; + $this->assertEqual($langcode, $field->getEntity()->language()->getId(), format_string('Translatable field %field_name on translation %langcode has correct entity reference in translation %langcode after cloning.', $args)); + } + else { + $args = ['%field_name' => $field_name, '%langcode' => $langcode, '%default_langcode' => $default_langcode]; + $this->assertEqual($default_langcode, $field->getEntity()->language()->getId(), format_string('Non translatable field %field_name on translation %langcode has correct entity reference in the default translation %default_langcode after cloning.', $args)); + } + } + } + } + +} diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php index 88f21c932e1c..21ff39284556 100644 --- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php @@ -769,4 +769,33 @@ function testEntityAdapter() { } } + /** + * Tests if entity references are correct after adding a new translation. + */ + public function testFieldEntityReference() { + $entity_type = 'entity_test_mul'; + $controller = $this->entityManager->getStorage($entity_type); + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $controller->create(); + + foreach ($this->langcodes as $langcode) { + $entity->addTranslation($langcode); + } + + $default_langcode = $entity->getUntranslated()->language()->getId(); + foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { + $translation = $entity->getTranslation($langcode); + foreach ($translation->getFields() as $field_name => $field) { + if ($field->getFieldDefinition()->isTranslatable()) { + $args = ['%field_name' => $field_name, '%langcode' => $langcode]; + $this->assertEqual($langcode, $field->getEntity()->language()->getId(), format_string('Translatable field %field_name on translation %langcode has correct entity reference in translation %langcode.', $args)); + } + else { + $args = ['%field_name' => $field_name, '%langcode' => $langcode, '%default_langcode' => $default_langcode]; + $this->assertEqual($default_langcode, $field->getEntity()->language()->getId(), format_string('Non translatable field %field_name on translation %langcode has correct entity reference in the default translation %default_langcode.', $args)); + } + } + } + } + } diff --git a/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php b/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php index 61e45bd692c3..4b828cfb2dec 100644 --- a/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php +++ b/core/modules/taxonomy/src/Tests/TaxonomyTranslationTestTrait.php @@ -78,8 +78,12 @@ protected function enableTranslation() { /** * Adds term reference field for the article content type. + * + * @param bool $translatable + * (optional) If TRUE, create a translatable term reference field. Defaults + * to FALSE. */ - protected function setUpTermReferenceField() { + protected function setUpTermReferenceField($translatable = FALSE) { $handler_settings = array( 'target_bundles' => array( $this->vocabulary->id() => $this->vocabulary->id(), @@ -88,7 +92,7 @@ protected function setUpTermReferenceField() { ); $this->createEntityReferenceField('node', 'article', $this->termFieldName, NULL, 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); $field_storage = FieldStorageConfig::loadByName('node', $this->termFieldName); - $field_storage->setTranslatable(FALSE); + $field_storage->setTranslatable($translatable); $field_storage->save(); entity_get_form_display('node', 'article', 'default') diff --git a/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php b/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php index 9474d11c5e18..e9d2afe27a0b 100644 --- a/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php +++ b/core/modules/taxonomy/src/Tests/TermTranslationFieldViewTest.php @@ -50,7 +50,7 @@ protected function setUp() { $this->vocabulary = $this->createVocabulary(); $this->enableTranslation(); $this->setUpTerm(); - $this->setUpTermReferenceField(); + $this->setUpTermReferenceField(TRUE); $this->setUpNode(); } @@ -85,7 +85,7 @@ protected function setUpNode() { 'langcode' => $this->baseLangcode, )); $node->save(); - $node->addTranslation($this->translateToLangcode, array()); + $node->addTranslation($this->translateToLangcode, $node->toArray()); $node->save(); $this->node = $node; } -- GitLab