diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 5cc1a95..80adc94 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Config; +use Drupal\Component\Utility\String; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\Lock\LockBackendInterface; @@ -144,7 +145,7 @@ public function reset() { * @return bool * TRUE if there are changes to process and FALSE if not. */ - public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')) { + public function hasUnprocessedChanges($ops = array('delete', 'create', 'update', 'rename')) { foreach ($ops as $op) { if (count($this->getUnprocessed($op))) { return TRUE; @@ -210,7 +211,7 @@ public function import() { // to handle dependencies correctly. // @todo Implement proper dependency ordering using // https://drupal.org/node/2080823 - foreach (array('delete', 'create', 'update') as $op) { + foreach (array('delete', 'create', 'update', 'rename') as $op) { foreach ($this->getUnprocessed($op) as $name) { $this->process($op, $name); } @@ -237,6 +238,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->getUnprocessed('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; } @@ -297,6 +314,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); + } // Call to the configuration entity's storage controller to handle the // configuration change. $handled_by_module = FALSE; @@ -325,6 +346,31 @@ protected function importInvokeOwner($op, $name) { } /** + * @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); + } + + $this->configManager->getEntityManager()->getStorageController($entity_type_id)->importRename($new_name, $new_config, $old_name, $old_config); + $this->setProcessed('rename', $name); + return TRUE; + } + + /** * Determines if a import is already running. * * @return bool diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index a70ad36..13ef76f 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -436,4 +436,19 @@ public function importDelete($name, Config $new_config, Config $old_config) { return TRUE; } + /** + * {@inheritdoc} + */ + public function importRename($new_name, Config $new_config, $old_name, 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/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php index c08cb5a..0875f95 100644 --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -87,6 +87,7 @@ public function getEmptyChangelist() { 'create' => array(), 'update' => array(), 'delete' => array(), + 'rename' => array() ); } @@ -117,7 +118,8 @@ public function createChangelist() { return $this ->addChangelistCreate() ->addChangelistUpdate() - ->addChangelistDelete(); + ->addChangelistDelete() + ->addChangelistRename(); } /** @@ -142,12 +144,43 @@ public function addChangelistUpdate() { $source_config_data = $this->sourceStorage->read($name); $target_config_data = $this->targetStorage->read($name); if ($source_config_data !== $target_config_data) { - $this->addChangeList('update', array($name)); + if (isset($source_config_data['uuid']) && $source_config_data['uuid'] != $target_config_data['uuid']) { + // Needs to be a delete and a create. + $this->addChangeList('delete', array($name)); + $this->addChangeList('create', array($name)); + } + else { + $this->addChangeList('update', array($name)); + } } } return $this; } + public function addChangelistRename() { + // Renames will be present in both create and delete lists + $create_uuids = array(); + foreach ($this->getSourceStorage()->readMultiple(array_diff($this->getSourceNames(), $this->getTargetNames())) as $create_id => $data) { + if (isset($data['uuid'])) { + $create_uuids[$data['uuid']] = $create_id; + } + } + $renames = array(); + foreach ($this->getTargetStorage()->readMultiple(array_diff($this->getTargetNames(), $this->getSourceNames())) as $delete_id => $data) { + if (isset($data['uuid']) && isset($create_uuids[$data['uuid']])) { + $renames[] = $delete_id . '::' . $create_uuids[$data['uuid']]; + + $key = array_search($create_uuids[$data['uuid']], $this->changelist['create']); + unset($this->changelist['create'][$key]); + $key = array_search($delete_id, $this->changelist['delete']); + unset($this->changelist['delete'][$key]); + } + } + // Reverse the array until we can manage dependencies. + $this->addChangeList('rename', array_reverse($renames)); + return $this; + } + /** * {@inheritdoc} */ @@ -160,7 +193,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/modules/config/lib/Drupal/config/Tests/ConfigImportRenameTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameTest.php new file mode 100644 index 0000000..f862cce --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameTest.php @@ -0,0 +1,140 @@ + 'Import renamed configuration', + 'description' => 'Tests importing renamed configuration.', + 'group' => 'Configuration', + ); + } + + 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->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); + } + + public function testRenamed() { + $content_type = entity_create('node_type', array( + 'type' => Unicode::strtolower($this->randomName(16)), + 'name' => $this->randomName(), + )); + $content_type->save(); + /** @var \Drupal\Core\Config\StorageInterface $active */ + $active = $this->container->get('config.storage'); + /** @var \Drupal\Core\Config\StorageInterface $staging */ + $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, 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->getUnprocessed('create')), 'There are no configuration items to create.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessed('delete')), 'There are no configuration items to delete.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessed('update')), 'There are no configuration items to update.'); + $this->assertEqual(5, count($this->configImporter->getUnprocessed('rename')), 'There are 5 configuration items to rename.'); + + // If we try to do this then we fail badly because of secondary writes and + // deletes. + //$this->configImporter->import(); + } + + public function testSameName() { + $type_name = Unicode::strtolower($this->randomName(16)); + $content_type = entity_create('node_type', array( + 'type' => $type_name, + 'name' => 'first node type', + )); + $content_type->save(); + /** @var \Drupal\Core\Config\StorageInterface $active */ + $active = $this->container->get('config.storage'); + /** @var \Drupal\Core\Config\StorageInterface $staging */ + $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, the body field instance, the + // entity form display and the entity view displays for teaser and default. + $content_type->delete(); + $this->assertFalse($active->exists($config_name), 'Content type\'s old name does not exist active store.'); + // Recreate with the same type - this will have a different UUID. + $content_type = entity_create('node_type', array( + 'type' => $type_name, + 'name' => 'second node type', + )); + $content_type->save(); + + $this->configImporter->reset(); + $this->assertEqual(6, count($this->configImporter->getUnprocessed('create')), 'There are 6 configuration items to create.'); + $this->assertEqual(6, count($this->configImporter->getUnprocessed('delete')), 'There are 6 configuration items to delete.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessed('update')), 'There are no configuration items to update.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessed('rename')), 'There are no configuration items to rename.'); + + $this->configImporter->import(); + + // Verify that there is nothing more to import. + $this->assertFalse($this->configImporter->reset()->hasUnprocessedChanges()); + $content_type = entity_load('node_type', $type_name); + $this->assertEqual('first node type', $content_type->label()); + } + + +} +