diff -u b/core/modules/field/field.module b/core/modules/field/field.module --- b/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -371,6 +371,23 @@ - $fields = \Drupal\field\FieldConfigImporter::getFieldsToPurge($config_importer); + $fields = \Drupal\field\FieldConfigImporter::getFieldsToPurge($config_importer->getStorageComparer()); if (count($fields)) { array_unshift($sync_steps, array('\Drupal\field\FieldConfigImporter', 'processDeletedFields')); }; } +/** + * Implements hook_form_FORM_ID_alter(). + * + * Adds a warning if field data will be permanently removed by the configuration + * synchronisation. + * + * @see \Drupal\field\FieldConfigImporter + */ +function field_form_config_admin_import_form_alter(&$form, &$form_state) { + if (isset($form_state['storage_comparer'])) { + $fields = \Drupal\field\FieldConfigImporter::getFieldsToPurge($form_state['storage_comparer']); + if (count($fields)) { + // @todo improve message and don't use dsm. + drupal_set_message(t('Field data will be deleted by this synchronisation.'), 'warning'); + } + } +} diff -u b/core/modules/field/lib/Drupal/field/FieldConfigImporter.php b/core/modules/field/lib/Drupal/field/FieldConfigImporter.php --- b/core/modules/field/lib/Drupal/field/FieldConfigImporter.php +++ b/core/modules/field/lib/Drupal/field/FieldConfigImporter.php @@ -1,18 +1,19 @@ get('purge_batch_size'); + static::calculateNumberOfBatchSteps($context, $config_importer); } + // Get the list of fields to purge. - $fields = static::getFieldsToPurge($config_importer); + $fields = static::getFieldsToPurge($config_importer->getStorageComparer(), $config_importer->getUnprocessedConfiguration('delete')); if (!isset($context['sandbox']['field']['current_field_id']) || $context['sandbox']['field']['current_field_id'] != $fields[0]->id()) { $context['sandbox']['field']['current_field_id'] = $fields[0]->id(); // If the field has not been deleted yet we need to do that. This is the @@ -43,14 +40,43 @@ } } field_purge_batch($context['sandbox']['field']['purge_batch_size']); - $fields_to_delete_count = count(static::getFieldsToPurge($config_importer)); + $context['sandbox']['field']['current_progress']++; + $fields_to_delete_count = count(static::getFieldsToPurge($config_importer->getStorageComparer(), $config_importer->getUnprocessedConfiguration('delete'))); if ($fields_to_delete_count == 0) { $context['finished'] = 1; } else { - $context['finished'] = ($context['sandbox']['field']['fields_to_delete_count'] - $fields_to_delete_count) / $context['sandbox']['field']['fields_to_delete_count']; - $context['message'] = \Drupal::translation()->translate('Deleting field @field_label', array('@field_label' => $fields[0]->label())); + $context['finished'] = $context['sandbox']['field']['current_progress'] / $context['sandbox']['field']['steps_to_delete']; + $context['message'] = \Drupal::translation()->translate('Purging field @field_label', array('@field_label' => $fields[0]->label())); + } + } + + /** + * Calculates the number of steps necessary to purge all the field data. + * + * @param array $context + * The batch context. + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The config importer. + */ + protected static function calculateNumberOfBatchSteps(&$context, ConfigImporter $config_importer) { + // @todo should we use the staged batch size or the currently active? + $context['sandbox']['field']['purge_batch_size'] = \Drupal::config('field.settings')->get('purge_batch_size'); + + $fields = static::getFieldsToPurge($config_importer->getStorageComparer(), $config_importer->getUnprocessedConfiguration('delete')); + // There will be one step to delete the instances and field. + $context['sandbox']['field']['steps_to_delete'] = count($fields); + foreach ($fields as $field) { + $row_count = $field->numberOfRows(); + if ($row_count > 0) { + // The number of steps to delete each field is determined by the + // purge_batch_size setting. For example if the field has 9 rows and the + // batch size is 10 then this will add 1 step to $number_of_steps. + $how_many_steps = (int) ($row_count / $context['sandbox']['field']['purge_batch_size']) + 1; + $context['sandbox']['field']['steps_to_delete'] += $how_many_steps; + } } + $context['sandbox']['field']['current_progress'] = 0; } /** @@ -62,17 +88,21 @@ * fields exist whose field types are provided by modules that are being * uninstalled their data need to be purged too. * - * @param ConfigImporter $config_importer - * The configuration importer. + * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer + * The configuration storage comparer. + * @param array $deletes + * (Optional) The configuration to delete. Used to provide a current * * @return \Drupal\field\Entity\FieldConfig[] * An array of fields that need purging before configuration can be * synchronised. */ - public static function getFieldsToPurge(ConfigImporter $config_importer) { + public static function getFieldsToPurge(StorageComparerInterface $storage_comparer, array $deletes = NULL) { $fields_to_delete = array(); - $deletes = $config_importer->getUnprocessedConfiguration('delete'); - $staged_extensions = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension'); + if (!$deletes) { + $deletes = $storage_comparer->getChangelist('delete'); + } + $staged_extensions = $storage_comparer->getSourceStorage()->read('core.extension'); // Gather fields that will be deleted during configuration synchronization // where the module that provides the field type is also being uninstalled. @@ -96,5 +126,4 @@ } return $fields_to_delete; - } } diff -u b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php --- b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteUninstallTest.php @@ -7,8 +7,6 @@ namespace Drupal\field\Tests; -use Drupal\Core\Config\ConfigImporterException; - /** * Tests config sync of deleting fields and instances and uninstalling modules. * @@ -115,17 +113,19 @@ 'bundle' => 'entity_test', ))->save(); - $entity = entity_create('entity_test'); - $value = '+0123456789'; - $entity->field_test = $value; - $entity->name->value = $this->randomName(); - $entity->save(); - - // Verify entity has been created properly. - $id = $entity->id(); - $entity = entity_load('entity_test', $id); - $this->assertEqual($entity->field_test->value, $value); - $this->assertEqual($entity->field_test[0]->value, $value); + // Create 12 entities to ensure that the purging works as expected. + for ($i=0; $i < 12; $i++) { + $entity = entity_create('entity_test'); + $value = '+0123456789'; + $entity->field_test = $value; + $entity->name->value = $this->randomName(); + $entity->save(); + + // Verify entity has been created properly. + $id = $entity->id(); + $entity = entity_load('entity_test', $id); + $this->assertEqual($entity->field_test->value, $value); + } // Delete the field. $field->delete(); only in patch2: unchanged: --- a/core/modules/field/lib/Drupal/field/Entity/FieldConfig.php +++ b/core/modules/field/lib/Drupal/field/Entity/FieldConfig.php @@ -660,32 +660,57 @@ public static function getReservedColumns() { * TRUE if the field has data for any entity; FALSE otherwise. */ public function hasData() { - if ($this->getBundles()) { + return (bool) $this->numberOfRows(TRUE); + } + + /** + * Determines the number of rows of data this field has. + * + * @param bool $has_data + * (Optional) Optimises query for hasData(). + * + * @return int + * The number of rows of data for this field. If $has_data parameter is TRUE + * then the value will either be 0 or 1. + */ + public function numberOfRows($has_data = FALSE) { + $factory = \Drupal::service('entity.query'); + $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type); + // Entity Query throws an exception if there is no base table. + if (!$entity_type->getBaseTable()) { + return 0; + } + + if ($this->deleted) { + $query = $factory->get($this->entity_type) + ->condition('id:' . $this->uuid() . '.deleted', 1) + ->count() + ->accessCheck(FALSE); + } + elseif ($this->getBundles()) { $storage_details = $this->getSchema(); $columns = array_keys($storage_details['columns']); - $factory = \Drupal::service('entity.query'); - // Entity Query throws an exception if there is no base table. - $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type); - if (!$entity_type->getBaseTable()) { - return FALSE; - } + $query = $factory->get($this->entity_type); $group = $query->orConditionGroup(); foreach ($columns as $column) { $group->exists($this->name . '.' . $column); } - $result = $query + $query = $query ->condition($group) ->count() - ->accessCheck(FALSE) - ->range(0, 1) - ->execute(); - if ($result) { - return TRUE; - } + ->accessCheck(FALSE); } - return FALSE; + if ($query) { + // If we are performing the query just to check if the field has data + // limit the number of rows returned by the subquery. + if ($has_data) { + $query->range(0, 1); + } + return (int) $query->execute(); + } + return 0; } /**