Commit 37250bb5 authored by webchick's avatar webchick

Issue #1740378 by xjm, Désiré, alexpott | heyrocker: Implement renames in the import cycle.

parent 7ec9480e
......@@ -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;
......@@ -263,12 +264,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;
......@@ -291,7 +292,7 @@ public function getProcessedConfiguration() {
* Sets a change as processed.
*
* @param string $op
* The change operation performed, either delete, create or update.
* The change operation performed, either delete, create, rename, or update.
* @param string $name
* The name of the configuration processed.
*/
......@@ -304,7 +305,7 @@ protected function setProcessedConfiguration($op, $name) {
*
* @param string $op
* The change operation to get the unprocessed list for, either delete,
* create or update.
* create, rename, or update.
*
* @return array
* An array of configuration names.
......@@ -586,7 +587,7 @@ public function processConfigurations(array &$context) {
// into account.
if ($this->totalConfigurationToProcess == 0) {
$this->storageComparer->reset();
foreach (array('delete', 'create', 'update') as $op) {
foreach (array('delete', 'create', 'rename', 'update') as $op) {
foreach ($this->getUnprocessedConfiguration($op) as $name) {
$this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op));
}
......@@ -662,7 +663,7 @@ protected function getNextExtensionOperation() {
protected function getNextConfigurationOperation() {
// The order configuration operations is processed is important. Deletes
// have to come first so that recreates can work.
foreach (array('delete', 'create', 'update') as $op) {
foreach (array('delete', 'create', 'rename', 'update') as $op) {
$config_names = $this->getUnprocessedConfiguration($op);
if (!empty($config_names)) {
return array(
......@@ -685,6 +686,19 @@ protected function getNextConfigurationOperation() {
*/
public function validate() {
if (!$this->validated) {
// Validate renames.
foreach ($this->getUnprocessedConfiguration('rename') as $name) {
$names = $this->storageComparer->extractRenameNames($name);
$old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
$new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
if ($old_entity_type_id != $new_entity_type_id) {
$this->logError($this->t('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' => $names['old_name'], 'new_name' => $names['new_name'])));
}
// Has to be a configuration entity.
if (!$old_entity_type_id) {
$this->logError($this->t('Rename operation for simple configuration. Existing configuration !old_name and staged configuration !new_name.', array('old_name' => $names['old_name'], 'new_name' => $names['new_name'])));
}
}
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
if (count($this->getErrors())) {
throw new ConfigImporterException('There were errors validating the config synchronization.');
......@@ -787,6 +801,20 @@ protected function processExtension($type, $op, $name) {
* TRUE is to continue processing, FALSE otherwise.
*/
protected function checkOp($op, $name) {
if ($op == 'rename') {
$names = $this->storageComparer->extractRenameNames($name);
$target_exists = $this->storageComparer->getTargetStorage()->exists($names['new_name']);
if ($target_exists) {
// If the target exists, the rename has already occurred as the
// result of a secondary configuration write. Change the operation
// into an update. This is the desired behavior since renames often
// have to occur together. For example, renaming a node type must
// also result in renaming its field instances and entity displays.
$this->storageComparer->moveRenameToUpdate($name);
return FALSE;
}
return TRUE;
}
$target_exists = $this->storageComparer->getTargetStorage()->exists($name);
switch ($op) {
case 'delete':
......@@ -862,7 +890,7 @@ protected function importConfig($op, $name) {
*
* @param string $op
* The change operation to get the unprocessed list for, either delete,
* create or update.
* create, rename, or update.
* @param string $name
* The name of the configuration to process.
*
......@@ -875,6 +903,10 @@ protected function importConfig($op, $name) {
* otherwise.
*/
protected function importInvokeOwner($op, $name) {
// Renames are handled separately.
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)) {
......@@ -900,6 +932,45 @@ protected function importInvokeOwner($op, $name) {
$this->setProcessedConfiguration($op, $name);
return TRUE;
}
return FALSE;
}
/**
* Imports a configuration entity rename.
*
* @param string $rename_name
* The rename configuration name, as provided by
* \Drupal\Core\Config\StorageComparer::createRenameName().
*
* @return bool
* TRUE if the configuration was imported as a configuration entity. FALSE
* otherwise.
*
* @see \Drupal\Core\Config\ConfigImporter::createRenameName()
*/
protected function importInvokeRename($rename_name) {
$names = $this->storageComparer->extractRenameNames($rename_name);
$entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
$old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage()->read($names['old_name'])) {
$old_config->initWithData($old_data);
}
$data = $this->storageComparer->getSourceStorage()->read($names['new_name']);
$new_config = new Config($names['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_id)));
}
$entity_storage->importRename($names['old_name'], $new_config, $old_config);
$this->setProcessedConfiguration('rename', $rename_name);
return TRUE;
}
/**
......
......@@ -100,7 +100,10 @@ public function getConfigFactory() {
/**
* {@inheritdoc}
*/
public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $name) {
public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL) {
if (!isset($target_name)) {
$target_name = $source_name;
}
// @todo Replace with code that can be autoloaded.
// https://drupal.org/node/1848266
require_once __DIR__ . '/../../Component/Diff/DiffEngine.php';
......@@ -111,8 +114,8 @@ public function diff(StorageInterface $source_storage, StorageInterface $target_
$dumper = new Dumper();
$dumper->setIndentation(2);
$source_data = explode("\n", $dumper->dump($source_storage->read($name), PHP_INT_MAX));
$target_data = explode("\n", $dumper->dump($target_storage->read($name), PHP_INT_MAX));
$source_data = explode("\n", $dumper->dump($source_storage->read($source_name), PHP_INT_MAX));
$target_data = explode("\n", $dumper->dump($target_storage->read($target_name), PHP_INT_MAX));
// Check for new or removed files.
if ($source_data === array('false')) {
......
......@@ -46,15 +46,18 @@ public function getConfigFactory();
* The storage to diff configuration from.
* @param \Drupal\Core\Config\StorageInterface $target_storage
* The storage to diff configuration to.
* @param string $name
* The name of the configuration object to diff.
* @param string $source_name
* The name of the configuration object in the source storage to diff.
* @param string $target_name
* (optional) The name of the configuration object in the target storage.
* If omitted, the source name is used.
*
* @return core/lib/Drupal/Component/Diff
* A formatted string showing the difference between the two storages.
*
* @todo Make renderer injectable
*/
public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $name);
public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL);
/**
* Creates a configuration snapshot following a successful import.
......
......@@ -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;
}
}
......@@ -56,4 +56,16 @@ public function importUpdate($name, Config $new_config, Config $old_config);
*/
public function importDelete($name, Config $new_config, Config $old_config);
/**
* Renames 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);
}
......@@ -100,6 +100,7 @@ public function getEmptyChangelist() {
'create' => array(),
'update' => array(),
'delete' => array(),
'rename' => array(),
);
}
......@@ -117,7 +118,7 @@ public function getChangelist($op = NULL) {
* Adds changes to the changelist.
*
* @param string $op
* The change operation performed. Either delete, create or update.
* The change operation performed. Either delete, create, rename, or update.
* @param array $changes
* Array of changes to add to the changelist.
* @param array $sort_order
......@@ -147,6 +148,7 @@ public function createChangelist() {
$this->addChangelistCreate();
$this->addChangelistUpdate();
$this->addChangelistDelete();
$this->addChangelistRename();
$this->sourceData = NULL;
$this->targetData = NULL;
return $this;
......@@ -208,6 +210,80 @@ 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.
*/
protected function addChangelistRename() {
// Renames will be present in both the 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();
// Renames should be ordered so that dependencies are renamed last. This
// ensures that if there is logic in the configuration entity class to keep
// names in sync it will still work. $this->targetNames is in the desired
// order due to the use of configuration dependencies in
// \Drupal\Core\Config\StorageComparer::getAndSortConfigData().
// Node type is a good example of a configuration entity that renames other
// configuration when it is renamed.
// @see \Drupal\node\Entity\NodeType::postSave()
foreach ($this->targetNames as $name) {
$data = $this->targetData[$name];
if (isset($data['uuid']) && isset($create_uuids[$data['uuid']])) {
// Remove the item from the create list.
$this->removeFromChangelist('create', $create_uuids[$data['uuid']]);
// Remove the item from the delete list.
$this->removeFromChangelist('delete', $name);
// Create the rename name.
$renames[] = $this->createRenameName($name, $create_uuids[$data['uuid']]);
}
}
$this->addChangeList('rename', $renames);
}
/**
* Removes the entry from the given operation changelist for the given name.
*
* @param string $op
* The changelist to act on. Either delete, create, rename or update.
* @param string $name
* The name of the configuration to remove.
*/
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 = $this->extractRenameNames($rename);
$this->removeFromChangelist('rename', $rename);
$this->addChangeList('update', array($names['new_name']), $this->sourceNames);
}
/**
* {@inheritdoc}
*/
......@@ -220,7 +296,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;
......@@ -249,4 +325,31 @@ protected function getAndSortConfigData() {
$this->sourceNames = $dependency_manager->setData($this->sourceData)->sortAll();
}
/**
* Creates a rename name from the old and new names for the object.
*
* @param string $old_name
* The old configuration object name.
* @param string $new_name
* The new configuration object name.
*
* @return string
* The configuration change name that encodes both the old and the new name.
*
* @see \Drupal\Core\Config\StorageComparerInterface::extractRenameNames()
*/
protected function createRenameName($name1, $name2) {
return $name1 . '::' . $name2;
}
/**
* {@inheritdoc}
*/
public function extractRenameNames($name) {
$names = explode('::', $name, 2);
return array(
'old_name' => $names[0],
'new_name' => $names[1],
);
}
}
......@@ -80,4 +80,30 @@ public function hasChanges($ops = array('delete', 'create', 'update'));
*/
public function validateSiteUuid();
/**
* Moves a rename operation to an update.
*
* @param string $rename
* The rename name, as provided by ConfigImporter::createRenameName().
*
* @see \Drupal\Core\Config\ConfigImporter::createRenameName()
*/
public function moveRenameToUpdate($rename);
/**
* Extracts old and new configuration names from a configuration change name.
*
* @param string $name
* The configuration change name, as provided by
* ConfigImporter::createRenameName().
*
* @return array
* An associative array of configuration names. The array keys are
* 'old_name' and and 'new_name' representing the old and name configuration
* object names during a rename operation.
*
* @see \Drupal\Core\Config\StorageComparer::createRenameNames()
*/
public function extractRenameNames($name);
}
......@@ -7,9 +7,10 @@ config.sync:
_permission: 'synchronize configuration'
config.diff:
path: '/admin/config/development/configuration/sync/diff/{config_file}'
path: '/admin/config/development/configuration/sync/diff/{source_name}/{target_name}'
defaults:
_content: '\Drupal\config\Controller\ConfigController::diff'
target_name: NULL
requirements:
_permission: 'synchronize configuration'
......
......@@ -105,15 +105,15 @@ public function downloadExport() {
* @return string
* Table showing a two-way diff between the active and staged configuration.
*/
public function diff($config_file) {
public function diff($source_name, $target_name = NULL) {
$diff = $this->configManager->diff($this->targetStorage, $this->sourceStorage, $config_file);
$diff = $this->configManager->diff($this->targetStorage, $this->sourceStorage, $source_name, $target_name);
$formatter = new \DrupalDiffFormatter();
$formatter->show_header = FALSE;
$build = array();
$build['#title'] = t('View changes of @config_file', array('@config_file' => $config_file));
$build['#title'] = t('View changes of @config_file', array('@config_file' => $source_name));
// Add the CSS for the inline diff.
$build['#attached']['css'][] = drupal_get_path('module', 'system') . '/css/system.diff.css';
......
......@@ -184,8 +184,8 @@ public function buildForm(array $form, array &$form_state) {
// Add the AJAX library to the form for dialog support.
$form['#attached']['library'][] = 'core/drupal.ajax';
foreach ($storage_comparer->getChangelist() as $config_change_type => $config_files) {
if (empty($config_files)) {
foreach ($storage_comparer->getChangelist() as $config_change_type => $config_names) {
if (empty($config_names)) {
continue;
}
......@@ -197,15 +197,19 @@ public function buildForm(array $form, array &$form_state) {
);
switch ($config_change_type) {
case 'create':
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count new', '@count new');
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count new', '@count new');
break;
case 'update':
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count changed', '@count changed');
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count changed', '@count changed');
break;
case 'delete':
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count removed', '@count removed');
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count removed', '@count removed');
break;
case 'rename':
$form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count renamed', '@count renamed');
break;
}
$form[$config_change_type]['list'] = array(
......@@ -213,10 +217,18 @@ public function buildForm(array $form, array &$form_state) {
'#header' => array('Name', 'Operations'),
);
foreach ($config_files as $config_file) {
foreach ($config_names as $config_name) {
if ($config_change_type == 'rename') {
$names = $storage_comparer->extractRenameNames($config_name);
$href = $this->urlGenerator->getPathFromRoute('config.diff', array('source_name' => $names['old_name'], 'target_name' => $names['new_name']));
$config_name = $this->t('!source_name to !target_name', array('!source_name' => $names['old_name'], '!target_name' => $names['new_name']));
}
else {
$href = $this->urlGenerator->getPathFromRoute('config.diff', array('source_name' => $config_name));
}
$links['view_diff'] = array(
'title' => $this->t('View differences'),
'href' => $this->urlGenerator->getPathFromRoute('config.diff', array('config_file' => $config_file)),
'href' => $href,
'attributes' => array(
'class' => array('use-ajax'),
'data-accepts' => 'application/vnd.drupal-modal',
......@@ -226,7 +238,7 @@ public function buildForm(array $form, array &$form_state) {
),
);
$form[$config_change_type]['list']['#rows'][] = array(
'name' => $config_file,
'name' => $config_name,
'operations' => array(
'data' => array(
'#type' => 'operations',
......
......@@ -84,6 +84,33 @@ function testDiff() {
$this->assertEqual($diff->edits[1]->type, 'add', 'The second item in the diff is an add.');
$this->assertFalse($diff->edits[1]->orig, format_string("The key '%add_key' does not exist in active.", array('%add_key' => $add_key)));
$this->assertEqual($diff->edits[1]->closing[0], $add_key . ': ' . $add_data, format_string("The staging value for key '%add_key' is '%add_data'.", array('%add_key' => $add_key, '%add_data' => $add_data)));
// Test diffing a renamed config entity.
$test_entity_id = $this->randomName();
$test_entity = entity_create('config_test', array(
'id' => $test_entity_id,
'label' => $this->randomName(),
));
$test_entity->save();
$data = $active->read('config_test.dynamic.' . $test_entity_id);
$staging->write('config_test.dynamic.' . $test_entity_id, $data);
$config_name = 'config_test.dynamic.' . $test_entity_id;
$diff = \Drupal::service('config.manager')->diff($active, $staging, $config_name, $config_name);
// Prove the fields match.
$this->assertEqual($diff->edits[0]->type, 'copy', 'The first item in the diff is a copy.');
$this->assertEqual(count($diff->edits), 1, 'There is one item in the diff');
// Rename the entity.
$new_test_entity_id = $this->randomName();
$test_entity->set('id', $new_test_entity_id);
$test_entity->save();
$diff = \Drupal::service('config.manager')->diff($active, $staging, 'config_test.dynamic.' . $new_test_entity_id, $config_name);
$this->assertEqual($diff->edits[0]->type, 'change', 'The second item in the diff is a copy.');
$this->assertEqual($diff->edits[0]->orig, array('id: ' . $new_test_entity_id));
$this->assertEqual($diff->edits[0]->closing, array('id: ' . $test_entity_id));
$this->assertEqual($diff->edits[1]->type, 'copy', 'The second item in the diff is a copy.');
$this->assertEqual(count($diff->edits), 2, 'There are two items in the diff.');
}
}
\ No newline at end of file
}
<?php
/**
* @file
* Contains \Drupal\config\Tests\ConfigImportRenameValidationTest.
*/
namespace Drupal\config\Tests;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Uuid\Php;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\StorageComparer;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests validating renamed configuration in a configuration import.
*/
class ConfigImportRenameValidationTest extends DrupalUnitTestBase {
/**
* Config Importer object used for testing.
*
* @var \Drupal\Core\Config\ConfigImporter
*/
protected $configImporter;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'node', 'field', 'text', 'entity', 'config_test');
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Configuration import rename validation',
'description' => 'Tests validating renamed configuration in a configuration import.',
'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')
);
}
/**
* Tests configuration renaming validation.
*/
public function testRenameValidation() {
// Create a test entity.
$test_entity_id = $this->randomName();
$test_entity = entity_create('config_test', array(
'id' => $test_entity_id,
'label' => $this->randomName(),
));
$test_entity->save();
$uuid = $test_entity->uuid();
// Stage the test entity and then delete it from the active storage.
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
$test_entity->delete();
// Create a content type with a matching UUID in the active storage.
$content_type = entity_create('node_type', array(
'type' => Unicode::strtolower($this->randomName(16)),
'name' => $this->randomName(),
'uuid' => $uuid,
));
$content_type->save();
// Confirm that the staged configuration is detected as a rename since the
// UUIDs match.
$this->configImporter->reset();
$expected = array(
'node.type.' . $content_type->id() . '::config_test.dynamic.' . $test_entity_id,
);
$renames = $this->configImporter->getUnprocessedConfiguration('rename');
$this->assertIdentical($expected, $renames);
// Try to import the configuration. We expect an exception to be thrown
// because the staged entity is of a different type.
try {
$this->configImporter->import();
$this->fail('Expected ConfigImporterException thrown when a renamed configuration entity does not match the existing entity type.');
}
catch (ConfigImporterException $e) {
$this->pass('Expected ConfigImporterException thrown when a renamed configuration entity does not match the existing entity type.');
$expected = array(
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' => 'node_type', 'new_type' => 'config_test', 'old_name' => 'node.type.' . $content_type->id(), 'new_name' => 'config_test.dynamic.' . $test_entity_id))
);
$this->assertIdentical($expected, $this->configImporter->getErrors());
}
}
/**
* Tests configuration renaming validation for simple configuration.
*/
public function testRenameSimpleConfigValidation() {
$uuid = new Php();
// Create a simple configuration with a UUID.
$config = \Drupal::config('config_test.new');
$uuid_value = $uuid->generate();
$config->set('uuid', $uuid_value)->save();
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$this->copyConfig($active, $staging);
$config->delete();
// Create another simple configuration with the same UUID.
$config = \Drupal::config('config_test.old');
$config->set('uuid', $uuid_value)->save();