$invocations) { $actual_invocations = $actual_hooks[$hook]; // Check that the number of invocations is correct. $this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times."); // Check that the hook was called for each expected argument. foreach ($invocations as $argument) { $found = FALSE; foreach ($actual_invocations as $actual_arguments) { // The argument we are looking for is either an array of entities as // the second argument or a single entity object as the first. if ($argument instanceof EntityInterface && $actual_arguments[0]->id() == $argument->id()) { $found = TRUE; break; } // In case of an array, compare the array size and make sure it // contains the same elements. elseif (is_array($argument) && count($actual_arguments[1]) == count($argument) && count(array_diff_key($actual_arguments[1], $argument)) == 0) { $found = TRUE; break; } } $this->assertTrue($found, "$hook() was called on expected argument"); } } } protected function setUp() { parent::setUp(); $this->fieldStorages = []; $this->entities = []; $this->entitiesByBundles = []; // Create two bundles. $this->bundles = ['bb_1' => 'bb_1', 'bb_2' => 'bb_2']; foreach ($this->bundles as $name => $desc) { entity_test_create_bundle($name, $desc); } // Create two field storages. $field_storage = FieldStorageConfig::create([ 'field_name' => 'bf_1', 'entity_type' => $this->entityTypeId, 'type' => 'test_field', 'cardinality' => 1 ]); $field_storage->save(); $this->fieldStorages[] = $field_storage; $field_storage = FieldStorageConfig::create([ 'field_name' => 'bf_2', 'entity_type' => $this->entityTypeId, 'type' => 'test_field', 'cardinality' => 4 ]); $field_storage->save(); $this->fieldStorages[] = $field_storage; // For each bundle, create each field, and 10 entities with values for the // fields. foreach ($this->bundles as $bundle) { foreach ($this->fieldStorages as $field_storage) { FieldConfig::create([ 'field_storage' => $field_storage, 'bundle' => $bundle, ])->save(); } for ($i = 0; $i < 10; $i++) { $entity = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId) ->create(['type' => $bundle]); foreach ($this->fieldStorages as $field_storage) { $entity->{$field_storage->getName()}->setValue($this->_generateTestFieldValues($field_storage->getCardinality())); } $entity->save(); } } $this->entities = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId)->loadMultiple(); foreach ($this->entities as $entity) { // This test relies on the entities having stale field definitions // so that the deleted field can be accessed on them. Access the field // now, so that they are always loaded. $entity->bf_1->value; // Also keep track of the entities per bundle. $this->entitiesByBundles[$entity->bundle()][$entity->id()] = $entity; } } /** * Verify that deleting a field leaves the field data items in the database * and that the appropriate Field API functions can operate on the deleted * data and field definition. * * This tests how EntityFieldQuery interacts with field deletion and could be * moved to FieldCrudTestCase, but depends on this class's setUp(). */ public function testDeleteField() { $bundle = reset($this->bundles); $field_storage = reset($this->fieldStorages); $field_name = $field_storage->getName(); $factory = \Drupal::service('entity.query'); // There are 10 entities of this bundle. $found = $factory->get('entity_test') ->condition('type', $bundle) ->execute(); $this->assertEqual(count($found), 10, 'Correct number of entities found before deleting'); // Delete the field. $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name); $field->delete(); // The field still exists, deleted. $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]); $this->assertEqual(count($fields), 1, 'There is one deleted field'); $field = $fields[$field->uuid()]; $this->assertEqual($field->getTargetBundle(), $bundle, 'The deleted field is for the correct bundle'); // Check that the actual stored content did not change during delete. $storage = \Drupal::entityManager()->getStorage($this->entityTypeId); /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ $table_mapping = $storage->getTableMapping(); $table = $table_mapping->getDedicatedDataTableName($field_storage); $column = $table_mapping->getFieldColumnName($field_storage, 'value'); $result = db_select($table, 't') ->fields('t') ->execute(); foreach ($result as $row) { $this->assertEqual($this->entities[$row->entity_id]->{$field_name}->value, $row->$column); } // There are 0 entities of this bundle with non-deleted data. $found = $factory->get('entity_test') ->condition('type', $bundle) ->condition("$field_name.deleted", 0) ->execute(); $this->assertFalse($found, 'No entities found after deleting'); // There are 10 entities of this bundle when deleted fields are allowed, and // their values are correct. $found = $factory->get('entity_test') ->condition('type', $bundle) ->condition("$field_name.deleted", 1) ->sort('id') ->execute(); $this->assertEqual(count($found), 10, 'Correct number of entities found after deleting'); $this->assertFalse(array_diff($found, array_keys($this->entities))); } /** * Tests that recreating a field with the name as a deleted field works. */ public function testPurgeWithDeletedAndActiveField() { $bundle = reset($this->bundles); // Create another field storage. $field_name = 'bf_3'; $deleted_field_storage = FieldStorageConfig::create([ 'field_name' => $field_name, 'entity_type' => $this->entityTypeId, 'type' => 'test_field', 'cardinality' => 1 ]); $deleted_field_storage->save(); // Create the field. FieldConfig::create([ 'field_storage' => $deleted_field_storage, 'bundle' => $bundle, ])->save(); for ($i = 0; $i < 20; $i++) { $entity = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId) ->create(['type' => $bundle]); $entity->{$field_name}->setValue($this->_generateTestFieldValues(1)); $entity->save(); } // Delete the field. $deleted_field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name); $deleted_field->delete(); $deleted_field_uuid = $deleted_field->uuid(); // Reload the field storage. $field_storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $deleted_field_storage->uuid(), 'include_deleted' => TRUE]); $deleted_field_storage = reset($field_storages); // Create the field again. $field_storage = FieldStorageConfig::create([ 'field_name' => $field_name, 'entity_type' => $this->entityTypeId, 'type' => 'test_field', 'cardinality' => 1 ]); $field_storage->save(); FieldConfig::create([ 'field_storage' => $field_storage, 'bundle' => $bundle, ])->save(); // The field still exists, deleted, with the same field name. $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $deleted_field_uuid, 'include_deleted' => TRUE]); $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->isDeleted(), 'The field exists and is deleted'); $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->getName() == $field_name); for ($i = 0; $i < 10; $i++) { $entity = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId) ->create(['type' => $bundle]); $entity->{$field_name}->setValue($this->_generateTestFieldValues(1)); $entity->save(); } // Check that the two field storages have different tables. $storage = \Drupal::entityManager()->getStorage($this->entityTypeId); /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ $table_mapping = $storage->getTableMapping(); $deleted_table_name = $table_mapping->getDedicatedDataTableName($deleted_field_storage, TRUE); $active_table_name = $table_mapping->getDedicatedDataTableName($field_storage); field_purge_batch(50); // Ensure the new field still has its table and the deleted one has been // removed. $this->assertTrue(\Drupal::database()->schema()->tableExists($active_table_name)); $this->assertFalse(\Drupal::database()->schema()->tableExists($deleted_table_name)); // The field has been removed from the system. $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $deleted_field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]); $this->assertEqual(count($fields), 0, 'The field is gone'); // Verify there are still 10 entries in the main table. $count = \Drupal::database() ->select('entity_test__' . $field_name, 'f') ->fields('f', ['entity_id']) ->condition('bundle', $bundle) ->countQuery() ->execute() ->fetchField(); $this->assertEqual($count, 10); } /** * Verify that field data items and fields are purged when a field storage is * deleted. */ public function testPurgeField() { // Start recording hook invocations. field_test_memorize(); $bundle = reset($this->bundles); $field_storage = reset($this->fieldStorages); $field_name = $field_storage->getName(); // Delete the field. $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name); $field->delete(); // No field hooks were called. $mem = field_test_memorize(); $this->assertEqual(count($mem), 0, 'No field hooks were called'); $batch_size = 2; for ($count = 8; $count >= 0; $count -= $batch_size) { // Purge two entities. field_purge_batch($batch_size); // There are $count deleted entities left. $found = \Drupal::entityQuery('entity_test') ->condition('type', $bundle) ->condition($field_name . '.deleted', 1) ->execute(); $this->assertEqual(count($found), $count, 'Correct number of entities found after purging 2'); } // Check hooks invocations. // FieldItemInterface::delete() should have been called once for each entity in the // bundle. $actual_hooks = field_test_memorize(); $hooks = []; $entities = $this->entitiesByBundles[$bundle]; foreach ($entities as $id => $entity) { $hooks['field_test_field_delete'][] = $entity; } $this->checkHooksInvocations($hooks, $actual_hooks); // The field still exists, deleted. $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]); $this->assertEqual(count($fields), 1, 'There is one deleted field'); // Purge the field. field_purge_batch($batch_size); // The field is gone. $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]); $this->assertEqual(count($fields), 0, 'The field is gone'); // The field storage still exists, not deleted, because it has a second // field. $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]); $this->assertTrue(isset($storages[$field_storage->uuid()]), 'The field storage exists and is not deleted'); } /** * Verify that field storages are preserved and purged correctly as multiple * fields are deleted and purged. */ public function testPurgeFieldStorage() { // Start recording hook invocations. field_test_memorize(); $field_storage = reset($this->fieldStorages); $field_name = $field_storage->getName(); // Delete the first field. $bundle = reset($this->bundles); $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name); $field->delete(); // Assert that FieldItemInterface::delete() was not called yet. $mem = field_test_memorize(); $this->assertEqual(count($mem), 0, 'No field hooks were called.'); // Purge the data. field_purge_batch(10); // Check hooks invocations. // FieldItemInterface::delete() should have been called once for each entity in the // bundle. $actual_hooks = field_test_memorize(); $hooks = []; $entities = $this->entitiesByBundles[$bundle]; foreach ($entities as $id => $entity) { $hooks['field_test_field_delete'][] = $entity; } $this->checkHooksInvocations($hooks, $actual_hooks); // The field still exists, deleted. $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]); $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted'); // Purge again to purge the field. field_purge_batch(0); // The field is gone. $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]); $this->assertEqual(count($fields), 0, 'The field is purged.'); // The field storage still exists, not deleted. $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]); $this->assertTrue(isset($storages[$field_storage->uuid()]) && !$storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is not deleted'); // Delete the second field. $bundle = next($this->bundles); $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name); $field->delete(); // Assert that FieldItemInterface::delete() was not called yet. $mem = field_test_memorize(); $this->assertEqual(count($mem), 0, 'No field hooks were called.'); // Purge the data. field_purge_batch(10); // Check hooks invocations (same as above, for the 2nd bundle). $actual_hooks = field_test_memorize(); $hooks = []; $entities = $this->entitiesByBundles[$bundle]; foreach ($entities as $id => $entity) { $hooks['field_test_field_delete'][] = $entity; } $this->checkHooksInvocations($hooks, $actual_hooks); // The field and the storage still exist, deleted. $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]); $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted'); $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]); $this->assertTrue(isset($storages[$field_storage->uuid()]) && $storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is deleted'); // Purge again to purge the field and the storage. field_purge_batch(0); // The field and the storage are gone. $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]); $this->assertEqual(count($fields), 0, 'The field is purged.'); $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]); $this->assertEqual(count($storages), 0, 'The field storage is purged.'); } }