3 namespace Drupal\Tests\field\Kernel;
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\field\Entity\FieldConfig;
7 use Drupal\field\Entity\FieldStorageConfig;
10 * Bulk delete storages and fields, and clean up afterwards.
14 class BulkDeleteTest extends FieldKernelTestBase {
17 * The fields to use in this test.
21 protected $fieldStorages;
24 * The entities to use in this test.
31 * The entities to use in this test, keyed by bundle.
35 protected $entitiesByBundles;
38 * The bundles for the entities used in this test.
45 * The entity type to be used in the test classes.
49 protected $entityTypeId = 'entity_test';
52 * Tests that the expected hooks have been invoked on the expected entities.
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().
61 public function checkHooksInvocations($expected_hooks, $actual_hooks) {
62 foreach ($expected_hooks as $hook => $invocations) {
63 $actual_invocations = $actual_hooks[$hook];
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.");
68 // Check that the hook was called for each expected argument.
69 foreach ($invocations as $argument) {
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()) {
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) {
85 $this->assertTrue($found, "$hook() was called on expected argument");
90 protected function setUp() {
93 $this->fieldStorages = [];
95 $this->entitiesByBundles = [];
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);
103 // Create two field storages.
104 $field_storage = FieldStorageConfig::create([
105 'field_name' => 'bf_1',
106 'entity_type' => $this->entityTypeId,
107 'type' => 'test_field',
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',
118 $field_storage->save();
119 $this->fieldStorages[] = $field_storage;
121 // For each bundle, create each field, and 10 entities with values for the
123 foreach ($this->bundles as $bundle) {
124 foreach ($this->fieldStorages as $field_storage) {
125 FieldConfig::create([
126 'field_storage' => $field_storage,
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()));
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;
148 // Also keep track of the entities per bundle.
149 $this->entitiesByBundles[$entity->bundle()][$entity->id()] = $entity;
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.
158 * This tests how EntityFieldQuery interacts with field deletion and could be
159 * moved to FieldCrudTestCase, but depends on this class's setUp().
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');
167 // There are 10 entities of this bundle.
168 $found = $factory->get('entity_test')
169 ->condition('type', $bundle)
171 $this->assertEqual(count($found), 10, 'Correct number of entities found before deleting');
174 $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
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');
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')
192 foreach ($result as $row) {
193 $this->assertEqual($this->entities[$row->entity_id]->{$field_name}->value, $row->$column);
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)
201 $this->assertFalse($found, 'No entities found after deleting');
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)
210 $this->assertEqual(count($found), 10, 'Correct number of entities found after deleting');
211 $this->assertFalse(array_diff($found, array_keys($this->entities)));
215 * Tests that recreating a field with the name as a deleted field works.
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',
227 $deleted_field_storage->save();
229 FieldConfig::create([
230 'field_storage' => $deleted_field_storage,
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));
243 $deleted_field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
244 $deleted_field->delete();
245 $deleted_field_uuid = $deleted_field->uuid();
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);
251 // Create the field again.
252 $field_storage = FieldStorageConfig::create([
253 'field_name' => $field_name,
254 'entity_type' => $this->entityTypeId,
255 'type' => 'test_field',
258 $field_storage->save();
259 FieldConfig::create([
260 'field_storage' => $field_storage,
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);
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));
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);
284 field_purge_batch(50);
286 // Ensure the new field still has its table and the deleted one has been
288 $this->assertTrue(\Drupal::database()->schema()->tableExists($active_table_name));
289 $this->assertFalse(\Drupal::database()->schema()->tableExists($deleted_table_name));
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');
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)
303 $this->assertEqual($count, 10);
307 * Verify that field data items and fields are purged when a field storage is
310 public function testPurgeField() {
311 // Start recording hook invocations.
312 field_test_memorize();
314 $bundle = reset($this->bundles);
315 $field_storage = reset($this->fieldStorages);
316 $field_name = $field_storage->getName();
319 $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
322 // No field hooks were called.
323 $mem = field_test_memorize();
324 $this->assertEqual(count($mem), 0, 'No field hooks were called');
327 for ($count = 8; $count >= 0; $count -= $batch_size) {
328 // Purge two entities.
329 field_purge_batch($batch_size);
331 // There are $count deleted entities left.
332 $found = \Drupal::entityQuery('entity_test')
333 ->condition('type', $bundle)
334 ->condition($field_name . '.deleted', 1)
336 $this->assertEqual(count($found), $count, 'Correct number of entities found after purging 2');
339 // Check hooks invocations.
340 // FieldItemInterface::delete() should have been called once for each entity in the
342 $actual_hooks = field_test_memorize();
344 $entities = $this->entitiesByBundles[$bundle];
345 foreach ($entities as $id => $entity) {
346 $hooks['field_test_field_delete'][] = $entity;
348 $this->checkHooksInvocations($hooks, $actual_hooks);
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');
355 field_purge_batch($batch_size);
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');
361 // The field storage still exists, not deleted, because it has a second
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');
368 * Verify that field storages are preserved and purged correctly as multiple
369 * fields are deleted and purged.
371 public function testPurgeFieldStorage() {
372 // Start recording hook invocations.
373 field_test_memorize();
375 $field_storage = reset($this->fieldStorages);
376 $field_name = $field_storage->getName();
378 // Delete the first field.
379 $bundle = reset($this->bundles);
380 $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
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.');
388 field_purge_batch(10);
390 // Check hooks invocations.
391 // FieldItemInterface::delete() should have been called once for each entity in the
393 $actual_hooks = field_test_memorize();
395 $entities = $this->entitiesByBundles[$bundle];
396 foreach ($entities as $id => $entity) {
397 $hooks['field_test_field_delete'][] = $entity;
399 $this->checkHooksInvocations($hooks, $actual_hooks);
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');
405 // Purge again to purge the field.
406 field_purge_batch(0);
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');
415 // Delete the second field.
416 $bundle = next($this->bundles);
417 $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
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.');
425 field_purge_batch(10);
427 // Check hooks invocations (same as above, for the 2nd bundle).
428 $actual_hooks = field_test_memorize();
430 $entities = $this->entitiesByBundles[$bundle];
431 foreach ($entities as $id => $entity) {
432 $hooks['field_test_field_delete'][] = $entity;
434 $this->checkHooksInvocations($hooks, $actual_hooks);
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');
442 // Purge again to purge the field and the storage.
443 field_purge_batch(0);
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.');