Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / KernelTests / Core / Entity / EntityDefinitionUpdateTest.php
1 <?php
2
3 namespace Drupal\KernelTests\Core\Entity;
4
5 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
6 use Drupal\Core\Database\Database;
7 use Drupal\Core\Database\DatabaseExceptionWrapper;
8 use Drupal\Core\Database\IntegrityConstraintViolationException;
9 use Drupal\Core\Entity\ContentEntityType;
10 use Drupal\Core\Entity\EntityStorageException;
11 use Drupal\Core\Entity\EntityTypeEvents;
12 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
13 use Drupal\Core\Field\BaseFieldDefinition;
14 use Drupal\Core\Field\FieldException;
15 use Drupal\Core\Field\FieldStorageDefinitionEvents;
16 use Drupal\Core\Language\LanguageInterface;
17 use Drupal\entity_test_update\Entity\EntityTestUpdate;
18 use Drupal\system\Tests\Entity\EntityDefinitionTestTrait;
19
20 /**
21  * Tests EntityDefinitionUpdateManager functionality.
22  *
23  * @group Entity
24  */
25 class EntityDefinitionUpdateTest extends EntityKernelTestBase {
26
27   use EntityDefinitionTestTrait;
28
29   /**
30    * The entity definition update manager.
31    *
32    * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
33    */
34   protected $entityDefinitionUpdateManager;
35
36   /**
37    * The database connection.
38    *
39    * @var \Drupal\Core\Database\Connection
40    */
41   protected $database;
42
43   /**
44    * Modules to enable.
45    *
46    * @var array
47    */
48   public static $modules = ['entity_test_update'];
49
50   /**
51    * {@inheritdoc}
52    */
53   protected function setUp() {
54     parent::setUp();
55     $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
56     $this->database = $this->container->get('database');
57
58     // Install every entity type's schema that wasn't installed in the parent
59     // method.
60     foreach (array_diff_key($this->entityManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) {
61       $this->installEntitySchema($entity_type_id);
62     }
63   }
64
65   /**
66    * Tests that new entity type definitions are correctly handled.
67    */
68   public function testNewEntityType() {
69     $entity_type_id = 'entity_test_new';
70     $schema = $this->database->schema();
71
72     // Check that the "entity_test_new" is not defined.
73     $entity_types = $this->entityManager->getDefinitions();
74     $this->assertFalse(isset($entity_types[$entity_type_id]), 'The "entity_test_new" entity type does not exist.');
75     $this->assertFalse($schema->tableExists($entity_type_id), 'Schema for the "entity_test_new" entity type does not exist.');
76
77     // Check that the "entity_test_new" is now defined and the related schema
78     // has been created.
79     $this->enableNewEntityType();
80     $entity_types = $this->entityManager->getDefinitions();
81     $this->assertTrue(isset($entity_types[$entity_type_id]), 'The "entity_test_new" entity type exists.');
82     $this->assertTrue($schema->tableExists($entity_type_id), 'Schema for the "entity_test_new" entity type has been created.');
83   }
84
85   /**
86    * Tests when no definition update is needed.
87    */
88   public function testNoUpdates() {
89     // Ensure that the definition update manager reports no updates.
90     $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
91     $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), [], 'EntityDefinitionUpdateManager reports an empty change summary.');
92
93     // Ensure that applyUpdates() runs without error (it's not expected to do
94     // anything when there aren't updates).
95     $this->entityDefinitionUpdateManager->applyUpdates();
96   }
97
98   /**
99    * Tests updating entity schema when there are no existing entities.
100    */
101   public function testEntityTypeUpdateWithoutData() {
102     // The 'entity_test_update' entity type starts out non-revisionable, so
103     // ensure the revision table hasn't been created during setUp().
104     $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.');
105
106     // Update it to be revisionable and ensure the definition update manager
107     // reports that an update is needed.
108     $this->updateEntityTypeToRevisionable();
109     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
110     $expected = [
111       'entity_test_update' => [
112         t('The %entity_type entity type needs to be updated.', ['%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel()]),
113         // The revision key is now defined, so the revision field needs to be
114         // created.
115         t('The %field_name field needs to be installed.', ['%field_name' => 'Revision ID']),
116       ],
117     ];
118     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
119
120     // Run the update and ensure the revision table is created.
121     $this->entityDefinitionUpdateManager->applyUpdates();
122     $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.');
123   }
124
125   /**
126    * Tests updating entity schema when there are existing entities.
127    */
128   public function testEntityTypeUpdateWithData() {
129     // Save an entity.
130     $this->entityManager->getStorage('entity_test_update')->create()->save();
131
132     // Update the entity type to be revisionable and try to apply the update.
133     // It's expected to throw an exception.
134     $this->updateEntityTypeToRevisionable();
135     try {
136       $this->entityDefinitionUpdateManager->applyUpdates();
137       $this->fail('EntityStorageException thrown when trying to apply an update that requires data migration.');
138     }
139     catch (EntityStorageException $e) {
140       $this->pass('EntityStorageException thrown when trying to apply an update that requires data migration.');
141     }
142   }
143
144   /**
145    * Tests creating, updating, and deleting a base field if no entities exist.
146    */
147   public function testBaseFieldCreateUpdateDeleteWithoutData() {
148     // Add a base field, ensure the update manager reports it, and the update
149     // creates its schema.
150     $this->addBaseField();
151     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
152     $expected = [
153       'entity_test_update' => [
154         t('The %field_name field needs to be installed.', ['%field_name' => t('A new base field')]),
155       ],
156     ];
157     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
158     $this->entityDefinitionUpdateManager->applyUpdates();
159     $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
160
161     // Add an index on the base field, ensure the update manager reports it,
162     // and the update creates it.
163     $this->addBaseFieldIndex();
164     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
165     $expected = [
166       'entity_test_update' => [
167         t('The %field_name field needs to be updated.', ['%field_name' => t('A new base field')]),
168       ],
169     ];
170     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
171     $this->entityDefinitionUpdateManager->applyUpdates();
172     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.');
173
174     // Remove the above index, ensure the update manager reports it, and the
175     // update deletes it.
176     $this->removeBaseFieldIndex();
177     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
178     $expected = [
179       'entity_test_update' => [
180         t('The %field_name field needs to be updated.', ['%field_name' => t('A new base field')]),
181       ],
182     ];
183     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
184     $this->entityDefinitionUpdateManager->applyUpdates();
185     $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.');
186
187     // Update the type of the base field from 'string' to 'text', ensure the
188     // update manager reports it, and the update adjusts the schema
189     // accordingly.
190     $this->modifyBaseField();
191     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
192     $expected = [
193       'entity_test_update' => [
194         t('The %field_name field needs to be updated.', ['%field_name' => t('A new base field')]),
195       ],
196     ];
197     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
198     $this->entityDefinitionUpdateManager->applyUpdates();
199     $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.');
200     $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.');
201     $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.');
202
203     // Remove the base field, ensure the update manager reports it, and the
204     // update deletes the schema.
205     $this->removeBaseField();
206     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
207     $expected = [
208       'entity_test_update' => [
209         t('The %field_name field needs to be uninstalled.', ['%field_name' => t('A new base field')]),
210       ],
211     ];
212     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
213     $this->entityDefinitionUpdateManager->applyUpdates();
214     $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.');
215     $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.');
216   }
217
218   /**
219    * Tests creating, updating, and deleting a bundle field if no entities exist.
220    */
221   public function testBundleFieldCreateUpdateDeleteWithoutData() {
222     // Add a bundle field, ensure the update manager reports it, and the update
223     // creates its schema.
224     $this->addBundleField();
225     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
226     $expected = [
227       'entity_test_update' => [
228         t('The %field_name field needs to be installed.', ['%field_name' => t('A new bundle field')]),
229       ],
230     ];
231     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
232     $this->entityDefinitionUpdateManager->applyUpdates();
233     $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
234
235     // Update the type of the base field from 'string' to 'text', ensure the
236     // update manager reports it, and the update adjusts the schema
237     // accordingly.
238     $this->modifyBundleField();
239     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
240     $expected = [
241       'entity_test_update' => [
242         t('The %field_name field needs to be updated.', ['%field_name' => t('A new bundle field')]),
243       ],
244     ];
245     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
246     $this->entityDefinitionUpdateManager->applyUpdates();
247     $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.');
248
249     // Remove the bundle field, ensure the update manager reports it, and the
250     // update deletes the schema.
251     $this->removeBundleField();
252     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
253     $expected = [
254       'entity_test_update' => [
255         t('The %field_name field needs to be uninstalled.', ['%field_name' => t('A new bundle field')]),
256       ],
257     ];
258     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
259     $this->entityDefinitionUpdateManager->applyUpdates();
260     $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
261   }
262
263   /**
264    * Tests creating and deleting a base field if entities exist.
265    *
266    * This tests deletion when there are existing entities, but not existing data
267    * for the field being deleted.
268    *
269    * @see testBaseFieldDeleteWithExistingData()
270    */
271   public function testBaseFieldCreateDeleteWithExistingEntities() {
272     // Save an entity.
273     $name = $this->randomString();
274     $storage = $this->entityManager->getStorage('entity_test_update');
275     $entity = $storage->create(['name' => $name]);
276     $entity->save();
277
278     // Add a base field and run the update. Ensure the base field's column is
279     // created and the prior saved entity data is still there.
280     $this->addBaseField();
281     $this->entityDefinitionUpdateManager->applyUpdates();
282     $schema_handler = $this->database->schema();
283     $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
284     $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
285     $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field creation.');
286
287     // Remove the base field and run the update. Ensure the base field's column
288     // is deleted and the prior saved entity data is still there.
289     $this->removeBaseField();
290     $this->entityDefinitionUpdateManager->applyUpdates();
291     $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
292     $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
293     $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field deletion.');
294
295     // Add a base field with a required property and run the update. Ensure
296     // 'not null' is not applied and thus no exception is thrown.
297     $this->addBaseField('shape_required');
298     $this->entityDefinitionUpdateManager->applyUpdates();
299     $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
300     $this->assertTrue($assert, 'Columns created in shared table for new_base_field.');
301
302     // Recreate the field after emptying the base table and check that its
303     // columns are not 'not null'.
304     // @todo Revisit this test when allowing for required storage field
305     //   definitions. See https://www.drupal.org/node/2390495.
306     $entity->delete();
307     $this->removeBaseField();
308     $this->entityDefinitionUpdateManager->applyUpdates();
309     $assert = !$schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && !$schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
310     $this->assert($assert, 'Columns removed from the shared table for new_base_field.');
311     $this->addBaseField('shape_required');
312     $this->entityDefinitionUpdateManager->applyUpdates();
313     $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
314     $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.');
315     $entity = $storage->create(['name' => $name]);
316     $entity->save();
317     $this->pass('The new_base_field columns are still nullable');
318   }
319
320   /**
321    * Tests creating and deleting a bundle field if entities exist.
322    *
323    * This tests deletion when there are existing entities, but not existing data
324    * for the field being deleted.
325    *
326    * @see testBundleFieldDeleteWithExistingData()
327    */
328   public function testBundleFieldCreateDeleteWithExistingEntities() {
329     // Save an entity.
330     $name = $this->randomString();
331     $storage = $this->entityManager->getStorage('entity_test_update');
332     $entity = $storage->create(['name' => $name]);
333     $entity->save();
334
335     // Add a bundle field and run the update. Ensure the bundle field's table
336     // is created and the prior saved entity data is still there.
337     $this->addBundleField();
338     $this->entityDefinitionUpdateManager->applyUpdates();
339     $schema_handler = $this->database->schema();
340     $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
341     $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
342     $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field creation.');
343
344     // Remove the base field and run the update. Ensure the bundle field's
345     // table is deleted and the prior saved entity data is still there.
346     $this->removeBundleField();
347     $this->entityDefinitionUpdateManager->applyUpdates();
348     $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
349     $entity = $this->entityManager->getStorage('entity_test_update')->load($entity->id());
350     $this->assertIdentical($entity->name->value, $name, 'Entity data preserved during field deletion.');
351
352     // Test that required columns are created as 'not null'.
353     $this->addBundleField('shape_required');
354     $this->entityDefinitionUpdateManager->applyUpdates();
355     $message = 'The new_bundle_field_shape column is not nullable.';
356     $values = [
357       'bundle' => $entity->bundle(),
358       'deleted' => 0,
359       'entity_id' => $entity->id(),
360       'revision_id' => $entity->id(),
361       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
362       'delta' => 0,
363       'new_bundle_field_color' => $this->randomString(),
364     ];
365     try {
366       // Try to insert a record without providing a value for the 'not null'
367       // column. This should fail.
368       $this->database->insert('entity_test_update__new_bundle_field')
369         ->fields($values)
370         ->execute();
371       $this->fail($message);
372     }
373     catch (\RuntimeException $e) {
374       if ($e instanceof DatabaseExceptionWrapper || $e instanceof IntegrityConstraintViolationException) {
375         // Now provide a value for the 'not null' column. This is expected to
376         // succeed.
377         $values['new_bundle_field_shape'] = $this->randomString();
378         $this->database->insert('entity_test_update__new_bundle_field')
379           ->fields($values)
380           ->execute();
381         $this->pass($message);
382       }
383       else {
384         // Keep throwing it.
385         throw $e;
386       }
387     }
388   }
389
390   /**
391    * Tests deleting a base field when it has existing data.
392    */
393   public function testBaseFieldDeleteWithExistingData() {
394     // Add the base field and run the update.
395     $this->addBaseField();
396     $this->entityDefinitionUpdateManager->applyUpdates();
397
398     // Save an entity with the base field populated.
399     $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
400
401     // Remove the base field and apply updates. It's expected to throw an
402     // exception.
403     // @todo Revisit that expectation once purging is implemented for
404     //   all fields: https://www.drupal.org/node/2282119.
405     $this->removeBaseField();
406     try {
407       $this->entityDefinitionUpdateManager->applyUpdates();
408       $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
409     }
410     catch (FieldStorageDefinitionUpdateForbiddenException $e) {
411       $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
412     }
413   }
414
415   /**
416    * Tests deleting a bundle field when it has existing data.
417    */
418   public function testBundleFieldDeleteWithExistingData() {
419     // Add the bundle field and run the update.
420     $this->addBundleField();
421     $this->entityDefinitionUpdateManager->applyUpdates();
422
423     // Save an entity with the bundle field populated.
424     entity_test_create_bundle('custom');
425     $this->entityManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
426
427     // Remove the bundle field and apply updates. It's expected to throw an
428     // exception.
429     // @todo Revisit that expectation once purging is implemented for
430     //   all fields: https://www.drupal.org/node/2282119.
431     $this->removeBundleField();
432     try {
433       $this->entityDefinitionUpdateManager->applyUpdates();
434       $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
435     }
436     catch (FieldStorageDefinitionUpdateForbiddenException $e) {
437       $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.');
438     }
439   }
440
441   /**
442    * Tests updating a base field when it has existing data.
443    */
444   public function testBaseFieldUpdateWithExistingData() {
445     // Add the base field and run the update.
446     $this->addBaseField();
447     $this->entityDefinitionUpdateManager->applyUpdates();
448
449     // Save an entity with the base field populated.
450     $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
451
452     // Change the field's field type and apply updates. It's expected to
453     // throw an exception.
454     $this->modifyBaseField();
455     try {
456       $this->entityDefinitionUpdateManager->applyUpdates();
457       $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
458     }
459     catch (FieldStorageDefinitionUpdateForbiddenException $e) {
460       $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
461     }
462   }
463
464   /**
465    * Tests updating a bundle field when it has existing data.
466    */
467   public function testBundleFieldUpdateWithExistingData() {
468     // Add the bundle field and run the update.
469     $this->addBundleField();
470     $this->entityDefinitionUpdateManager->applyUpdates();
471
472     // Save an entity with the bundle field populated.
473     entity_test_create_bundle('custom');
474     $this->entityManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
475
476     // Change the field's field type and apply updates. It's expected to
477     // throw an exception.
478     $this->modifyBundleField();
479     try {
480       $this->entityDefinitionUpdateManager->applyUpdates();
481       $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
482     }
483     catch (FieldStorageDefinitionUpdateForbiddenException $e) {
484       $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
485     }
486   }
487
488   /**
489    * Tests creating and deleting a multi-field index when there are no existing entities.
490    */
491   public function testEntityIndexCreateDeleteWithoutData() {
492     // Add an entity index and ensure the update manager reports that as an
493     // update to the entity type.
494     $this->addEntityIndex();
495     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
496     $expected = [
497       'entity_test_update' => [
498         t('The %entity_type entity type needs to be updated.', ['%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel()]),
499       ],
500     ];
501     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
502
503     // Run the update and ensure the new index is created.
504     $this->entityDefinitionUpdateManager->applyUpdates();
505     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
506
507     // Remove the index and ensure the update manager reports that as an
508     // update to the entity type.
509     $this->removeEntityIndex();
510     $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
511     $expected = [
512       'entity_test_update' => [
513         t('The %entity_type entity type needs to be updated.', ['%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel()]),
514       ],
515     ];
516     $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.');
517
518     // Run the update and ensure the index is deleted.
519     $this->entityDefinitionUpdateManager->applyUpdates();
520     $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
521
522     // Test that composite indexes are handled correctly when dropping and
523     // re-creating one of their columns.
524     $this->addEntityIndex();
525     $this->entityDefinitionUpdateManager->applyUpdates();
526     $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update');
527     $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
528     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
529     $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
530     $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
531     $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition);
532     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.');
533   }
534
535   /**
536    * Tests creating a multi-field index when there are existing entities.
537    */
538   public function testEntityIndexCreateWithData() {
539     // Save an entity.
540     $name = $this->randomString();
541     $entity = $this->entityManager->getStorage('entity_test_update')->create(['name' => $name]);
542     $entity->save();
543
544     // Add an entity index, run the update. Ensure that the index is created
545     // despite having data.
546     $this->addEntityIndex();
547     $this->entityDefinitionUpdateManager->applyUpdates();
548     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.');
549   }
550
551   /**
552    * Tests entity type and field storage definition events.
553    */
554   public function testDefinitionEvents() {
555     /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */
556     $event_subscriber = $this->container->get('entity_test.definition.subscriber');
557     $event_subscriber->enableEventTracking();
558
559     // Test field storage definition events.
560     $storage_definition = current($this->entityManager->getFieldStorageDefinitions('entity_test_rev'));
561     $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.');
562     $this->entityManager->onFieldStorageDefinitionDelete($storage_definition);
563     $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.');
564     $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.');
565     $this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
566     $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.');
567     $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.');
568     $this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $storage_definition);
569     $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.');
570
571     // Test entity type events.
572     $entity_type = $this->entityManager->getDefinition('entity_test_rev');
573     $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.');
574     $this->entityManager->onEntityTypeCreate($entity_type);
575     $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.');
576     $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.');
577     $this->entityManager->onEntityTypeUpdate($entity_type, $entity_type);
578     $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.');
579     $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.');
580     $this->entityManager->onEntityTypeDelete($entity_type);
581     $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.');
582   }
583
584   /**
585    * Tests updating entity schema and creating a base field.
586    *
587    * This tests updating entity schema and creating a base field at the same
588    * time when there are no existing entities.
589    */
590   public function testEntityTypeSchemaUpdateAndBaseFieldCreateWithoutData() {
591     $this->updateEntityTypeToRevisionable();
592     $this->addBaseField();
593     $message = 'Successfully updated entity schema and created base field at the same time.';
594     // Entity type updates create base fields as well, thus make sure doing both
595     // at the same time does not lead to errors due to the base field being
596     // created twice.
597     try {
598       $this->entityDefinitionUpdateManager->applyUpdates();
599       $this->pass($message);
600     }
601     catch (\Exception $e) {
602       $this->fail($message);
603       throw $e;
604     }
605   }
606
607   /**
608    * Tests updating entity schema and creating a revisionable base field.
609    *
610    * This tests updating entity schema and creating a revisionable base field
611    * at the same time when there are no existing entities.
612    */
613   public function testEntityTypeSchemaUpdateAndRevisionableBaseFieldCreateWithoutData() {
614     $this->updateEntityTypeToRevisionable();
615     $this->addRevisionableBaseField();
616     $message = 'Successfully updated entity schema and created revisionable base field at the same time.';
617     // Entity type updates create base fields as well, thus make sure doing both
618     // at the same time does not lead to errors due to the base field being
619     // created twice.
620     try {
621       $this->entityDefinitionUpdateManager->applyUpdates();
622       $this->pass($message);
623     }
624     catch (\Exception $e) {
625       $this->fail($message);
626       throw $e;
627     }
628   }
629
630   /**
631    * Tests applying single updates.
632    */
633   public function testSingleActionCalls() {
634     $db_schema = $this->database->schema();
635
636     // Ensure that a non-existing entity type cannot be installed.
637     $message = 'A non-existing entity type cannot be installed';
638     try {
639       $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo']));
640       $this->fail($message);
641     }
642     catch (PluginNotFoundException $e) {
643       $this->pass($message);
644     }
645
646     // Ensure that a field cannot be installed on non-existing entity type.
647     $message = 'A field cannot be installed on a non-existing entity type';
648     try {
649       $storage_definition = BaseFieldDefinition::create('string')
650         ->setLabel(t('A new revisionable base field'))
651         ->setRevisionable(TRUE);
652       $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition);
653       $this->fail($message);
654     }
655     catch (PluginNotFoundException $e) {
656       $this->pass($message);
657     }
658
659     // Ensure that a non-existing field cannot be installed.
660     $storage_definition = BaseFieldDefinition::create('string')
661       ->setLabel(t('A new revisionable base field'))
662       ->setRevisionable(TRUE);
663     $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'entity_test_update', 'entity_test', $storage_definition);
664     $this->assertFalse($db_schema->fieldExists('entity_test_update', 'bar'), "A non-existing field cannot be installed.");
665
666     // Ensure that installing an existing entity type is a no-op.
667     $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
668     $this->entityDefinitionUpdateManager->installEntityType($entity_type);
669     $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op');
670
671     // Create a new base field.
672     $this->addRevisionableBaseField();
673     $storage_definition = BaseFieldDefinition::create('string')
674       ->setLabel(t('A new revisionable base field'))
675       ->setRevisionable(TRUE);
676     $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
677     $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
678     $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
679
680     // Ensure that installing an existing field is a no-op.
681     $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
682     $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op');
683
684     // Update an existing field schema.
685     $this->modifyBaseField();
686     $storage_definition = BaseFieldDefinition::create('text')
687       ->setName('new_base_field')
688       ->setTargetEntityTypeId('entity_test_update')
689       ->setLabel(t('A new revisionable base field'))
690       ->setRevisionable(TRUE);
691     $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
692     $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists.");
693     $this->assertTrue(
694       $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
695       "New schema for 'new_base_field' has been created."
696     );
697
698     // Drop an existing field schema.
699     $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
700     $this->assertFalse(
701       $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
702       "The schema for 'new_base_field' has been dropped."
703     );
704
705     // Make the entity type revisionable.
706     $this->updateEntityTypeToRevisionable();
707     $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update.");
708     $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
709     $keys = $entity_type->getKeys();
710     $keys['revision'] = 'revision_id';
711     $entity_type->set('entity_keys', $keys);
712     $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
713     $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
714   }
715
716   /**
717    * Ensures that a new field and index on a shared table are created.
718    *
719    * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema
720    */
721   public function testCreateFieldAndIndexOnSharedTable() {
722     $this->addBaseField();
723     $this->addBaseFieldIndex();
724     $this->entityDefinitionUpdateManager->applyUpdates();
725     $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
726     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table.");
727     // Check index size in for MySQL.
728     if (Database::getConnection()->driver() == 'mysql') {
729       $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject();
730       $this->assertEqual(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
731     }
732   }
733
734   /**
735    * Ensures that a new entity level index is created when data exists.
736    *
737    * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate
738    */
739   public function testCreateIndexUsingEntityStorageSchemaWithData() {
740     // Save an entity.
741     $name = $this->randomString();
742     $storage = $this->entityManager->getStorage('entity_test_update');
743     $entity = $storage->create(['name' => $name]);
744     $entity->save();
745
746     // Create an index.
747     $indexes = [
748       'entity_test_update__type_index' => ['type'],
749     ];
750     $this->state->set('entity_test_update.additional_entity_indexes', $indexes);
751     $this->entityDefinitionUpdateManager->applyUpdates();
752     $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table.");
753     // Check index size in for MySQL.
754     if (Database::getConnection()->driver() == 'mysql') {
755       $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject();
756       $this->assertEqual(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
757     }
758   }
759
760   /**
761    * Tests updating a base field when it has existing data.
762    */
763   public function testBaseFieldEntityKeyUpdateWithExistingData() {
764     // Add the base field and run the update.
765     $this->addBaseField();
766     $this->entityDefinitionUpdateManager->applyUpdates();
767
768     // Save an entity with the base field populated.
769     $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save();
770
771     // Save an entity with the base field not populated.
772     /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
773     $entity = $this->entityManager->getStorage('entity_test_update')->create();
774     $entity->save();
775
776     // Promote the base field to an entity key. This will trigger the addition
777     // of a NOT NULL constraint.
778     $this->makeBaseFieldEntityKey();
779
780     // Field storage CRUD operations use the last installed entity type
781     // definition so we need to update it before doing any other field storage
782     // updates.
783     $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type'));
784
785     // Try to apply the update and verify they fail since we have a NULL value.
786     $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
787     try {
788       $this->entityDefinitionUpdateManager->applyUpdates();
789       $this->fail($message);
790     }
791     catch (EntityStorageException $e) {
792       $this->pass($message);
793     }
794
795     // Check that the update is correctly applied when no NULL data is left.
796     $entity->set('new_base_field', $this->randomString());
797     $entity->save();
798     $this->entityDefinitionUpdateManager->applyUpdates();
799     $this->pass('The update is correctly performed when no NULL data exists.');
800
801     // Check that the update actually applied a NOT NULL constraint.
802     $entity->set('new_base_field', NULL);
803     $message = 'The NOT NULL constraint was correctly applied.';
804     try {
805       $entity->save();
806       $this->fail($message);
807     }
808     catch (EntityStorageException $e) {
809       $this->pass($message);
810     }
811   }
812
813   /**
814    * Check that field schema is correctly handled with long-named fields.
815    */
816   public function testLongNameFieldIndexes() {
817     $this->addLongNameBaseField();
818     $entity_type_id = 'entity_test_update';
819     $entity_type = $this->entityManager->getDefinition($entity_type_id);
820     $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type);
821     $name = 'new_long_named_entity_reference_base_field';
822     $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]);
823     $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.');
824   }
825
826   /**
827    * Tests adding a base field with initial values.
828    */
829   public function testInitialValue() {
830     $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
831     $db_schema = $this->database->schema();
832
833     // Create two entities before adding the base field.
834     /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
835     $storage->create()->save();
836     $storage->create()->save();
837
838     // Add a base field with an initial value.
839     $this->addBaseField();
840     $storage_definition = BaseFieldDefinition::create('string')
841       ->setLabel(t('A new base field'))
842       ->setInitialValue('test value');
843
844     $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
845     $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
846     $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
847
848     // Check that the initial values have been applied.
849     $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
850     $entities = $storage->loadMultiple();
851     $this->assertEquals('test value', $entities[1]->get('new_base_field')->value);
852     $this->assertEquals('test value', $entities[2]->get('new_base_field')->value);
853   }
854
855   /**
856    * Tests adding a base field with initial values inherited from another field.
857    */
858   public function testInitialValueFromField() {
859     $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
860     $db_schema = $this->database->schema();
861
862     // Create two entities before adding the base field.
863     /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
864     $storage->create(['name' => 'First entity'])->save();
865     $storage->create(['name' => 'Second entity'])->save();
866
867     // Add a base field with an initial value inherited from another field.
868     $this->addBaseField();
869     $storage_definition = BaseFieldDefinition::create('string')
870       ->setLabel(t('A new base field'))
871       ->setInitialValueFromField('name');
872
873     $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
874     $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
875     $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
876
877     // Check that the initial values have been applied.
878     $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
879     $entities = $storage->loadMultiple();
880     $this->assertEquals('First entity', $entities[1]->get('new_base_field')->value);
881     $this->assertEquals('Second entity', $entities[2]->get('new_base_field')->value);
882   }
883
884   /**
885    * Tests the error handling when using initial values from another field.
886    */
887   public function testInitialValueFromFieldErrorHandling() {
888     // Check that setting invalid values for 'initial value from field' doesn't
889     // work.
890     try {
891       $this->addBaseField();
892       $storage_definition = BaseFieldDefinition::create('string')
893         ->setLabel(t('A new base field'))
894         ->setInitialValueFromField('field_that_does_not_exist');
895       $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
896       $this->fail('Using a non-existent field as initial value does not work.');
897     }
898     catch (FieldException $e) {
899       $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage());
900       $this->pass('Using a non-existent field as initial value does not work.');
901     }
902
903     try {
904       $this->addBaseField();
905       $storage_definition = BaseFieldDefinition::create('integer')
906         ->setLabel(t('A new base field'))
907         ->setInitialValueFromField('name');
908       $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
909       $this->fail('Using a field of a different type as initial value does not work.');
910     }
911     catch (FieldException $e) {
912       $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage());
913       $this->pass('Using a field of a different type as initial value does not work.');
914     }
915
916     try {
917       // Add a base field that will not be stored in the shared tables.
918       $initial_field = BaseFieldDefinition::create('string')
919         ->setName('initial_field')
920         ->setLabel(t('An initial field'))
921         ->setCardinality(2);
922       $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]);
923       $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field);
924
925       // Now add the base field which will try to use the previously added field
926       // as the source of its initial values.
927       $new_base_field = BaseFieldDefinition::create('string')
928         ->setName('new_base_field')
929         ->setLabel(t('A new base field'))
930         ->setInitialValueFromField('initial_field');
931       $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]);
932       $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field);
933       $this->fail('Using a field that is not stored in the shared tables as initial value does not work.');
934     }
935     catch (FieldException $e) {
936       $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage());
937       $this->pass('Using a field that is not stored in the shared tables as initial value does not work.');
938     }
939   }
940
941 }