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