900c154b4b065bae865df958ad81be5d687c2bef
[yaffs-website] / web / core / modules / field / tests / src / Kernel / BulkDeleteTest.php
1 <?php
2
3 namespace Drupal\Tests\field\Kernel;
4
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\field\Entity\FieldConfig;
7 use Drupal\field\Entity\FieldStorageConfig;
8
9 /**
10  * Bulk delete storages and fields, and clean up afterwards.
11  *
12  * @group field
13  */
14 class BulkDeleteTest extends FieldKernelTestBase {
15
16   /**
17    * The fields to use in this test.
18    *
19    * @var array
20    */
21   protected $fieldStorages;
22
23   /**
24    * The entities to use in this test.
25    *
26    * @var array
27    */
28   protected $entities;
29
30   /**
31    * The entities to use in this test, keyed by bundle.
32    *
33    * @var array
34    */
35   protected $entitiesByBundles;
36
37   /**
38    * The bundles for the entities used in this test.
39    *
40    * @var array
41    */
42   protected $bundles;
43
44   /**
45    * The entity type to be used in the test classes.
46    *
47    * @var string
48    */
49   protected $entityTypeId = 'entity_test';
50
51   /**
52    * Tests that the expected hooks have been invoked on the expected entities.
53    *
54    * @param $expected_hooks
55    *   An array keyed by hook name, with one entry per expected invocation.
56    *   Each entry is the value of the "$entity" parameter the hook is expected
57    *   to have been passed.
58    * @param $actual_hooks
59    *   The array of actual hook invocations recorded by field_test_memorize().
60    */
61   public function checkHooksInvocations($expected_hooks, $actual_hooks) {
62     foreach ($expected_hooks as $hook => $invocations) {
63       $actual_invocations = $actual_hooks[$hook];
64
65       // Check that the number of invocations is correct.
66       $this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times.");
67
68       // Check that the hook was called for each expected argument.
69       foreach ($invocations as $argument) {
70         $found = FALSE;
71         foreach ($actual_invocations as $actual_arguments) {
72           // The argument we are looking for is either an array of entities as
73           // the second argument or a single entity object as the first.
74           if ($argument instanceof EntityInterface && $actual_arguments[0]->id() == $argument->id()) {
75             $found = TRUE;
76             break;
77           }
78           // In case of an array, compare the array size and make sure it
79           // contains the same elements.
80           elseif (is_array($argument) && count($actual_arguments[1]) == count($argument) && count(array_diff_key($actual_arguments[1], $argument)) == 0) {
81             $found = TRUE;
82             break;
83           }
84         }
85         $this->assertTrue($found, "$hook() was called on expected argument");
86       }
87     }
88   }
89
90   protected function setUp() {
91     parent::setUp();
92
93     $this->fieldStorages = [];
94     $this->entities = [];
95     $this->entitiesByBundles = [];
96
97     // Create two bundles.
98     $this->bundles = ['bb_1' => 'bb_1', 'bb_2' => 'bb_2'];
99     foreach ($this->bundles as $name => $desc) {
100       entity_test_create_bundle($name, $desc);
101     }
102
103     // Create two field storages.
104     $field_storage = FieldStorageConfig::create([
105       'field_name' => 'bf_1',
106       'entity_type' => $this->entityTypeId,
107       'type' => 'test_field',
108       'cardinality' => 1
109     ]);
110     $field_storage->save();
111     $this->fieldStorages[] = $field_storage;
112     $field_storage = FieldStorageConfig::create([
113       'field_name' => 'bf_2',
114       'entity_type' => $this->entityTypeId,
115       'type' => 'test_field',
116       'cardinality' => 4
117     ]);
118     $field_storage->save();
119     $this->fieldStorages[] = $field_storage;
120
121     // For each bundle, create each field, and 10 entities with values for the
122     // fields.
123     foreach ($this->bundles as $bundle) {
124       foreach ($this->fieldStorages as $field_storage) {
125         FieldConfig::create([
126           'field_storage' => $field_storage,
127           'bundle' => $bundle,
128         ])->save();
129       }
130       for ($i = 0; $i < 10; $i++) {
131         $entity = $this->container->get('entity_type.manager')
132           ->getStorage($this->entityTypeId)
133           ->create(['type' => $bundle]);
134         foreach ($this->fieldStorages as $field_storage) {
135           $entity->{$field_storage->getName()}->setValue($this->_generateTestFieldValues($field_storage->getCardinality()));
136         }
137         $entity->save();
138       }
139     }
140     $this->entities = $this->container->get('entity_type.manager')
141       ->getStorage($this->entityTypeId)->loadMultiple();
142     foreach ($this->entities as $entity) {
143       // This test relies on the entities having stale field definitions
144       // so that the deleted field can be accessed on them. Access the field
145       // now, so that they are always loaded.
146       $entity->bf_1->value;
147
148       // Also keep track of the entities per bundle.
149       $this->entitiesByBundles[$entity->bundle()][$entity->id()] = $entity;
150     }
151   }
152
153   /**
154    * Verify that deleting a field leaves the field data items in the database
155    * and that the appropriate Field API functions can operate on the deleted
156    * data and field definition.
157    *
158    * This tests how EntityFieldQuery interacts with field deletion and could be
159    * moved to FieldCrudTestCase, but depends on this class's setUp().
160    */
161   public function testDeleteField() {
162     $bundle = reset($this->bundles);
163     $field_storage = reset($this->fieldStorages);
164     $field_name = $field_storage->getName();
165     $factory = \Drupal::service('entity.query');
166
167     // There are 10 entities of this bundle.
168     $found = $factory->get('entity_test')
169       ->condition('type', $bundle)
170       ->execute();
171     $this->assertEqual(count($found), 10, 'Correct number of entities found before deleting');
172
173     // Delete the field.
174     $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
175     $field->delete();
176
177     // The field still exists, deleted.
178     $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
179     $this->assertEqual(count($fields), 1, 'There is one deleted field');
180     $field = $fields[$field->uuid()];
181     $this->assertEqual($field->getTargetBundle(), $bundle, 'The deleted field is for the correct bundle');
182
183     // Check that the actual stored content did not change during delete.
184     $storage = \Drupal::entityManager()->getStorage($this->entityTypeId);
185     /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
186     $table_mapping = $storage->getTableMapping();
187     $table = $table_mapping->getDedicatedDataTableName($field_storage);
188     $column = $table_mapping->getFieldColumnName($field_storage, 'value');
189     $result = db_select($table, 't')
190       ->fields('t')
191       ->execute();
192     foreach ($result as $row) {
193       $this->assertEqual($this->entities[$row->entity_id]->{$field_name}->value, $row->$column);
194     }
195
196     // There are 0 entities of this bundle with non-deleted data.
197     $found = $factory->get('entity_test')
198       ->condition('type', $bundle)
199       ->condition("$field_name.deleted", 0)
200       ->execute();
201     $this->assertFalse($found, 'No entities found after deleting');
202
203     // There are 10 entities of this bundle when deleted fields are allowed, and
204     // their values are correct.
205     $found = $factory->get('entity_test')
206       ->condition('type', $bundle)
207       ->condition("$field_name.deleted", 1)
208       ->sort('id')
209       ->execute();
210     $this->assertEqual(count($found), 10, 'Correct number of entities found after deleting');
211     $this->assertFalse(array_diff($found, array_keys($this->entities)));
212   }
213
214   /**
215    * Tests that recreating a field with the name as a deleted field works.
216    */
217   public function testPurgeWithDeletedAndActiveField() {
218     $bundle = reset($this->bundles);
219     // Create another field storage.
220     $field_name = 'bf_3';
221     $deleted_field_storage = FieldStorageConfig::create([
222       'field_name' => $field_name,
223       'entity_type' => $this->entityTypeId,
224       'type' => 'test_field',
225       'cardinality' => 1
226     ]);
227     $deleted_field_storage->save();
228     // Create the field.
229     FieldConfig::create([
230       'field_storage' => $deleted_field_storage,
231       'bundle' => $bundle,
232     ])->save();
233
234     for ($i = 0; $i < 20; $i++) {
235       $entity = $this->container->get('entity_type.manager')
236         ->getStorage($this->entityTypeId)
237         ->create(['type' => $bundle]);
238       $entity->{$field_name}->setValue($this->_generateTestFieldValues(1));
239       $entity->save();
240     }
241
242     // Delete the field.
243     $deleted_field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
244     $deleted_field->delete();
245     $deleted_field_uuid = $deleted_field->uuid();
246
247     // Reload the field storage.
248     $field_storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $deleted_field_storage->uuid(), 'include_deleted' => TRUE]);
249     $deleted_field_storage = reset($field_storages);
250
251     // Create the field again.
252     $field_storage = FieldStorageConfig::create([
253       'field_name' => $field_name,
254       'entity_type' => $this->entityTypeId,
255       'type' => 'test_field',
256       'cardinality' => 1
257     ]);
258     $field_storage->save();
259     FieldConfig::create([
260       'field_storage' => $field_storage,
261       'bundle' => $bundle,
262     ])->save();
263
264     // The field still exists, deleted, with the same field name.
265     $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $deleted_field_uuid, 'include_deleted' => TRUE]);
266     $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->isDeleted(), 'The field exists and is deleted');
267     $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->getName() == $field_name);
268
269     for ($i = 0; $i < 10; $i++) {
270       $entity = $this->container->get('entity_type.manager')
271         ->getStorage($this->entityTypeId)
272         ->create(['type' => $bundle]);
273       $entity->{$field_name}->setValue($this->_generateTestFieldValues(1));
274       $entity->save();
275     }
276
277     // Check that the two field storages have different tables.
278     $storage = \Drupal::entityManager()->getStorage($this->entityTypeId);
279     /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
280     $table_mapping = $storage->getTableMapping();
281     $deleted_table_name = $table_mapping->getDedicatedDataTableName($deleted_field_storage, TRUE);
282     $active_table_name = $table_mapping->getDedicatedDataTableName($field_storage);
283
284     field_purge_batch(50);
285
286     // Ensure the new field still has its table and the deleted one has been
287     // removed.
288     $this->assertTrue(\Drupal::database()->schema()->tableExists($active_table_name));
289     $this->assertFalse(\Drupal::database()->schema()->tableExists($deleted_table_name));
290
291     // The field has been removed from the system.
292     $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $deleted_field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
293     $this->assertEqual(count($fields), 0, 'The field is gone');
294
295     // Verify there are still 10 entries in the main table.
296     $count = \Drupal::database()
297       ->select('entity_test__' . $field_name, 'f')
298       ->fields('f', ['entity_id'])
299       ->condition('bundle', $bundle)
300       ->countQuery()
301       ->execute()
302       ->fetchField();
303     $this->assertEqual($count, 10);
304   }
305
306   /**
307    * Verify that field data items and fields are purged when a field storage is
308    * deleted.
309    */
310   public function testPurgeField() {
311     // Start recording hook invocations.
312     field_test_memorize();
313
314     $bundle = reset($this->bundles);
315     $field_storage = reset($this->fieldStorages);
316     $field_name = $field_storage->getName();
317
318     // Delete the field.
319     $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
320     $field->delete();
321
322     // No field hooks were called.
323     $mem = field_test_memorize();
324     $this->assertEqual(count($mem), 0, 'No field hooks were called');
325
326     $batch_size = 2;
327     for ($count = 8; $count >= 0; $count -= $batch_size) {
328       // Purge two entities.
329       field_purge_batch($batch_size);
330
331       // There are $count deleted entities left.
332       $found = \Drupal::entityQuery('entity_test')
333         ->condition('type', $bundle)
334         ->condition($field_name . '.deleted', 1)
335         ->execute();
336       $this->assertEqual(count($found), $count, 'Correct number of entities found after purging 2');
337     }
338
339     // Check hooks invocations.
340     // FieldItemInterface::delete() should have been called once for each entity in the
341     // bundle.
342     $actual_hooks = field_test_memorize();
343     $hooks = [];
344     $entities = $this->entitiesByBundles[$bundle];
345     foreach ($entities as $id => $entity) {
346       $hooks['field_test_field_delete'][] = $entity;
347     }
348     $this->checkHooksInvocations($hooks, $actual_hooks);
349
350     // The field still exists, deleted.
351     $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
352     $this->assertEqual(count($fields), 1, 'There is one deleted field');
353
354     // Purge the field.
355     field_purge_batch($batch_size);
356
357     // The field is gone.
358     $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
359     $this->assertEqual(count($fields), 0, 'The field is gone');
360
361     // The field storage still exists, not deleted, because it has a second
362     // field.
363     $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
364     $this->assertTrue(isset($storages[$field_storage->uuid()]), 'The field storage exists and is not deleted');
365   }
366
367   /**
368    * Verify that field storages are preserved and purged correctly as multiple
369    * fields are deleted and purged.
370    */
371   public function testPurgeFieldStorage() {
372     // Start recording hook invocations.
373     field_test_memorize();
374
375     $field_storage = reset($this->fieldStorages);
376     $field_name = $field_storage->getName();
377
378     // Delete the first field.
379     $bundle = reset($this->bundles);
380     $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
381     $field->delete();
382
383     // Assert that FieldItemInterface::delete() was not called yet.
384     $mem = field_test_memorize();
385     $this->assertEqual(count($mem), 0, 'No field hooks were called.');
386
387     // Purge the data.
388     field_purge_batch(10);
389
390     // Check hooks invocations.
391     // FieldItemInterface::delete() should have been called once for each entity in the
392     // bundle.
393     $actual_hooks = field_test_memorize();
394     $hooks = [];
395     $entities = $this->entitiesByBundles[$bundle];
396     foreach ($entities as $id => $entity) {
397       $hooks['field_test_field_delete'][] = $entity;
398     }
399     $this->checkHooksInvocations($hooks, $actual_hooks);
400
401     // The field still exists, deleted.
402     $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
403     $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted');
404
405     // Purge again to purge the field.
406     field_purge_batch(0);
407
408     // The field is gone.
409     $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
410     $this->assertEqual(count($fields), 0, 'The field is purged.');
411     // The field storage still exists, not deleted.
412     $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
413     $this->assertTrue(isset($storages[$field_storage->uuid()]) && !$storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is not deleted');
414
415     // Delete the second field.
416     $bundle = next($this->bundles);
417     $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
418     $field->delete();
419
420     // Assert that FieldItemInterface::delete() was not called yet.
421     $mem = field_test_memorize();
422     $this->assertEqual(count($mem), 0, 'No field hooks were called.');
423
424     // Purge the data.
425     field_purge_batch(10);
426
427     // Check hooks invocations (same as above, for the 2nd bundle).
428     $actual_hooks = field_test_memorize();
429     $hooks = [];
430     $entities = $this->entitiesByBundles[$bundle];
431     foreach ($entities as $id => $entity) {
432       $hooks['field_test_field_delete'][] = $entity;
433     }
434     $this->checkHooksInvocations($hooks, $actual_hooks);
435
436     // The field and the storage still exist, deleted.
437     $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
438     $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted');
439     $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
440     $this->assertTrue(isset($storages[$field_storage->uuid()]) && $storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is deleted');
441
442     // Purge again to purge the field and the storage.
443     field_purge_batch(0);
444
445     // The field and the storage are gone.
446     $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
447     $this->assertEqual(count($fields), 0, 'The field is purged.');
448     $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
449     $this->assertEqual(count($storages), 0, 'The field storage is purged.');
450   }
451
452 }