Loading core/lib/Drupal/Core/Entity/ContentEntityBase.php +5 −2 Original line number Diff line number Diff line Loading @@ -1279,11 +1279,14 @@ 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, values, isDefaultRevision and // enforceRevisionTranslationAffected. // translatableEntityKeys, values, isDefaultRevision, // enforceRevisionTranslationAffected and originalEntity. $enforce_is_new = $this->enforceIsNew; $this->enforceIsNew = &$enforce_is_new; $originalEntity = $this->originalEntity; $this->originalEntity = &$originalEntity; $new_revision = $this->newRevision; $this->newRevision = &$new_revision; Loading core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php +110 −4 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\RevisionableStorageInterface; use Drupal\entity_test\Entity\EntityTestMul; use Drupal\entity_test\Entity\EntityTestMulRev; use Drupal\language\Entity\ConfigurableLanguage; Loading Loading @@ -320,13 +322,12 @@ public function testEntityPropertiesModifications(): void { ]; foreach ($properties as $property) { // The original property is typed and can not be set to a string. if ($property->getName() === 'originalEntity') { $property_name = $property->getName(); // @see \Drupal\KernelTests\Core\Entity\ContentEntityCloneTest::testEntityOriginal() if ($property_name === 'originalEntity') { continue; } $property_name = $property->getName(); // Modify each entity property on the clone and assert that the change is // not propagated to the original entity. $property->setValue($entity, 'default-value'); Loading Loading @@ -367,4 +368,109 @@ public function testEntityPropertiesModifications(): void { } } /** * Tests the entity original property when cloning entities. */ public function testEntityOriginal(): void { // Make sure entity bundles are translatable. \Drupal::state()->set('entity_test.translation', TRUE); $bundleInfo = \Drupal::service('entity_type.bundle.info'); $this->assertInstanceOf(EntityTypeBundleInfoInterface::class, $bundleInfo); $bundleInfo->clearCachedBundles(); $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev'); assert($storage instanceof RevisionableStorageInterface); // Create a test entity with a translation. The translation will be created // by cloning the original entity but the originalEntity property will be // synchronized. $entity = $storage->create([ 'name' => 'original', 'language' => 'en', ]); $this->assertInstanceOf(EntityTestMulRev::class, $entity); $this->assertTrue($entity->isTranslatable()); $this->assertNull($entity->getOriginal()); $translationLangcode = 'de'; $translation = $entity->addTranslation($translationLangcode); $this->assertInstanceOf(EntityTestMulRev::class, $translation); // We could assert here that the original of the entity and the translation // is null before and after the save, to avoid false positives below. This, // however would lead to a PHPStan error below, due to a bug in PHPStan. /* @see https://github.com/phpstan/phpstan/issues/13416 */ $entity->save(); // Clone the entity and the translation manually, and make sure that while // the entity's and the translation's original are synchronized, the clones // are not affected by this. $entityClone = clone $entity; $entityCloneTranslation = $entityClone->getTranslation($translationLangcode); $translationClone = clone $translation; $entityOriginal = EntityTestMulRev::create(); $entity->setOriginal($entityOriginal); $this->assertSame($entityOriginal, $entity->getOriginal()); $this->assertSame($entityOriginal, $translation->getOriginal()); $this->assertNull($entityClone->getOriginal()); $this->assertNull($translationClone->getOriginal()); $entityCloneOriginal = EntityTestMulRev::create(); $this->assertNotSame($entityOriginal, $entityCloneOriginal); $entityCloneTranslation->setOriginal($entityCloneOriginal); $this->assertSame($entityOriginal, $entity->getOriginal()); $this->assertSame($entityOriginal, $translation->getOriginal()); $this->assertSame($entityCloneOriginal, $entityClone->getOriginal()); $this->assertSame($entityCloneOriginal, $entityCloneTranslation->getOriginal()); $this->assertNull($translationClone->getOriginal()); // Unset the original property. $translation->setOriginal(NULL); $this->assertNull($entity->getOriginal()); $this->assertNull($translation->getOriginal()); // Create two draft revisions of the entity to trigger the merged revision original // behavior. $firstDraft = $storage->createRevision($entity, default: FALSE); $firstDraftClone = clone $firstDraft; $this->assertNull($entity->getOriginal()); $this->assertNull($firstDraft->getOriginal()); $this->assertNull($firstDraftClone->getOriginal()); $secondDraft = $storage->createRevision($firstDraft, default: FALSE); $this->assertNull($entity->getOriginal()); $this->assertNull($firstDraft->getOriginal()); $this->assertNull($firstDraftClone->getOriginal()); $secondDraftOriginal = $secondDraft->getOriginal(); $this->assertInstanceOf(EntityTestMulRev::class, $secondDraftOriginal); $this->assertNotSame($entityOriginal, $secondDraftOriginal); $this->assertNotSame($entityCloneOriginal, $secondDraftOriginal); $this->assertNull($secondDraftOriginal->getOriginal()); // Make sure that the original is still synchronized across translations. $secondDraftTranslation = $secondDraft->getTranslation($translationLangcode); $this->assertInstanceOf(EntityTestMulRev::class, $secondDraftTranslation); $this->assertSame($secondDraftOriginal, $secondDraftTranslation->getOriginal()); // Test that the synchronization still does not affect clones. $secondDraftClone = clone $secondDraft; $secondDraftTranslationClone = clone $secondDraftTranslation; $secondDraftCloneOriginal = EntityTestMulRev::create(); $secondDraftClone->setOriginal($secondDraftCloneOriginal); $this->assertSame($secondDraftCloneOriginal, $secondDraftClone->getOriginal()); $secondDraftTranslationClone->setOriginal(NULL); $this->assertNull($secondDraftTranslationClone->getOriginal()); $this->assertSame($secondDraftOriginal, $secondDraft->getOriginal()); $this->assertSame($secondDraftOriginal, $secondDraftTranslation->getOriginal()); // Create a third draft revision to trigger the merged revision original // again, but this time starting with an entity that already has an original // entity set. $thirdDraft = $storage->createRevision($secondDraft, default: FALSE); $this->assertNull($firstDraft->getOriginal()); $this->assertSame($secondDraftOriginal, $secondDraft->getOriginal()); $thirdDraftOriginal = $thirdDraft->getOriginal(); $this->assertInstanceOf(EntityTestMulRev::class, $thirdDraftOriginal); $this->assertNotSame($secondDraftOriginal, $thirdDraftOriginal); } } Loading
core/lib/Drupal/Core/Entity/ContentEntityBase.php +5 −2 Original line number Diff line number Diff line Loading @@ -1279,11 +1279,14 @@ 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, values, isDefaultRevision and // enforceRevisionTranslationAffected. // translatableEntityKeys, values, isDefaultRevision, // enforceRevisionTranslationAffected and originalEntity. $enforce_is_new = $this->enforceIsNew; $this->enforceIsNew = &$enforce_is_new; $originalEntity = $this->originalEntity; $this->originalEntity = &$originalEntity; $new_revision = $this->newRevision; $this->newRevision = &$new_revision; Loading
core/tests/Drupal/KernelTests/Core/Entity/ContentEntityCloneTest.php +110 −4 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\RevisionableStorageInterface; use Drupal\entity_test\Entity\EntityTestMul; use Drupal\entity_test\Entity\EntityTestMulRev; use Drupal\language\Entity\ConfigurableLanguage; Loading Loading @@ -320,13 +322,12 @@ public function testEntityPropertiesModifications(): void { ]; foreach ($properties as $property) { // The original property is typed and can not be set to a string. if ($property->getName() === 'originalEntity') { $property_name = $property->getName(); // @see \Drupal\KernelTests\Core\Entity\ContentEntityCloneTest::testEntityOriginal() if ($property_name === 'originalEntity') { continue; } $property_name = $property->getName(); // Modify each entity property on the clone and assert that the change is // not propagated to the original entity. $property->setValue($entity, 'default-value'); Loading Loading @@ -367,4 +368,109 @@ public function testEntityPropertiesModifications(): void { } } /** * Tests the entity original property when cloning entities. */ public function testEntityOriginal(): void { // Make sure entity bundles are translatable. \Drupal::state()->set('entity_test.translation', TRUE); $bundleInfo = \Drupal::service('entity_type.bundle.info'); $this->assertInstanceOf(EntityTypeBundleInfoInterface::class, $bundleInfo); $bundleInfo->clearCachedBundles(); $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev'); assert($storage instanceof RevisionableStorageInterface); // Create a test entity with a translation. The translation will be created // by cloning the original entity but the originalEntity property will be // synchronized. $entity = $storage->create([ 'name' => 'original', 'language' => 'en', ]); $this->assertInstanceOf(EntityTestMulRev::class, $entity); $this->assertTrue($entity->isTranslatable()); $this->assertNull($entity->getOriginal()); $translationLangcode = 'de'; $translation = $entity->addTranslation($translationLangcode); $this->assertInstanceOf(EntityTestMulRev::class, $translation); // We could assert here that the original of the entity and the translation // is null before and after the save, to avoid false positives below. This, // however would lead to a PHPStan error below, due to a bug in PHPStan. /* @see https://github.com/phpstan/phpstan/issues/13416 */ $entity->save(); // Clone the entity and the translation manually, and make sure that while // the entity's and the translation's original are synchronized, the clones // are not affected by this. $entityClone = clone $entity; $entityCloneTranslation = $entityClone->getTranslation($translationLangcode); $translationClone = clone $translation; $entityOriginal = EntityTestMulRev::create(); $entity->setOriginal($entityOriginal); $this->assertSame($entityOriginal, $entity->getOriginal()); $this->assertSame($entityOriginal, $translation->getOriginal()); $this->assertNull($entityClone->getOriginal()); $this->assertNull($translationClone->getOriginal()); $entityCloneOriginal = EntityTestMulRev::create(); $this->assertNotSame($entityOriginal, $entityCloneOriginal); $entityCloneTranslation->setOriginal($entityCloneOriginal); $this->assertSame($entityOriginal, $entity->getOriginal()); $this->assertSame($entityOriginal, $translation->getOriginal()); $this->assertSame($entityCloneOriginal, $entityClone->getOriginal()); $this->assertSame($entityCloneOriginal, $entityCloneTranslation->getOriginal()); $this->assertNull($translationClone->getOriginal()); // Unset the original property. $translation->setOriginal(NULL); $this->assertNull($entity->getOriginal()); $this->assertNull($translation->getOriginal()); // Create two draft revisions of the entity to trigger the merged revision original // behavior. $firstDraft = $storage->createRevision($entity, default: FALSE); $firstDraftClone = clone $firstDraft; $this->assertNull($entity->getOriginal()); $this->assertNull($firstDraft->getOriginal()); $this->assertNull($firstDraftClone->getOriginal()); $secondDraft = $storage->createRevision($firstDraft, default: FALSE); $this->assertNull($entity->getOriginal()); $this->assertNull($firstDraft->getOriginal()); $this->assertNull($firstDraftClone->getOriginal()); $secondDraftOriginal = $secondDraft->getOriginal(); $this->assertInstanceOf(EntityTestMulRev::class, $secondDraftOriginal); $this->assertNotSame($entityOriginal, $secondDraftOriginal); $this->assertNotSame($entityCloneOriginal, $secondDraftOriginal); $this->assertNull($secondDraftOriginal->getOriginal()); // Make sure that the original is still synchronized across translations. $secondDraftTranslation = $secondDraft->getTranslation($translationLangcode); $this->assertInstanceOf(EntityTestMulRev::class, $secondDraftTranslation); $this->assertSame($secondDraftOriginal, $secondDraftTranslation->getOriginal()); // Test that the synchronization still does not affect clones. $secondDraftClone = clone $secondDraft; $secondDraftTranslationClone = clone $secondDraftTranslation; $secondDraftCloneOriginal = EntityTestMulRev::create(); $secondDraftClone->setOriginal($secondDraftCloneOriginal); $this->assertSame($secondDraftCloneOriginal, $secondDraftClone->getOriginal()); $secondDraftTranslationClone->setOriginal(NULL); $this->assertNull($secondDraftTranslationClone->getOriginal()); $this->assertSame($secondDraftOriginal, $secondDraft->getOriginal()); $this->assertSame($secondDraftOriginal, $secondDraftTranslation->getOriginal()); // Create a third draft revision to trigger the merged revision original // again, but this time starting with an entity that already has an original // entity set. $thirdDraft = $storage->createRevision($secondDraft, default: FALSE); $this->assertNull($firstDraft->getOriginal()); $this->assertSame($secondDraftOriginal, $secondDraft->getOriginal()); $thirdDraftOriginal = $thirdDraft->getOriginal(); $this->assertInstanceOf(EntityTestMulRev::class, $thirdDraftOriginal); $this->assertNotSame($secondDraftOriginal, $thirdDraftOriginal); } }