diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 9ddbfb0..b0da124 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -11,6 +11,7 @@ use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Component\Utility\String; use Drupal\Core\Config\Entity\ImportableEntityStorageInterface; +use Drupal\Core\Config\ConfigEvents; use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Lock\LockBackendInterface; @@ -245,12 +246,12 @@ protected function getEmptyExtensionsProcessedList() { * * @param array $ops * The operations to check for changes. Defaults to all operations, i.e. - * array('delete', 'create', 'update'). + * array('delete', 'create', 'update', 'rename'). * * @return bool * TRUE if there are changes to process and FALSE if not. */ - public function hasUnprocessedConfigurationChanges($ops = array('delete', 'create', 'update')) { + public function hasUnprocessedConfigurationChanges($ops = array('delete', 'create', 'rename', 'update')) { foreach ($ops as $op) { if (count($this->getUnprocessedConfiguration($op))) { return TRUE; @@ -451,7 +452,9 @@ public function import() { // First pass deleted, then new, and lastly changed configuration, in order // to handle dependencies correctly. - foreach (array('delete', 'create', 'update') as $op) { + // @todo Implement proper dependency ordering using + // https://drupal.org/node/2080823 + foreach (array('delete', 'create', 'rename', 'update') as $op) { foreach ($this->getUnprocessedConfiguration($op) as $name) { if ($this->checkOp($op, $name)) { $this->processConfiguration($op, $name); @@ -479,6 +482,22 @@ public function validate() { if (!$this->storageComparer->validateSiteUuid()) { throw new ConfigImporterException('Site UUID in source storage does not match the target storage.'); } + // Validate renames. + foreach ($this->getUnprocessedConfiguration('rename') as $name) { + $names = explode('::', $name); + $old_name = $names[0]; + $new_name = $names[1]; + $old_entity_type_id = $this->configManager->getEntityTypeIdByName($old_name); + $new_entity_type_id = $this->configManager->getEntityTypeIdByName($new_name); + if ($old_entity_type_id != $new_entity_type_id) { + throw new ConfigImporterException(String::format('Entity type mismatch on rename. !old_type not equal to !new_type for existing configuration !old_name and staged configuration !new_name.', array('old_type' => $old_entity_type_id, 'new_type' => $new_entity_type_id, 'old_name' => $old_name, 'new_name' => $new_name))); + } + // Has to be a configuration entity. + if (!$old_entity_type_id) { + throw new ConfigImporterException(String::format('Rename operation for simple configuration. Existing configuration !old_name and staged configuration !new_name.', array('old_name' => $old_name, 'new_name' => $new_name))); + } + } + $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this)); $this->validated = TRUE; } @@ -576,6 +595,17 @@ protected function processExtension($type, $op, $name) { * TRUE is to continue processing, FALSE otherwise. */ protected function checkOp($op, $name) { + if ($op == 'rename') { + $names = explode('::', $name); + $new_name = $names[1]; + $target_exists = $this->storageComparer->getTargetStorage()->exists($new_name); + if ($target_exists) { + // Change the operation into an update. + $this->storageComparer->moveRenameToUpdate($name); + return FALSE; + } + return TRUE; + } $target_exists = $this->storageComparer->getTargetStorage()->exists($name); switch ($op) { case 'delete': @@ -664,6 +694,10 @@ protected function importConfig($op, $name) { * otherwise. */ protected function importInvokeOwner($op, $name) { + // Special case renames because they are hard. + if ($op == 'rename') { + return $this->importInvokeRename($name); + } // Validate the configuration object name before importing it. // Config::validateName($name); if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { @@ -689,6 +723,38 @@ protected function importInvokeOwner($op, $name) { $this->setProcessedConfiguration($op, $name); return TRUE; } + return FALSE; + } + + /** + * @param string $name + * @return bool + */ + protected function importInvokeRename($name) { + $names = explode('::', $name); + $old_name = $names[0]; + $new_name = $names[1]; + $entity_type_id = $this->configManager->getEntityTypeIdByName($old_name); + $old_config = new Config($old_name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($old_data = $this->storageComparer->getTargetStorage()->read($old_name)) { + $old_config->initWithData($old_data); + } + + $data = $this->storageComparer->getSourceStorage()->read($new_name); + $new_config = new Config($new_name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($data !== FALSE) { + $new_config->setData($data); + } + + $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id); + // Call to the configuration entity's storage to handle the configuration + // change. + if (!($entity_storage instanceof ImportableEntityStorageInterface)) { + throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type))); + } + $entity_storage->importRename($old_name, $new_config, $old_config); + $this->setProcessedConfiguration('rename', $name); + return TRUE; } /** diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php index 126275a..1fd73e7 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php @@ -455,4 +455,19 @@ public function importDelete($name, Config $new_config, Config $old_config) { return TRUE; } + /** + * {@inheritdoc} + */ + public function importRename($old_name, Config $new_config, Config $old_config) { + $id = static::getIDFromConfigName($old_name, $this->entityType->getConfigPrefix()); + $entity = $this->load($id); + $entity->setSyncing(TRUE); + $data = $new_config->get(); + foreach ($data as $key => $value) { + $entity->set($key, $value); + } + $entity->save(); + return TRUE; + } + } diff --git a/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php b/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php index 4eae41e..c09fd2a 100644 --- a/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php @@ -56,4 +56,16 @@ public function importUpdate($name, Config $new_config, Config $old_config); */ public function importDelete($name, Config $new_config, Config $old_config); + /** + * Rename entities upon synchronizing configuration changes. + * + * @param string $old_name + * The original name of the configuration object. + * @param \Drupal\Core\Config\Config $new_config + * A configuration object containing the new configuration data. + * @param \Drupal\Core\Config\Config $old_config + * A configuration object containing the old configuration data. + */ + public function importRename($old_name, Config $new_config, Config $old_config); + } diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php index b30db17..daf392d 100644 --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -100,6 +100,7 @@ public function getEmptyChangelist() { 'create' => array(), 'update' => array(), 'delete' => array(), + 'rename' => array() ); } @@ -147,6 +148,7 @@ public function createChangelist() { $this->addChangelistCreate(); $this->addChangelistUpdate(); $this->addChangelistDelete(); + $this->addChangelistRename(); $this->sourceData = NULL; $this->targetData = NULL; return $this; @@ -209,6 +211,70 @@ protected function addChangelistUpdate() { } /** + * Creates the rename changelist. + * + * The list of renames is created from the different source and target names + * with same uuid, these changes will be removed from the create and delete + * lists. For example, renamed content types should be not removed, but + * updated with the related nodes. + */ + protected function addChangelistRename() { + // Renames will be present in both create and delete lists. + $create_list = $this->getChangelist('create'); + $delete_list = $this->getChangelist('delete'); + if (empty($create_list) || empty($delete_list)) { + return; + } + + $create_uuids = array(); + foreach ($this->sourceData as $id => $data) { + if (isset($data['uuid']) && in_array($id, $create_list)) { + $create_uuids[$data['uuid']] = $id; + } + } + if (empty($create_uuids)) { + return; + } + + $renames = array(); + foreach ($this->targetData as $id => $data) { + if (isset($data['uuid']) && isset($create_uuids[$data['uuid']])) { + // Remove from create list. + $this->removeFromChangelist('create', $create_uuids[$data['uuid']]); + // Remove from delete list. + $this->removeFromChangelist('delete', $id); + // Create rename operation. + $renames[] = $id . '::' . $create_uuids[$data['uuid']]; + } + } + + // Import in the order that will do content types before fields. + $this->addChangeList('rename', array_reverse($renames)); + } + + /** + * Removes changes from all lists for the given name. + * + * @param string $op + * @param string $name + */ + protected function removeFromChangelist($op, $name) { + $key = array_search($name, $this->changelist[$op]); + if ($key !== FALSE) { + unset($this->changelist[$op][$key]); + } + } + + /** + * {@inheritdoc} + */ + public function moveRenameToUpdate($rename) { + $names = explode('::', $rename); + $this->removeFromChangelist('rename', $rename); + $this->addChangeList('update', array($names[1]), $this->sourceNames); + } + + /** * {@inheritdoc} */ public function reset() { @@ -220,7 +286,7 @@ public function reset() { /** * {@inheritdoc} */ - public function hasChanges($ops = array('delete', 'create', 'update')) { + public function hasChanges($ops = array('delete', 'create', 'update', 'rename')) { foreach ($ops as $op) { if (!empty($this->changelist[$op])) { return TRUE; diff --git a/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php index e8c597d..c96fe8d 100644 --- a/core/lib/Drupal/Core/Config/StorageComparerInterface.php +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -80,4 +80,12 @@ public function hasChanges($ops = array('delete', 'create', 'update')); */ public function validateSiteUuid(); + /** + * Move a rename operation to an update. + * + * @param string $rename + * The rename old_config_name::new_config_name + */ + public function moveRenameToUpdate($rename); + } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameTest.php new file mode 100644 index 0000000..61a9fef --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameTest.php @@ -0,0 +1,108 @@ + 'Import renamed configuration', + 'description' => 'Tests importing renamed configuration.', + 'group' => 'Configuration', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->installSchema('system', 'config_snapshot'); + $this->installSchema('node', 'node'); + + // Set up the ConfigImporter object for testing. + $storage_comparer = new StorageComparer( + $this->container->get('config.storage.staging'), + $this->container->get('config.storage') + ); + $this->configImporter = new ConfigImporter( + $storage_comparer->createChangelist(), + $this->container->get('event_dispatcher'), + $this->container->get('config.manager'), + $this->container->get('lock'), + $this->container->get('config.typed'), + $this->container->get('module_handler'), + $this->container->get('theme_handler'), + $this->container->get('string_translation') + ); + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); + } + + /** + * Tests configuration renaming. + */ + public function testRenamed() { + $content_type = entity_create('node_type', array( + 'type' => Unicode::strtolower($this->randomName(16)), + 'name' => $this->randomName(), + )); + $content_type->save(); + $active = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + + $config_name = $content_type->getEntityType()->getConfigPrefix() . '.' . $content_type->id(); + $this->copyConfig($active, $staging); + + // Change the machine name of the content type. This wil rename 5 + // configuration entities: the node type, the body field instance, the + // entity form display and the entity view display. + $content_type->type = Unicode::strtolower($this->randomName(8)); + $content_type->save(); + $renamed_config_name = $content_type->getEntityType()->getConfigPrefix() . '.' . $content_type->id(); + $this->assertTrue($active->exists($renamed_config_name), 'Content type has new name in active store.'); + $this->assertFalse($active->exists($config_name), 'Content type\'s old name does not exist active store.'); + + $this->configImporter->reset(); + $this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('create')), 'There are no configuration items to create.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('delete')), 'There are no configuration items to delete.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('update')), 'There are no configuration items to update.'); + $this->assertEqual(4, count($this->configImporter->getUnprocessedConfiguration('rename')), 'There are 4 configuration items to rename.'); + + // If we try to do this then we fail badly because of secondary writes and + // deletes. + $this->configImporter->import(); + } + +} +