reverted: --- b/core/modules/language/language.module +++ a/core/modules/language/language.module @@ -640,7 +640,7 @@ * Implements hook_language_insert(). */ function language_language_insert($language) { + if (!empty($language->locked)) { - if (!empty($language->locked) || $language->isSyncing()) { return; } only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -252,9 +252,57 @@ public function validate() { * The name of the configuration to process. */ protected function process($op, $name) { - if (!$this->importInvokeOwner($op, $name)) { - $this->importConfig($op, $name); + if ($this->checkOp($op, $name)) { + if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { + $this->importConfigurationEntity($op, $name, $entity_type); + } + else { + $this->importConfig($op, $name); + } + } + } + + /** + * Checks that the operation is still valid. + * + * During a configuration import secondary writes and deletes are possible. + * This method checks that the operation is still valid before processing a + * configuration change. + * + * @param string $op + * The change operation. + * @param $name + * The name of the configuration to process. + * + * @return bool + * TRUE is to continue processing, FALSE otherwise. + */ + protected function checkOp($op, $name) { + $target_exists = $this->storageComparer->getTargetStorage()->exists($name); + switch ($op) { + case 'delete': + if (!$target_exists) { + // The configuration has already been deleted. For example, a field + // is automatically deleted if all the instances are. + $this->setProcessed($op, $name); + return FALSE; + } + break; + case 'create': + if ($target_exists) { + // Become an update. This is possible since updates are run after + // creates. + $this->storageComparer->swapOp('create', 'update', $name); + return FALSE; + } + break; + case 'update': + if (!$target_exists) { + // Error? + } + break; } + return TRUE; } /** @@ -291,37 +339,30 @@ protected function importConfig($op, $name) { * create or update. * @param string $name * The name of the configuration to process. + * @param string $entity_type + * The entity type of the configuration to process. * * @return bool * TRUE if the configuration was imported as a configuration entity. FALSE * otherwise. */ - protected function importInvokeOwner($op, $name) { - // Call to the configuration entity's storage controller to handle the - // configuration change. - $handled_by_module = FALSE; - // Validate the configuration object name before importing it. - // Config::validateName($name); - if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { - $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); - if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) { - $old_config->initWithData($old_data); - } - - $data = $this->storageComparer->getSourceStorage()->read($name); - $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); - if ($data !== FALSE) { - $new_config->setData($data); - } - - $method = 'import' . ucfirst($op); - $handled_by_module = $this->configManager->getEntityManager()->getStorageController($entity_type)->$method($name, $new_config, $old_config); + protected function importConfigurationEntity($op, $name, $entity_type) { + $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) { + $old_config->initWithData($old_data); } - if (!empty($handled_by_module)) { - $this->setProcessed($op, $name); - return TRUE; + + $data = $this->storageComparer->getSourceStorage()->read($name); + $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($data !== FALSE) { + $new_config->setData($data); } - return FALSE; + + $method = 'import' . ucfirst($op); + // Call the configuration entity's storage controller to handle the + // configuration change. + $this->configManager->getEntityManager()->getStorageController($entity_type)->$method($name, $new_config, $old_config); + $this->setProcessed($op, $name); } /** only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -204,4 +204,12 @@ public function validateSiteUuid() { return $source['uuid'] === $target['uuid']; } + /** + * {@inheritdoc} + */ + public function swapOp($from, $to, $name) { + $key = array_search($name, $this->changelist[$from]); + unset($this->changelist[$from][$key]); + $this->addChangeList($to, array($name)); + } } only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/StorageComparerInterface.php +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -125,4 +125,18 @@ public function hasChanges($ops = array('delete', 'create', 'update')); */ public function validateSiteUuid(); + /** + * Moves a configuration name from one change list to another. + * + * @param string $name + * The configuration object name. + * @param string $from + * The current change operation. Either delete, create or update. + * @param string $to + * The change operation to change to. Either delete, create or update. + * + * @return $this + */ + public function swapOp($name, $from, $to); + } only in patch2: unchanged: --- a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php @@ -6,6 +6,7 @@ */ namespace Drupal\field\Tests; +use Drupal\Component\Utility\String; /** * Tests deleting fields and instances as part of config import. @@ -31,13 +32,21 @@ public static function getInfo() { * Tests deleting fields and instances as part of config import. */ public function testImportDelete() { + // At this point there are 5 field configuration objects in the active + // storage. + // - field.field.entity_test.field_test_import + // - field.field.entity_test.field_test_import_2 + // - field.instance.entity_test.entity_test.field_test_import + // - field.instance.entity_test.entity_test.field_test_import_2 + // - field.instance.entity_test.test_bundle.field_test_import_2 + $field_name = 'field_test_import'; $field_id = "entity_test.$field_name"; $field_name_2 = 'field_test_import_2'; $field_id_2 = "entity_test.$field_name_2"; - $instance_id = "entity_test.test_bundle.$field_name"; - $instance_id_2a = "entity_test.test_bundle.$field_name_2"; - $instance_id_2b = "entity_test.test_bundle_2.$field_name_2"; + $instance_id = "entity_test.entity_test.$field_name"; + $instance_id_2a = "entity_test.entity_test.$field_name_2"; + $instance_id_2b = "entity_test.test_bundle.$field_name_2"; $field_config_name = "field.field.$field_id"; $field_config_name_2 = "field.field.$field_id_2"; $instance_config_name = "field.instance.$instance_id"; @@ -45,7 +54,7 @@ public function testImportDelete() { $instance_config_name_2b = "field.instance.$instance_id_2b"; // Create a second bundle for the 'Entity test' entity type. - entity_test_create_bundle('test_bundle_2'); + entity_test_create_bundle('test_bundle'); // Import default config. $this->installConfig(array('field_test_config')); @@ -57,11 +66,14 @@ public function testImportDelete() { $active = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); $this->copyConfig($active, $staging); - $staging->delete($field_config_name); - $staging->delete($field_config_name_2); - $staging->delete($instance_config_name); - $staging->delete($instance_config_name_2a); - $staging->delete($instance_config_name_2b); + $this->assertTrue($staging->delete($field_config_name), String::format('Deleted field: !field', array('!field' => $field_config_name))); + $this->assertTrue($staging->delete($field_config_name_2), String::format('Deleted field: !field', array('!field' => $field_config_name_2))); + $this->assertTrue($staging->delete($instance_config_name), String::format('Deleted field instance: !field_instance', array('!field_instance' => $instance_config_name))); + $this->assertTrue($staging->delete($instance_config_name_2a), String::format('Deleted field instance: !field_instance', array('!field_instance' => $instance_config_name_2a))); + $this->assertTrue($staging->delete($instance_config_name_2b), String::format('Deleted field instance: !field_instance', array('!field_instance' => $instance_config_name_2b))); + + $deletes = $this->configImporter()->getUnprocessed('delete'); + $this->assertEqual(count($deletes), 5, 'Importing configuration will delete 3 field instances and 2 fields.'); // Import the content of the staging directory. $this->configImporter()->import();