diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 0c7fa280ffe407383550ad5aa6e73869f6eb67d7..afddb228b90dc91cc8fc946d3f10617986559b78 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -830,6 +830,7 @@ protected function initializeTranslation($langcode) { $translation->translationInitialize = FALSE; $translation->typedData = NULL; $translation->loadedRevisionId = &$this->loadedRevisionId; + $translation->isDefaultRevision = &$this->isDefaultRevision; return $translation; } @@ -1095,7 +1096,7 @@ public function __clone() { // Ensure that the following properties are actually cloned by // overwriting the original references with ones pointing to copies of // them: enforceIsNew, newRevision, loadedRevisionId, fields, entityKeys, - // translatableEntityKeys and values. + // translatableEntityKeys, values and isDefaultRevision. $enforce_is_new = $this->enforceIsNew; $this->enforceIsNew = &$enforce_is_new; @@ -1117,6 +1118,9 @@ public function __clone() { $values = $this->values; $this->values = &$values; + $default_revision = $this->isDefaultRevision; + $this->isDefaultRevision = &$default_revision; + foreach ($this->fields as $name => $fields_by_langcode) { $this->fields[$name] = []; // Untranslatable fields may have multiple references for the same field diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php index 245a3336ad74eb24d17306755db64a6769b4ab73..0f77535a3f6b850e0a36023e5b7305d739ba3ece 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php @@ -251,4 +251,87 @@ public function testFieldValuesAfterSerialize() { $this->assertEquals('clone', $clone->getName()); } + /** + * Tests changing the default revision flag. + */ + public function testDefaultRevision() { + // Create a test entity with a translation, which will internally trigger + // entity cloning for the new translation and create references for some of + // the entity properties. + $entity = EntityTestMulRev::create([ + 'name' => 'original', + 'language' => 'en', + ]); + $entity->addTranslation('de'); + $entity->save(); + + // Assert that the entity is in the default revision. + $this->assertTrue($entity->isDefaultRevision()); + + // Clone the entity and modify its default revision flag. + $clone = clone $entity; + $clone->isDefaultRevision(FALSE); + + // Assert that the clone is not in default revision, but the original entity + // is still in the default revision. + $this->assertFalse($clone->isDefaultRevision()); + $this->assertTrue($entity->isDefaultRevision()); + } + + /** + * Tests references of entity properties after entity cloning. + */ + public function testEntityPropertiesModifications() { + // Create a test entity with a translation, which will internally trigger + // entity cloning for the new translation and create references for some of + // the entity properties. + $entity = EntityTestMulRev::create([ + 'name' => 'original', + 'language' => 'en', + ]); + $translation = $entity->addTranslation('de'); + $entity->save(); + + // Clone the entity. + $clone = clone $entity; + + // Retrieve the entity properties. + $reflection = new \ReflectionClass($entity); + $properties = $reflection->getProperties(~\ReflectionProperty::IS_STATIC); + $translation_unique_properties = ['activeLangcode', 'translationInitialize', 'fieldDefinitions', 'languages', 'langcodeKey', 'defaultLangcode', 'defaultLangcodeKey', 'validated', 'validationRequired', 'entityTypeId', 'typedData', 'cacheContexts', 'cacheTags', 'cacheMaxAge', '_serviceIds']; + + foreach ($properties as $property) { + // Modify each entity property on the clone and assert that the change is + // not propagated to the original entity. + $property->setAccessible(TRUE); + $property->setValue($entity, 'default-value'); + $property->setValue($translation, 'default-value'); + $property->setValue($clone, 'test-entity-cloning'); + $this->assertEquals('default-value', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + $this->assertEquals('default-value', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + $this->assertEquals('test-entity-cloning', $property->getValue($clone), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + + // Modify each entity property on the translation entity object and assert + // that the change is propagated to the default translation entity object + // except for the properties that are unique for each entity translation + // object. + $property->setValue($translation, 'test-translation-cloning'); + // Using assertEquals or assertNotEquals here is dangerous as if the + // assertion fails and the property for some reasons contains the entity + // object e.g. the "typedData" property then the property will be + // serialized, but this will cause exceptions because the entity is + // modified in a non-consistent way and ContentEntityBase::__sleep() will + // not be able to properly access all properties and this will cause + // exceptions without a proper backtrace. + if (in_array($property->getName(), $translation_unique_properties)) { + $this->assertEquals('default-value', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + $this->assertEquals('test-translation-cloning', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + } + else { + $this->assertEquals('test-translation-cloning', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + $this->assertEquals('test-translation-cloning', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); + } + } + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php index 676a98abac33f2489b2cf9e518bf85ce41c99d0c..5c0ef7b382369447e6a44e220330a078758e581d 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php @@ -132,4 +132,29 @@ public function testTranslationValuesWhenSavingForwardRevisions() { $this->assertEquals($forward_revision->getTranslation('de')->name->value, 'forward revision - de'); } + /** + * Tests changing the default revision flag is propagated to all translations. + */ + public function testDefaultRevision() { + // Create a test entity with a translation, which will internally trigger + // entity cloning for the new translation and create references for some of + // the entity properties. + $entity = EntityTestMulRev::create([ + 'name' => 'original', + 'language' => 'en', + ]); + $translation = $entity->addTranslation('de'); + $entity->save(); + + // Assert that the entity is in the default revision. + $this->assertTrue($entity->isDefaultRevision()); + $this->assertTrue($translation->isDefaultRevision()); + + // Change the default revision flag on one of the entity translations and + // assert that the change is propagated to all entity translation objects. + $translation->isDefaultRevision(FALSE); + $this->assertFalse($entity->isDefaultRevision()); + $this->assertFalse($translation->isDefaultRevision()); + } + }