1<?php 2 3namespace Drupal\KernelTests\Core\Entity; 4 5use Drupal\Component\Render\FormattableMarkup; 6use Drupal\entity_test\Entity\EntityTestMul; 7use Drupal\entity_test\Entity\EntityTestMulRev; 8use Drupal\language\Entity\ConfigurableLanguage; 9 10/** 11 * Tests proper cloning of content entities. 12 * 13 * @group Entity 14 */ 15class ContentEntityCloneTest extends EntityKernelTestBase { 16 17 /** 18 * {@inheritdoc} 19 */ 20 public static $modules = ['language', 'entity_test']; 21 22 /** 23 * {@inheritdoc} 24 */ 25 protected function setUp() { 26 parent::setUp(); 27 28 // Enable an additional language. 29 ConfigurableLanguage::createFromLangcode('de')->save(); 30 31 $this->installEntitySchema('entity_test_mul'); 32 $this->installEntitySchema('entity_test_mulrev'); 33 } 34 35 /** 36 * Tests if entity references on fields are still correct after cloning. 37 */ 38 public function testFieldEntityReferenceAfterClone() { 39 $user = $this->createUser(); 40 41 // Create a test entity. 42 $entity = EntityTestMul::create([ 43 'name' => $this->randomString(), 44 'user_id' => $user->id(), 45 'language' => 'en', 46 ]); 47 $translation = $entity->addTranslation('de'); 48 49 // Initialize the fields on the translation objects in order to check that 50 // they are properly cloned and have a reference to the cloned entity 51 // object and not to the original one. 52 $entity->getFields(); 53 $translation->getFields(); 54 55 $clone = clone $translation; 56 57 $this->assertEqual($entity->getTranslationLanguages(), $clone->getTranslationLanguages(), 'The entity and its clone have the same translation languages.'); 58 59 $default_langcode = $entity->getUntranslated()->language()->getId(); 60 foreach (array_keys($clone->getTranslationLanguages()) as $langcode) { 61 $translation = $clone->getTranslation($langcode); 62 foreach ($translation->getFields() as $field_name => $field) { 63 if ($field->getFieldDefinition()->isTranslatable()) { 64 $args = ['%field_name' => $field_name, '%langcode' => $langcode]; 65 $this->assertEqual($langcode, $field->getEntity()->language()->getId(), new FormattableMarkup('Translatable field %field_name on translation %langcode has correct entity reference in translation %langcode after cloning.', $args)); 66 $this->assertSame($translation, $field->getEntity(), new FormattableMarkup('Translatable field %field_name on translation %langcode has correct reference to the cloned entity object.', $args)); 67 } 68 else { 69 $args = ['%field_name' => $field_name, '%langcode' => $langcode, '%default_langcode' => $default_langcode]; 70 $this->assertEqual($default_langcode, $field->getEntity()->language()->getId(), new FormattableMarkup('Non translatable field %field_name on translation %langcode has correct entity reference in the default translation %default_langcode after cloning.', $args)); 71 $this->assertSame($translation->getUntranslated(), $field->getEntity(), new FormattableMarkup('Non translatable field %field_name on translation %langcode has correct reference to the cloned entity object in the default translation %default_langcode.', $args)); 72 } 73 } 74 } 75 } 76 77 /** 78 * Tests that the flag for enforcing a new entity is not shared. 79 */ 80 public function testEnforceIsNewOnClonedEntityTranslation() { 81 // Create a test entity. 82 $entity = EntityTestMul::create([ 83 'name' => $this->randomString(), 84 'language' => 'en', 85 ]); 86 $entity->save(); 87 $entity_translation = $entity->addTranslation('de'); 88 $entity->save(); 89 90 // The entity is not new anymore. 91 $this->assertFalse($entity_translation->isNew()); 92 93 // The clone should not be new either. 94 $clone = clone $entity_translation; 95 $this->assertFalse($clone->isNew()); 96 97 // After forcing the clone to be new only it should be flagged as new, but 98 // the original entity should not. 99 $clone->enforceIsNew(); 100 $this->assertTrue($clone->isNew()); 101 $this->assertFalse($entity_translation->isNew()); 102 } 103 104 /** 105 * Tests if the entity fields are properly cloned. 106 */ 107 public function testClonedEntityFields() { 108 $user = $this->createUser(); 109 110 // Create a test entity. 111 $entity = EntityTestMul::create([ 112 'name' => $this->randomString(), 113 'user_id' => $user->id(), 114 'language' => 'en', 115 ]); 116 117 $entity->addTranslation('de'); 118 $entity->save(); 119 $fields = array_keys($entity->getFieldDefinitions()); 120 121 // Reload the entity, clone it and check that both entity objects reference 122 // different field instances. 123 $entity = $this->reloadEntity($entity); 124 $clone = clone $entity; 125 126 $different_references = TRUE; 127 foreach ($fields as $field_name) { 128 if ($entity->get($field_name) === $clone->get($field_name)) { 129 $different_references = FALSE; 130 } 131 } 132 $this->assertTrue($different_references, 'The entity object and the cloned entity object reference different field item list objects.'); 133 134 // Reload the entity, initialize one translation, clone it and check that 135 // both entity objects reference different field instances. 136 $entity = $this->reloadEntity($entity); 137 $entity->getTranslation('de'); 138 $clone = clone $entity; 139 140 $different_references = TRUE; 141 foreach ($fields as $field_name) { 142 if ($entity->get($field_name) === $clone->get($field_name)) { 143 $different_references = FALSE; 144 } 145 } 146 $this->assertTrue($different_references, 'The entity object and the cloned entity object reference different field item list objects if the entity is cloned after an entity translation has been initialized.'); 147 } 148 149 /** 150 * Tests that the flag for enforcing a new revision is not shared. 151 */ 152 public function testNewRevisionOnCloneEntityTranslation() { 153 // Create a test entity. 154 $entity = EntityTestMulRev::create([ 155 'name' => $this->randomString(), 156 'language' => 'en', 157 ]); 158 $entity->save(); 159 $entity->addTranslation('de'); 160 $entity->save(); 161 162 // Reload the entity as ContentEntityBase::postCreate() forces the entity to 163 // be a new revision. 164 $entity = EntityTestMulRev::load($entity->id()); 165 $entity_translation = $entity->getTranslation('de'); 166 167 // The entity is not set to be a new revision. 168 $this->assertFalse($entity_translation->isNewRevision()); 169 170 // The clone should not be set to be a new revision either. 171 $clone = clone $entity_translation; 172 $this->assertFalse($clone->isNewRevision()); 173 174 // After forcing the clone to be a new revision only it should be flagged 175 // as a new revision, but the original entity should not. 176 $clone->setNewRevision(); 177 $this->assertTrue($clone->isNewRevision()); 178 $this->assertFalse($entity_translation->isNewRevision()); 179 } 180 181 /** 182 * Tests modifications on entity keys of a cloned entity object. 183 */ 184 public function testEntityKeysModifications() { 185 // Create a test entity with a translation, which will internally trigger 186 // entity cloning for the new translation and create references for some of 187 // the entity properties. 188 $entity = EntityTestMulRev::create([ 189 'name' => 'original-name', 190 'uuid' => 'original-uuid', 191 'language' => 'en', 192 ]); 193 $entity->addTranslation('de'); 194 $entity->save(); 195 196 // Clone the entity. 197 $clone = clone $entity; 198 199 // Alter a non-translatable and a translatable entity key fields of the 200 // cloned entity and assert that retrieving the value through the entity 201 // keys local cache will be different for the cloned and the original 202 // entity. 203 // We first have to call the ::uuid() and ::label() method on the original 204 // entity as it is going to cache the field values into the $entityKeys and 205 // $translatableEntityKeys properties of the entity object and we want to 206 // check that the cloned and the original entity aren't sharing the same 207 // reference to those local cache properties. 208 $uuid_field_name = $entity->getEntityType()->getKey('uuid'); 209 $this->assertFalse($entity->getFieldDefinition($uuid_field_name)->isTranslatable()); 210 $clone->$uuid_field_name->value = 'clone-uuid'; 211 $this->assertEquals('original-uuid', $entity->uuid()); 212 $this->assertEquals('clone-uuid', $clone->uuid()); 213 214 $label_field_name = $entity->getEntityType()->getKey('label'); 215 $this->assertTrue($entity->getFieldDefinition($label_field_name)->isTranslatable()); 216 $clone->$label_field_name->value = 'clone-name'; 217 $this->assertEquals('original-name', $entity->label()); 218 $this->assertEquals('clone-name', $clone->label()); 219 } 220 221 /** 222 * Tests the field values after serializing an entity and its clone. 223 */ 224 public function testFieldValuesAfterSerialize() { 225 // Create a test entity with a translation, which will internally trigger 226 // entity cloning for the new translation and create references for some of 227 // the entity properties. 228 $entity = EntityTestMulRev::create([ 229 'name' => 'original', 230 'language' => 'en', 231 ]); 232 $entity->addTranslation('de'); 233 $entity->save(); 234 235 // Clone the entity. 236 $clone = clone $entity; 237 238 // Alter the name field value of the cloned entity object. 239 $clone->setName('clone'); 240 241 // Serialize the entity and the cloned object in order to destroy the field 242 // objects and put the field values into the entity property $values, so 243 // that on accessing a field again it will be newly created with the value 244 // from the $values property. 245 serialize($entity); 246 serialize($clone); 247 248 // Assert that the original and the cloned entity both have different names. 249 $this->assertEquals('original', $entity->getName()); 250 $this->assertEquals('clone', $clone->getName()); 251 } 252 253 /** 254 * Tests changing the default revision flag. 255 */ 256 public function testDefaultRevision() { 257 // Create a test entity with a translation, which will internally trigger 258 // entity cloning for the new translation and create references for some of 259 // the entity properties. 260 $entity = EntityTestMulRev::create([ 261 'name' => 'original', 262 'language' => 'en', 263 ]); 264 $entity->addTranslation('de'); 265 $entity->save(); 266 267 // Assert that the entity is in the default revision. 268 $this->assertTrue($entity->isDefaultRevision()); 269 270 // Clone the entity and modify its default revision flag. 271 $clone = clone $entity; 272 $clone->isDefaultRevision(FALSE); 273 274 // Assert that the clone is not in default revision, but the original entity 275 // is still in the default revision. 276 $this->assertFalse($clone->isDefaultRevision()); 277 $this->assertTrue($entity->isDefaultRevision()); 278 } 279 280 /** 281 * Tests references of entity properties after entity cloning. 282 */ 283 public function testEntityPropertiesModifications() { 284 // Create a test entity with a translation, which will internally trigger 285 // entity cloning for the new translation and create references for some of 286 // the entity properties. 287 $entity = EntityTestMulRev::create([ 288 'name' => 'original', 289 'language' => 'en', 290 ]); 291 $translation = $entity->addTranslation('de'); 292 $entity->save(); 293 294 // Clone the entity. 295 $clone = clone $entity; 296 297 // Retrieve the entity properties. 298 $reflection = new \ReflectionClass($entity); 299 $properties = $reflection->getProperties(~\ReflectionProperty::IS_STATIC); 300 $translation_unique_properties = ['activeLangcode', 'translationInitialize', 'fieldDefinitions', 'languages', 'langcodeKey', 'defaultLangcode', 'defaultLangcodeKey', 'revisionTranslationAffectedKey', 'validated', 'validationRequired', 'entityTypeId', 'typedData', 'cacheContexts', 'cacheTags', 'cacheMaxAge', '_serviceIds', '_entityStorages']; 301 302 foreach ($properties as $property) { 303 // Modify each entity property on the clone and assert that the change is 304 // not propagated to the original entity. 305 $property->setAccessible(TRUE); 306 $property->setValue($entity, 'default-value'); 307 $property->setValue($translation, 'default-value'); 308 $property->setValue($clone, 'test-entity-cloning'); 309 // Static properties remain the same across all instances of the class. 310 if ($property->isStatic()) { 311 $this->assertEquals('test-entity-cloning', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 312 $this->assertEquals('test-entity-cloning', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 313 $this->assertEquals('test-entity-cloning', $property->getValue($clone), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 314 } 315 else { 316 $this->assertEquals('default-value', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 317 $this->assertEquals('default-value', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 318 $this->assertEquals('test-entity-cloning', $property->getValue($clone), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 319 } 320 321 // Modify each entity property on the translation entity object and assert 322 // that the change is propagated to the default translation entity object 323 // except for the properties that are unique for each entity translation 324 // object. 325 $property->setValue($translation, 'test-translation-cloning'); 326 // Using assertEquals or assertNotEquals here is dangerous as if the 327 // assertion fails and the property for some reasons contains the entity 328 // object e.g. the "typedData" property then the property will be 329 // serialized, but this will cause exceptions because the entity is 330 // modified in a non-consistent way and ContentEntityBase::__sleep() will 331 // not be able to properly access all properties and this will cause 332 // exceptions without a proper backtrace. 333 if (in_array($property->getName(), $translation_unique_properties)) { 334 $this->assertEquals('default-value', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 335 $this->assertEquals('test-translation-cloning', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 336 } 337 else { 338 $this->assertEquals('test-translation-cloning', $property->getValue($entity), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 339 $this->assertEquals('test-translation-cloning', $property->getValue($translation), (string) new FormattableMarkup('Entity property %property_name is not cloned properly.', ['%property_name' => $property->getName()])); 340 } 341 } 342 } 343 344} 345