diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index 7b3a74b..c7796c4 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -413,12 +413,12 @@ protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array if ($type == 'config' || $type == 'content') { $affected_dependencies[$type] = array_map(function ($name) use ($type) { if ($type == 'config') { - $entity_type_id = $this->getEntityTypeIdByName($name); + return $this->loadConfigEntityByName($name); } else { list($entity_type_id) = explode(':', $name); + return $this->entityManager->loadEntityByConfigTarget($entity_type_id, $name); } - return $this->entityManager->loadEntityByConfigTarget($entity_type_id, $name); }, $affected_dependencies[$type]); } } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigDependencyDeleteFormTrait.php b/core/lib/Drupal/Core/Config/Entity/ConfigDependencyDeleteFormTrait.php new file mode 100644 index 0000000..ada9ac3 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/ConfigDependencyDeleteFormTrait.php @@ -0,0 +1,130 @@ +getConfigEntitiesToChangeOnDependencyRemoval($type, $names); + $entity_types = array(); + + $form['entity_updates'] = array( + '#type' => 'details', + '#title' => $this->t('Configuration updates'), + '#description' => $this->t('The listed configuration will be updated.'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#access' => FALSE, + ); + + foreach ($dependent_entities['update'] as $entity) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + $entity_type_id = $entity->getEntityTypeId(); + if (!isset($form['entity_updates'][$entity_type_id])) { + $entity_type = $entity_manager->getDefinition($entity_type_id); + // Store the ID and label to sort the entity types and entities later. + $label = $entity_type->getLabel(); + $entity_types[$entity_type_id] = $label; + $form['entity_updates'][$entity_type_id] = array( + '#theme' => 'item_list', + '#title' => $label, + '#items' => array(), + ); + } + $form['entity_updates'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id(); + } + if (!empty($dependent_entities['update'])) { + $form['entity_updates']['#access'] = TRUE; + + // Add a weight key to the entity type sections. + asort($entity_types, SORT_FLAG_CASE); + $weight = 0; + foreach ($entity_types as $entity_type_id => $label) { + $form['entity_updates'][$entity_type_id]['#weight'] = $weight; + // Sort the list of entity labels alphabetically. + sort($form['entity_updates'][$entity_type_id]['#items'], SORT_FLAG_CASE); + $weight++; + } + } + + $form['entity_deletes'] = array( + '#type' => 'details', + '#title' => $this->t('Configuration deletions'), + '#description' => $this->t('The listed configuration will be deleted.'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#access' => FALSE, + ); + + foreach ($dependent_entities['delete'] as $entity) { + $entity_type_id = $entity->getEntityTypeId(); + if (!isset($form['entity_deletes'][$entity_type_id])) { + $entity_type = $entity_manager->getDefinition($entity_type_id); + // Store the ID and label to sort the entity types and entities later. + $label = $entity_type->getLabel(); + $entity_types[$entity_type_id] = $label; + $form['entity_deletes'][$entity_type_id] = array( + '#theme' => 'item_list', + '#title' => $label, + '#items' => array(), + ); + } + $form['entity_deletes'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id(); + } + if (!empty($dependent_entities['delete'])) { + $form['entity_deletes']['#access'] = TRUE; + + // Add a weight key to the entity type sections. + asort($entity_types, SORT_FLAG_CASE); + $weight = 0; + foreach ($entity_types as $entity_type_id => $label) { + $form['entity_deletes'][$entity_type_id]['#weight'] = $weight; + // Sort the list of entity labels alphabetically. + sort($form['entity_deletes'][$entity_type_id]['#items'], SORT_FLAG_CASE); + $weight++; + } + } + + } +} diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 7d684e0..d4bbbaa 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Cache\Cache; +use Drupal\Core\Config\ConfigException; use Drupal\Core\Config\Schema\SchemaIncompleteException; use Drupal\Core\Entity\Entity; use Drupal\Core\Config\ConfigDuplicateUUIDException; @@ -233,7 +234,8 @@ public function createDuplicate() { } /** - * Helper callback for uasort() to sort configuration entities by weight and label. + * Helper callback for uasort() to sort configuration entities by weight and + * label. */ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) { $a_weight = isset($a->weight) ? $a->weight : 0; @@ -517,4 +519,36 @@ public function getThirdPartyProviders() { return array_keys($this->third_party_settings); } + /** + * {@inheritdoc} + */ + public static function preDelete(EntityStorageInterface $storage, array $entities) { + foreach ($entities as $entity) { + if ($entity->isUninstalling() || $entity->isSyncing()) { + // During extension uninstall and configuration synchronization + // deletions are already managed. + break; + } + // Fix or remove any dependencies. + $config_entities = static::getConfigManager()->getConfigEntitiesToChangeOnDependencyRemoval('config', [$entity->getConfigDependencyName()], FALSE); + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent_entity */ + foreach ($config_entities['update'] as $dependent_entity) { + $dependent_entity->save(); + } + foreach ($config_entities['delete'] as $dependent_entity) { + $dependent_entity->delete(); + } + } + } + + /** + * Gets the configuration manager. + * + * @return \Drupal\Core\Config\ConfigManager + * The configuration manager. + */ + protected static function getConfigManager() { + return \Drupal::service('config.manager'); + } + } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php index 1db3b42..86095c7 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php @@ -165,10 +165,6 @@ public function calculateDependencies(); * caller for the changes to take effect. Implementations should not save the * entity. * - * @todo https://www.drupal.org/node/2336727 this method is only fired during - * extension uninstallation but it could be used during config entity - * deletion too. - * * @param array $dependencies * An array of dependencies that will be deleted keyed by dependency type. * Dependency types are, for example, entity, module and theme. @@ -176,7 +172,11 @@ public function calculateDependencies(); * @return bool * TRUE if the entity has changed, FALSE if not. * + * @return bool + * TRUE if the entity has been changed as a result, FALSE if not. + * * @see \Drupal\Core\Config\Entity\ConfigDependencyManager + * @see \Drupal\Core\Config\ConfigEntityBase::preDelete() * @see \Drupal\Core\Config\ConfigManager::uninstall() * @see \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval() */ diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteForm.php b/core/lib/Drupal/Core/Entity/EntityDeleteForm.php index 71560a2..7869af8 100644 --- a/core/lib/Drupal/Core/Entity/EntityDeleteForm.php +++ b/core/lib/Drupal/Core/Entity/EntityDeleteForm.php @@ -7,13 +7,45 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Form\FormStateInterface; + /** * Provides a generic base class for an entity deletion form. * * @ingroup entity_api */ class EntityDeleteForm extends EntityConfirmFormBase { - use EntityDeleteFormTrait; + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $entity = $this->getEntity(); + // Only do dependency processing for configuration entities. Whilst it is + // possible for a configuration entity to be dependent on a content entity, + // these dependencies are soft and content delete permissions are often + // given to more users. This method should not make assumptions that $entity + // is a configuration entity in case we decide to remove the following + // condition. + if (!($entity instanceof ConfigEntityInterface)) { + return $form; + } + $this->addDependencyListsToForm($form, $entity->getConfigDependencyKey(), [$entity->getConfigDependencyName()], $this->getConfigManager(), $this->entityManager); + + return $form; + } + + /** + * Gets the configuration manager. + * + * @return \Drupal\Core\Config\ConfigManager + * The configuration manager. + */ + protected function getConfigManager() { + return \Drupal::service('config.manager'); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php index 80bb50d..7e4ce04 100644 --- a/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php +++ b/core/lib/Drupal/Core/Entity/EntityDeleteFormTrait.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Config\Entity\ConfigDependencyDeleteFormTrait; use Drupal\Core\Form\FormStateInterface; /** @@ -18,13 +19,7 @@ * @ingroup entity_api */ trait EntityDeleteFormTrait { - - /** - * Translates a string to the current language or to a given language. - * - * Provided by \Drupal\Core\StringTranslation\StringTranslationTrait. - */ - abstract protected function t($string, array $args = array(), array $options = array()); + use ConfigDependencyDeleteFormTrait; /** * Returns the entity of this form. diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php index 7388b4b..026570f 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigBase.php +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php @@ -264,6 +264,20 @@ public function calculateDependencies() { /** * {@inheritdoc} */ + public function onDependencyRemoval(array $dependencies) { + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $definition = $field_type_manager->getDefinition($this->getType()); + $changed = $definition['class']::onDependencyRemoval($this, $dependencies); + if ($changed) { + $this->save(); + } + return $changed; + } + + + /** + * {@inheritdoc} + */ public function postCreate(EntityStorageInterface $storage) { parent::postCreate($storage); // If it was not present in the $values passed to create(), (e.g. for diff --git a/core/lib/Drupal/Core/Field/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php index b411b88..b2119e3 100644 --- a/core/lib/Drupal/Core/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php @@ -273,4 +273,11 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def return array(); } + /** + * {@inheritdoc} + */ + public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { + return FALSE; + } + } diff --git a/core/lib/Drupal/Core/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Field/FieldItemInterface.php index 1d0de29..f74b3f7 100644 --- a/core/lib/Drupal/Core/Field/FieldItemInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemInterface.php @@ -405,4 +405,20 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state); */ public static function calculateDependencies(FieldDefinitionInterface $field_definition); + /** + * Informs the plugin that a dependency of the field will be deleted. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition. + * @param array $dependencies + * An array of dependencies that will be deleted keyed by dependency type. + * Dependency types are, for example, entity, module and theme. + * + * @return bool + * TRUE if the field definition has been changed as a result, FALSE if not. + * + * @see \Drupal\Core\Config\ConfigEntityInterface::onDependencyRemoval() + */ + public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies); + } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index e03750d..10497ad 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -286,4 +286,26 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def return $dependencies; } + /** + * {@inheritdoc} + */ + public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { + $changed = FALSE; + if (is_array($field_definition->default_value) && count($field_definition->default_value)) { + $target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type')); + foreach ($field_definition->default_value as $default_value) { + if (is_array($default_value) && isset($default_value['target_uuid'])) { + $entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $default_value['target_uuid']); + // If the entity does not exist do not create the dependency. + // @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue() + if ($entity && in_array($entity, $dependencies[$target_entity_type->getConfigDependencyKey()])) { + $field_definition->default_value = []; + $changed = TRUE; + } + } + } + } + return $changed; + } + } diff --git a/core/modules/config/src/Tests/ConfigDependencyTest.php b/core/modules/config/src/Tests/ConfigDependencyTest.php index 047a20d..c78eaac 100644 --- a/core/modules/config/src/Tests/ConfigDependencyTest.php +++ b/core/modules/config/src/Tests/ConfigDependencyTest.php @@ -273,6 +273,106 @@ public function testConfigEntityUninstall() { } /** + * Tests deleting a configuration entity and dependency management. + */ + public function testConfigEntityDelete() { + /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ + $config_manager = \Drupal::service('config.manager'); + /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */ + $storage = $this->container->get('entity.manager')->getStorage('config_test'); + // Test dependencies between configuration entities. + $entity1 = $storage->create( + array( + 'id' => 'entity1' + ) + ); + $entity1->save(); + $entity2 = $storage->create( + array( + 'id' => 'entity2', + 'dependencies' => array( + 'enforced' => array( + 'config' => array($entity1->getConfigDependencyName()), + ), + ), + ) + ); + $entity2->save(); + + // Do a dry run using + // \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval(). + $config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('config', [$entity1->getConfigDependencyName()]); + $this->assertEqual($entity2->uuid(), reset($config_entities['delete'])->uuid(), 'Entity 2 will be deleted.'); + $this->assertTrue(empty($config_entities['update']), 'No dependent configuration entities will be updated.'); + $this->assertTrue(empty($config_entities['unchanged']), 'No dependent configuration entities will be unchanged.'); + + // Test that doing a delete of entity1 deletes entity2 since it is dependent + // on entity1. + $entity1->delete(); + $this->assertFalse($storage->load('entity1'), 'Entity 1 deleted'); + $this->assertFalse($storage->load('entity2'), 'Entity 2 deleted'); + + // Set a more complicated test where dependencies will be fixed. + \Drupal::state()->set('config_test.fix_dependencies', array($entity1->getConfigDependencyName())); + + // Entity1 will be deleted by the test. + $entity1 = $storage->create( + array( + 'id' => 'entity1', + ) + ); + $entity1->save(); + + // Entity2 has a dependency on Entity1 but it can be fixed because + // \Drupal\config_test\Entity::onDependencyRemoval() will remove the + // dependency before config entities are deleted. + $entity2 = $storage->create( + array( + 'id' => 'entity2', + 'dependencies' => array( + 'enforced' => array( + 'config' => array($entity1->getConfigDependencyName()), + ), + ), + ) + ); + $entity2->save(); + + // Entity3 will be unchanged because it is dependent on Entity2 which can + // be fixed. + $entity3 = $storage->create( + array( + 'id' => 'entity3', + 'dependencies' => array( + 'enforced' => array( + 'config' => array($entity2->getConfigDependencyName()), + ), + ), + ) + ); + $entity3->save(); + + // Do a dry run using + // \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval(). + $config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('config', [$entity1->getConfigDependencyName()]); + $this->assertTrue(empty($config_entities['delete']), 'No dependent configuration entities will be deleted.'); + $this->assertEqual($entity2->uuid(), reset($config_entities['update'])->uuid(), 'Entity 2 will be updated.'); + $this->assertEqual($entity3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.'); + + // Perform the uninstall. + $entity1->delete(); + + // Test that expected actions have been performed. + $this->assertFalse($storage->load('entity1'), 'Entity 1 deleted'); + $entity2 = $storage->load('entity2'); + $this->assertTrue($entity2, 'Entity 2 not deleted'); + $this->assertEqual($entity2->calculateDependencies()['config'], array(), 'Entity 2 dependencies updated to remove dependency on Entity1.'); + $entity3 = $storage->load('entity3'); + $this->assertTrue($entity3, 'Entity 3 not deleted'); + $this->assertEqual($entity3->calculateDependencies()['config'], [$entity2->getConfigDependencyName()], 'Entity 3 still depends on Entity 2.'); + } + + /** * Tests getConfigEntitiesToChangeOnDependencyRemoval() with content entities. * * At the moment there is no runtime code that calculates configuration diff --git a/core/modules/config/src/Tests/ConfigDependencyWebTest.php b/core/modules/config/src/Tests/ConfigDependencyWebTest.php new file mode 100644 index 0000000..02a9170 --- /dev/null +++ b/core/modules/config/src/Tests/ConfigDependencyWebTest.php @@ -0,0 +1,133 @@ +container->get('entity.manager')->getStorage('config_test'); + // Entity1 will be deleted by the test. + $entity1 = $storage->create( + array( + 'id' => 'entity1', + 'label' => 'Entity One', + ) + ); + $entity1->save(); + + // Entity2 has a dependency on Entity1 but it can be fixed because + // \Drupal\config_test\Entity::onDependencyRemoval() will remove the + // dependency before config entities are deleted. + $entity2 = $storage->create( + array( + 'id' => 'entity2', + 'dependencies' => array( + 'enforced' => array( + 'config' => array($entity1->getConfigDependencyName()), + ), + ), + ) + ); + $entity2->save(); + + $this->drupalGet($entity2->urlInfo('delete-form')); + $this->assertNoText(t('Configuration updates'), 'No configuration updates found.'); + $this->assertNoText(t('Configuration deletions'), 'No configuration deletes found.'); + $this->drupalGet($entity1->urlInfo('delete-form')); + $this->assertNoText(t('Configuration updates'), 'No configuration updates found.'); + $this->assertText(t('Configuration deletions'), 'Configuration deletions found.'); + $this->assertText($entity2->id(), 'Entity2 id found'); + $this->drupalPostForm($entity1->urlInfo('delete-form'), array(), 'Delete'); + $storage->resetCache(); + $this->assertFalse($storage->loadMultiple([$entity1->id(), $entity2->id()]), 'Test entities deleted'); + + // Set a more complicated test where dependencies will be fixed. + // Entity1 will be deleted by the test. + $entity1 = $storage->create( + array( + 'id' => 'entity1', + ) + ); + $entity1->save(); + \Drupal::state()->set('config_test.fix_dependencies', array($entity1->getConfigDependencyName())); + + // Entity2 has a dependency on Entity1 but it can be fixed because + // \Drupal\config_test\Entity::onDependencyRemoval() will remove the + // dependency before config entities are deleted. + $entity2 = $storage->create( + array( + 'id' => 'entity2', + 'label' => 'Entity Two', + 'dependencies' => array( + 'enforced' => array( + 'config' => array($entity1->getConfigDependencyName()), + ), + ), + ) + ); + $entity2->save(); + + // Entity3 will be unchanged because it is dependent on Entity2 which can + // be fixed. + $entity3 = $storage->create( + array( + 'id' => 'entity3', + 'dependencies' => array( + 'enforced' => array( + 'config' => array($entity2->getConfigDependencyName()), + ), + ), + ) + ); + $entity3->save(); + + $this->drupalGet($entity1->urlInfo('delete-form')); + $this->assertText(t('Configuration updates'), 'Configuration updates found.'); + $this->assertNoText(t('Configuration deletions'), 'No configuration deletions found.'); + $this->assertNoText($entity2->id(), 'Entity2 id not found'); + $this->assertText($entity2->label(), 'Entity2 label not found'); + $this->assertNoText($entity3->id(), 'Entity3 id not found'); + $this->drupalPostForm($entity1->urlInfo('delete-form'), array(), 'Delete'); + $storage->resetCache(); + $this->assertFalse($storage->load('entity1'), 'Test entity 1 deleted'); + $entity2 = $storage->load('entity2'); + $this->assertTrue($entity2, 'Entity 2 not deleted'); + $this->assertEqual($entity2->calculateDependencies()['config'], array(), 'Entity 2 dependencies updated to remove dependency on Entity1.'); + $entity3 = $storage->load('entity3'); + $this->assertTrue($entity3, 'Entity 3 not deleted'); + $this->assertEqual($entity3->calculateDependencies()['config'], [$entity2->getConfigDependencyName()], 'Entity 3 still depends on Entity 2.'); + + } + +} diff --git a/core/modules/config/src/Tests/ConfigImporterTest.php b/core/modules/config/src/Tests/ConfigImporterTest.php index d2c7cf0..bfef3a8 100644 --- a/core/modules/config/src/Tests/ConfigImporterTest.php +++ b/core/modules/config/src/Tests/ConfigImporterTest.php @@ -381,6 +381,10 @@ function testSecondaryUpdateDeletedDeleterFirst() { /** * Tests that secondary updates for deleted files work as expected. + * + * This test is completely hypothetical since we only support full + * configuration tree imports. Therefore, any configuration updates that cause + * secondary deletes should be reflected already in the staged configuration. */ function testSecondaryUpdateDeletedDeleteeFirst() { $name_deleter = 'config_test.dynamic.deleter'; @@ -416,10 +420,10 @@ function testSecondaryUpdateDeletedDeleteeFirst() { $this->configImporter->reset()->import(); $entity_storage = \Drupal::entityManager()->getStorage('config_test'); - $deleter = $entity_storage->load('deleter'); - $this->assertEqual($deleter->id(), 'deleter'); - $this->assertEqual($deleter->uuid(), $values_deleter['uuid']); - $this->assertEqual($deleter->label(), $values_deleter['label']); + // Both entities are deleted. ConfigTest::postSave() causes updates of the + // deleter entity to delete the deletee entity. Since the deleter depends on + // the deletee, removing the deletee causes the deleter to be removed. + $this->assertFalse($entity_storage->load('deleter')); // @todo The deletee entity does not exist as the update worked but the // entity was deleted after that. There is also no log message as this // happened outside of the config importer. diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php index cabaf22..2203fd8 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceIntegrationTest.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\config\Tests\AssertConfigEntityImportTrait; +use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\field\Entity\FieldConfig; use Drupal\simpletest\WebTestBase; @@ -152,6 +153,9 @@ public function testSupportedEntityTypesAndWidgets() { // Ensure that the field can be imported without change even after the // default value deleted. $referenced_entities[0]->delete(); + // Reload the field since deleting the default value can change the field. + \Drupal::entityManager()->getStorage($field->getEntityTypeId())->resetCache([$field->id()]); + $field = FieldConfig::loadByName($this->entityType, $this->bundle, $this->fieldName); $this->assertConfigEntityImport($field); // Once the default value has been removed after saving the dependency diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index c5b9ad5..802bc1b 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -182,6 +182,7 @@ public function calculateDependencies() { public static function preDelete(EntityStorageInterface $storage, array $fields) { $state = \Drupal::state(); + parent::preDelete($storage, $fields); // Keep the field definitions in the state storage so we can use them // later during field_purge_batch(). $deleted_fields = $state->get('field.field.deleted') ?: array(); @@ -222,7 +223,7 @@ public static function postDelete(EntityStorageInterface $storage, array $fields $storages_to_delete = array(); foreach ($fields as $field) { $storage_definition = $field->getFieldStorageDefinition(); - if (!$field->deleted && empty($field->noFieldDelete) && !$field->isUninstalling() && $storage_definition->isDeletable()) { + if (!$field->deleted && !$field->isUninstalling() && $storage_definition->isDeletable()) { // Key by field UUID to avoid deleting the same storage twice. $storages_to_delete[$storage_definition->uuid()] = $storage_definition; } @@ -230,29 +231,6 @@ public static function postDelete(EntityStorageInterface $storage, array $fields if ($storages_to_delete) { \Drupal::entityManager()->getStorage('field_storage_config')->delete($storages_to_delete); } - - // Cleanup entity displays. - $displays_to_update = array(); - foreach ($fields as $field) { - if (!$field->deleted) { - $view_modes = \Drupal::entityManager()->getViewModeOptions($field->entity_type, TRUE); - foreach (array_keys($view_modes) as $mode) { - $displays_to_update['entity_view_display'][$field->entity_type . '.' . $field->bundle . '.' . $mode][] = $field->getName(); - } - $form_modes = \Drupal::entityManager()->getFormModeOptions($field->entity_type, TRUE); - foreach (array_keys($form_modes) as $mode) { - $displays_to_update['entity_form_display'][$field->entity_type . '.' . $field->bundle . '.' . $mode][] = $field->getName(); - } - } - } - foreach ($displays_to_update as $type => $ids) { - foreach (entity_load_multiple($type, array_keys($ids)) as $id => $display) { - foreach ($ids[$id] as $field_name) { - $display->removeComponent($field_name); - } - $display->save(); - } - } } /** diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index ab869db..a1c2d58 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -198,6 +198,13 @@ class FieldStorageConfig extends ConfigEntityBase implements FieldStorageConfigI protected $propertyDefinitions; /** + * Static flag set to prevent recursion during field deletes. + * + * @var bool + */ + protected static $inDeletion = FALSE; + + /** * Constructs a FieldStorageConfig object. * * @param array $values @@ -374,29 +381,13 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { */ public static function preDelete(EntityStorageInterface $storage, array $field_storages) { $state = \Drupal::state(); - $field_config_storage = \Drupal::entityManager()->getStorage('field_config'); - // Delete fields first. Note: when deleting a field storage through - // FieldConfig::postDelete(), the fields have been deleted already, so - // no fields will be found here. - $field_ids = array(); - foreach ($field_storages as $field_storage) { - if (!$field_storage->deleted) { - foreach ($field_storage->getBundles() as $bundle) { - $entity_type = $field_storage->getTargetEntityTypeId(); - $field_name = $field_storage->getName(); - $field_ids[] = "{$entity_type}.$bundle.{$field_name}"; - } - } - } - if ($field_ids) { - $fields = $field_config_storage->loadMultiple($field_ids); - // Tag the objects to preserve recursive deletion of the field. - foreach ($fields as $field) { - $field->noFieldDelete = TRUE; - } - $field_config_storage->delete($fields); - } + // Set the static flag so that we don't delete field storages whilst + // deleting fields. + static::$inDeletion = TRUE; + + // Delete or fix any configuration that is dependent, for example, fields. + parent::preDelete($storage, $field_storages); // Keep the field definitions in the state storage so we can use them later // during field_purge_batch(). @@ -424,6 +415,8 @@ public static function postDelete(EntityStorageInterface $storage, array $fields $field->deleted = TRUE; } } + // Unset static flag. + static::$inDeletion = FALSE; } /** @@ -794,8 +787,9 @@ public static function loadByName($entity_type_id, $field_name) { */ public function isDeletable() { // The field storage is not deleted, is configured to be removed when there - // are no fields and the field storage has no bundles. - return !$this->deleted && !$this->persist_with_no_fields && count($this->getBundles()) == 0; + // are no fields, the field storage has no bundles, and field storages are + // not in the process of being deleted. + return !$this->deleted && !$this->persist_with_no_fields && count($this->getBundles()) == 0 && !static::$inDeletion; } /** diff --git a/core/modules/system/src/Form/ModulesUninstallConfirmForm.php b/core/modules/system/src/Form/ModulesUninstallConfirmForm.php index d6e5b15..159959d 100644 --- a/core/modules/system/src/Form/ModulesUninstallConfirmForm.php +++ b/core/modules/system/src/Form/ModulesUninstallConfirmForm.php @@ -8,6 +8,7 @@ namespace Drupal\system\Form; use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\Entity\ConfigDependencyDeleteFormTrait; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Form\ConfirmFormBase; @@ -21,6 +22,7 @@ * Builds a confirmation form to uninstall selected modules. */ class ModulesUninstallConfirmForm extends ConfirmFormBase { + use ConfigDependencyDeleteFormTrait; /** * The module installer service. @@ -145,86 +147,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { }, $this->modules), ); - // Get the dependent entities. - $entity_types = array(); - $dependent_entities = $this->configManager->getConfigEntitiesToChangeOnDependencyRemoval('module', $this->modules); - - $form['entity_updates'] = array( - '#type' => 'details', - '#title' => $this->t('Configuration updates'), - '#description' => $this->t('The listed configuration will be updated.'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#access' => FALSE, - ); - - foreach ($dependent_entities['update'] as $entity) { - /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ - $entity_type_id = $entity->getEntityTypeId(); - if (!isset($form['entity_updates'][$entity_type_id])) { - $entity_type = $this->entityManager->getDefinition($entity_type_id); - // Store the ID and label to sort the entity types and entities later. - $label = $entity_type->getLabel(); - $entity_types[$entity_type_id] = $label; - $form['entity_updates'][$entity_type_id] = array( - '#theme' => 'item_list', - '#title' => $label, - '#items' => array(), - ); - } - $form['entity_updates'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id(); - } - if (!empty($dependent_entities['update'])) { - $form['entity_updates']['#access'] = TRUE; - - // Add a weight key to the entity type sections. - asort($entity_types, SORT_FLAG_CASE); - $weight = 0; - foreach ($entity_types as $entity_type_id => $label) { - $form['entity_updates'][$entity_type_id]['#weight'] = $weight; - // Sort the list of entity labels alphabetically. - sort($form['entity_updates'][$entity_type_id]['#items'], SORT_FLAG_CASE); - $weight++; - } - } - - $form['entity_deletes'] = array( - '#type' => 'details', - '#title' => $this->t('Configuration deletions'), - '#description' => $this->t('The listed configuration will be deleted.'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#access' => FALSE, - ); - - foreach ($dependent_entities['delete'] as $entity) { - $entity_type_id = $entity->getEntityTypeId(); - if (!isset($form['entity_deletes'][$entity_type_id])) { - $entity_type = $this->entityManager->getDefinition($entity_type_id); - // Store the ID and label to sort the entity types and entities later. - $label = $entity_type->getLabel(); - $entity_types[$entity_type_id] = $label; - $form['entity_deletes'][$entity_type_id] = array( - '#theme' => 'item_list', - '#title' => $label, - '#items' => array(), - ); - } - $form['entity_deletes'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id(); - } - if (!empty($dependent_entities['delete'])) { - $form['entity_deletes']['#access'] = TRUE; - - // Add a weight key to the entity type sections. - asort($entity_types, SORT_FLAG_CASE); - $weight = 0; - foreach ($entity_types as $entity_type_id => $label) { - $form['entity_deletes'][$entity_type_id]['#weight'] = $weight; - // Sort the list of entity labels alphabetically. - sort($form['entity_deletes'][$entity_type_id]['#items'], SORT_FLAG_CASE); - $weight++; - } - } + // List the dependent entities. + $this->addDependencyListsToForm($form, 'module', $this->modules ,$this->configManager, $this->entityManager); return parent::buildForm($form, $form_state); } diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php index 4ed1c20..9f05892 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php @@ -97,6 +97,13 @@ class ConfigEntityStorageTest extends UnitTestCase { protected $typedConfigManager; /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $configManager; + + /** * {@inheritdoc} * * @covers ::__construct @@ -160,10 +167,14 @@ protected function setUp() { $this->typedConfigManager->expects($this->any()) ->method('getDefinition') ->will($this->returnValue(array('mapping' => array('id' => '', 'uuid' => '', 'dependencies' => '')))); + + $this->configManager = $this->getMock('Drupal\Core\Config\ConfigManagerInterface'); + $container = new ContainerBuilder(); $container->set('entity.manager', $this->entityManager); $container->set('config.typed', $this->typedConfigManager); $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator); + $container->set('config.manager', $this->configManager); \Drupal::setContainer($container); } @@ -728,6 +739,11 @@ public function testDeleteRevision() { * @covers ::doDelete */ public function testDelete() { + // Dependencies are tested elsewhere. + // @see \Drupal\config\Tests\ConfigDependencyTest + $this->configManager->expects($this->any()) + ->method('getConfigEntitiesToChangeOnDependencyRemoval') + ->willReturn(['update' => [], 'delete' => [], 'unchanged' => []]); $entities = array(); $configs = array(); $config_map = array();