3 namespace Drupal\Tests\entity_reference_revisions\Kernel;
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Core\Field\FieldStorageDefinitionInterface;
7 use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
8 use Drupal\field\Entity\FieldConfig;
9 use Drupal\field\Entity\FieldStorageConfig;
10 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
11 use Drupal\language\Entity\ConfigurableLanguage;
12 use Drupal\node\Entity\Node;
13 use Drupal\node\Entity\NodeType;
14 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
15 use Drupal\Tests\node\Traits\NodeCreationTrait;
18 * Tests the entity_reference_revisions composite relationship.
20 * @group entity_reference_revisions
22 class EntityReferenceRevisionsCompositeTranslationTest extends EntityKernelTestBase {
24 use ContentTypeCreationTrait;
25 use NodeCreationTrait;
32 public static $modules = [
35 'entity_reference_revisions',
36 'entity_composite_relationship_test',
42 * The current database connection.
44 * @var \Drupal\Core\Database\Connection
49 * The entity type manager.
51 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
54 protected $entityTypeManager;
59 protected function setUp() {
62 ConfigurableLanguage::createFromLangcode('de')->save();
63 ConfigurableLanguage::createFromLangcode('fr')->save();
65 $this->installEntitySchema('entity_test_composite');
66 $this->installSchema('node', ['node_access']);
68 // Create article content type.
69 NodeType::create(['type' => 'article', 'name' => 'Article'])->save();
71 // Create the reference to the composite entity test.
72 $field_storage = FieldStorageConfig::create([
73 'field_name' => 'composite_reference',
74 'entity_type' => 'node',
75 'type' => 'entity_reference_revisions',
76 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
78 'target_type' => 'entity_test_composite'
81 $field_storage->save();
82 $field = FieldConfig::create([
83 'field_storage' => $field_storage,
84 'bundle' => 'article',
85 'translatable' => FALSE,
89 // Create an untranslatable field on the composite entity.
90 $text_field_storage = FieldStorageConfig::create([
91 'field_name' => 'field_untranslatable',
92 'entity_type' => 'entity_test_composite',
95 $text_field_storage->save();
96 $text_field = FieldConfig::create([
97 'field_storage' => $text_field_storage,
98 'bundle' => 'entity_test_composite',
99 'translatable' => FALSE,
103 // Add a nested composite field.
104 $field_storage = FieldStorageConfig::create([
105 'field_name' => 'composite_reference',
106 'entity_type' => 'entity_test_composite',
107 'type' => 'entity_reference_revisions',
108 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
110 'target_type' => 'entity_test_composite'
113 $field_storage->save();
114 $field = FieldConfig::create([
115 'field_storage' => $field_storage,
116 'bundle' => 'entity_test_composite',
117 'translatable' => FALSE,
121 // Inject database connection and entity type manager for the tests.
122 $this->database = \Drupal::database();
123 $this->entityTypeManager = \Drupal::entityTypeManager();
125 // @todo content_translation should not be needed for a storage test, but
126 // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns
127 // TRUE if the bundle is explicitly translatable.
128 \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
129 \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE);
130 \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [
131 'untranslatable_fields_hide' => TRUE,
133 \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
137 * Test the storage for handling pending revisions with translations.
139 public function testCompositePendingRevisionTranslation() {
140 /** @var \Drupal\node\NodeStorageInterface $node_storage */
141 $node_storage = \Drupal::entityTypeManager()->getStorage('node');
143 // Create a nested composite entity.
144 $nested_composite = EntityTestCompositeRelationship::create([
146 'name' => 'Initial Nested Source Composite',
148 $nested_composite->save();
150 // Create a composite entity.
151 $composite = EntityTestCompositeRelationship::create([
153 'name' => 'Initial Source Composite',
154 'field_untranslatable' => 'Initial untranslatable field',
155 'composite_reference' => $nested_composite,
159 // Create a node with a reference to the test composite entity.
160 $node = Node::create([
162 'title' => 'Initial Source Node',
164 'composite_reference' => $composite,
167 $initial_revision_id = $node->getRevisionId();
169 /** @var \Drupal\node\NodeInterface $node */
170 $node = $node_storage->load($node->id());
172 // Assert that there is only 1 revision when creating a node.
173 $this->assertRevisionCount(1, $node);
174 // Assert there is no new composite revision after creating a host entity.
175 $this->assertRevisionCount(1, $composite);
176 // Assert there is no new composite revision after creating a host entity.
177 $this->assertRevisionCount(1, $nested_composite);
179 // Create a second nested composite entity.
180 $second_nested_composite = EntityTestCompositeRelationship::create([
182 'name' => 'Initial Nested Composite #2',
185 // Add a pending revision.
186 $node = $node_storage->createRevision($node, FALSE);
187 $node->get('composite_reference')->entity->get('composite_reference')->appendItem($second_nested_composite);
189 $pending_en_revision_id = $node->getRevisionId();
191 $this->assertRevisionCount(2, $node);
192 $this->assertRevisionCount(2, $composite);
193 $this->assertRevisionCount(2, $nested_composite);
194 $this->assertRevisionCount(1, $second_nested_composite);
196 // Create a DE translation, start as a draft to replicate the behavior of
198 $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray());
199 $node_de = $node_storage->createRevision($node_de, FALSE);
201 // Despite starting of the draft revision, creating draft of the translation
202 // uses the paragraphs of the default revision.
203 $this->assertCount(1, $node_de->get('composite_reference')->entity->get('composite_reference'));
205 $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE');
206 $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE');
207 $node_de->isDefaultRevision(TRUE);
208 $violations = $node_de->validate();
209 foreach ($violations as $violation) {
210 $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage());
212 $this->assertEquals(0, count($violations));
215 $this->assertRevisionCount(3, $node);
216 $this->assertRevisionCount(3, $composite);
217 $this->assertRevisionCount(3, $nested_composite);
218 $this->assertRevisionCount(1, $second_nested_composite);
220 // Update the translation as a pending revision for both the composite and
222 $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Revision Composite #1 DE');
223 $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Nested Composite #1 DE');
224 $node_de->set('title', 'Pending Revision Node #1 DE');
225 $node_de->setNewRevision(TRUE);
226 $node_de->isDefaultRevision(FALSE);
227 $violations = $node_de->validate();
228 foreach ($violations as $violation) {
229 $this->fail($violation->getMessage());
231 $this->assertEquals(0, count($violations));
234 $this->assertRevisionCount(4, $node);
235 $this->assertRevisionCount(4, $composite);
236 $this->assertRevisionCount(4, $nested_composite);
237 $this->assertRevisionCount(1, $second_nested_composite);
239 /** @var \Drupal\node\NodeInterface $node_de */
240 $node_de = $node_storage->loadRevision($node_de->getRevisionId());
241 $this->assertFalse($node_de->isDefaultRevision());
242 $this->assertFalse((bool) $node_de->isRevisionTranslationAffected());
243 $this->assertTrue((bool) $node_de->getTranslation('de')->isRevisionTranslationAffected());
244 $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label());
245 $this->assertEquals('Initial Source Node', $node_de->label());
246 $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision());
247 $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label());
248 $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
249 $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value);
250 $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label());
252 // Reload the default revision of the node, make sure that the composite
253 // there is unchanged.
254 $node = $node_storage->load($node->id());
255 $this->assertTrue($node->hasTranslation('de'));
256 $this->assertEquals('Initial Source Node', $node->label());
257 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
258 $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label());
260 // Create a FR translation, start as a draft to replicate the behavior of
262 $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR'] + $node->toArray());
263 $node_fr = $node_storage->createRevision($node_fr, FALSE);
264 $node_fr->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Revision Composite #1 FR');
265 $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Nested Composite #1 FR');
266 $violations = $node_fr->validate();
267 $this->assertEquals(0, count($violations));
270 // Now assert that all 3 revisions exist as expected. Two translation
271 // pending revisions, each composite has the original revision as parent
272 // without any existing translation.
273 /** @var \Drupal\node\NodeInterface $node_fr */
274 $node_fr = $node_storage->loadRevision($node_fr->getRevisionId());
275 $this->assertFalse($node_fr->isDefaultRevision());
276 $this->assertTrue($node_fr->hasTranslation('de'));
277 $this->assertFalse((bool) $node_fr->isRevisionTranslationAffected());
278 $this->assertTrue((bool) $node_fr->getTranslation('fr')->isRevisionTranslationAffected());
279 $this->assertEquals('Pending Revision Node #1 FR', $node_fr->getTranslation('fr')->label());
280 $this->assertEquals('Initial Source Node', $node_fr->label());
281 $this->assertFalse($node_fr->get('composite_reference')->entity->isDefaultRevision());
282 $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('de'));
283 $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label());
284 $this->assertEquals('Pending Nested Composite #1 FR', $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label());
285 $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->getTranslation('fr')->get('field_untranslatable')->value);
286 $this->assertEquals('Initial Source Composite', $node_fr->get('composite_reference')->entity->label());
288 $node_de = $node_storage->loadRevision($node_de->getRevisionId());
289 $this->assertFalse($node_de->isDefaultRevision());
290 $this->assertFalse($node_de->hasTranslation('fr'));
291 $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label());
292 $this->assertEquals('Initial Source Node', $node_de->label());
293 $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision());
294 $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr'));
295 $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label());
296 $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
297 $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value);
298 $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label());
300 // Reload the default revision of the node, make sure that the composite
301 // there is unchanged.
302 $node = $node_storage->load($node->id());
303 $this->assertTrue($node->hasTranslation('de'));
304 $this->assertEquals('Initial Source Node', $node->label());
305 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
306 $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label());
308 // Create another pending EN revision and make that the default.
309 $node = $node_storage->loadRevision($pending_en_revision_id);
310 $new_revision = $node_storage->createRevision($node);
311 $new_revision->get('composite_reference')->entity->set('name', 'Updated Source Composite');
312 $new_revision->get('composite_reference')->entity->set('field_untranslatable', 'Updated untranslatable field');
313 $new_revision->setTitle('Updated Source Node');
314 $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->set('name', 'Draft Nested Source Composite #2');
315 $violations = $new_revision->validate();
316 $this->assertEquals(0, count($violations));
317 $new_revision->save();
319 // Assert the two english revisions.
320 // Reload the default revision of the node, make sure that the composite
321 // there is unchanged.
322 $node = $node_storage->load($node->id());
323 $this->assertTrue($node->isDefaultRevision());
324 $this->assertTrue($node->hasTranslation('de'));
325 $this->assertFalse($node->hasTranslation('fr'));
326 $this->assertTrue((bool) $node->isRevisionTranslationAffected());
327 $this->assertEquals('Updated Source Node', $node->label());
328 $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
329 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
330 $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr'));
331 $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
332 $this->assertEquals('Initial Nested Source Composite', $node->get('composite_reference')->entity->get('composite_reference')->entity->label());
333 $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label());
334 $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value);
336 $node_initial = $node_storage->loadRevision($initial_revision_id);
337 $this->assertFalse($node_initial->isDefaultRevision());
338 $this->assertFalse($node_initial->hasTranslation('de'));
339 $this->assertFalse($node_initial->hasTranslation('fr'));
340 $this->assertEquals('Initial Source Node', $node_initial->label());
341 $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision());
342 $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de'));
343 $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label());
344 $this->assertEquals('Initial Nested Source Composite', $node_initial->get('composite_reference')->entity->get('composite_reference')->entity->label());
345 $this->assertEquals('Initial untranslatable field', $node_initial->get('composite_reference')->entity->get('field_untranslatable')->value);
346 $this->assertCount(1, $node_initial->get('composite_reference')->entity->get('composite_reference'));
348 // The current node_fr pending revision still has the initial value before
349 // "merging" it, but it will get the new value for the untranslatable field
350 // in the new revision.
351 $node_fr = $node_storage->loadRevision($node_fr->getRevisionId());
352 $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->get('field_untranslatable')->value);
353 $this->assertCount(1, $node_fr->get('composite_reference')->entity->get('composite_reference'));
355 // Now publish the FR pending revision and also add a translation for
356 // the second composite that it now has.
357 $new_revision = $node_storage->createRevision($node_fr->getTranslation('fr'));
358 $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference'));
359 $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->set('name', 'FR Nested Composite #2');
361 $violations = $new_revision->validate();
362 $this->assertEquals(0, count($violations));
363 $new_revision->save();
365 $this->assertRevisionCount(7, $node);
366 $this->assertRevisionCount(7, $composite);
367 $this->assertRevisionCount(7, $nested_composite);
368 $this->assertRevisionCount(3, $second_nested_composite);
370 // The new default revision should now have the updated english source,
371 // original german translation and the french pending revision.
372 $node = $node_storage->load($node->id());
373 $this->assertTrue($node->isDefaultRevision());
374 $this->assertTrue($node->hasTranslation('de'));
375 $this->assertTrue($node->hasTranslation('fr'));
376 $this->assertFalse((bool) $node->isRevisionTranslationAffected());
377 $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected());
378 $this->assertEquals('Updated Source Node', $node->label());
379 $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
380 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
381 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr'));
382 $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label());
383 $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label());
384 $this->assertEquals('Pending Nested Composite #1 FR', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label());
385 $this->assertEquals('New Node #1 DE', $node->getTranslation('de')->label());
386 $this->assertEquals('New Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label());
387 $this->assertEquals('New Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
388 $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
389 $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value);
390 $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label());
391 $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label());
393 // Now publish the DE pending revision as well.
394 $new_revision = $node_storage->createRevision($node_de->getTranslation('de'));
395 $violations = $new_revision->validate();
396 $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference'));
397 $this->assertEquals(0, count($violations));
398 $new_revision->save();
400 $this->assertRevisionCount(8, $node);
401 $this->assertRevisionCount(8, $composite);
402 $this->assertRevisionCount(8, $nested_composite);
403 $this->assertRevisionCount(4, $second_nested_composite);
405 // The new default revision should now have the updated source and both
407 $node = $node_storage->load($node->id());
408 $this->assertTrue($node->isDefaultRevision());
409 $this->assertTrue($node->hasTranslation('de'));
410 $this->assertTrue($node->hasTranslation('fr'));
411 $this->assertFalse((bool) $node->isRevisionTranslationAffected());
412 $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected());
413 $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected());
414 $this->assertEquals('Updated Source Node', $node->label());
415 $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision());
416 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de'));
417 $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr'));
418 $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label());
419 $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label());
420 $this->assertEquals('Pending Revision Node #1 DE', $node->getTranslation('de')->label());
421 $this->assertEquals('Pending Revision Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label());
422 $this->assertEquals('Pending Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
423 $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label());
424 $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value);
425 $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label());
426 $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label());
428 // The second nested composite of DE inherited the default values for its
430 $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('de')->label());
432 // Simulate creating a new pending revision like
433 // \Drupal\content_moderation\EntityTypeInfo::entityPrepareForm().
434 $new_revision = $node_storage->createRevision($node);
435 $revision_key = $new_revision->getEntityType()->getKey('revision');
436 $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
437 $new_revision->save();
438 $this->assertEquals('Pending Nested Composite #1 DE', $new_revision->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label());
443 * Asserts the revision count of an entity.
445 * @param int $expected
446 * The expected amount of revisions.
447 * @param \Drupal\Core\Entity\EntityInterface $entity
450 protected function assertRevisionCount($expected, EntityInterface $entity) {
451 $node_revisions_count = \Drupal::entityQuery($entity->getEntityTypeId())
452 ->condition($entity->getEntityType()->getKey('id'), $entity->id())
456 $this->assertEquals($expected, $node_revisions_count);