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