From 2a03c1948368faa4ef9569a49fe6010aa0706430 Mon Sep 17 00:00:00 2001 From: Matt Glaman Date: Tue, 1 Mar 2016 17:47:36 -0600 Subject: [PATCH] WIP ConfigUpdater service Because hook_update_N + config == hard. --- commerce.services.yml | 4 + src/ConfigUpdater.php | 279 ++++++++++++++++++ src/ConfigUpdaterInterface.php | 102 +++++++ src/ConfigUpdaterResult.php | 51 ++++ src/ConfigUpdaterResultInterface.php | 26 ++ src/Tests/ConfigUpdaterTest.php | 170 +++++++++++ .../commerce_update_test.info.yml | 7 + ...erce_store.commerce_store_type.testing.yml | 6 + tests/src/Unit/ConfigUpdaterResultTest.php | 53 ++++ 9 files changed, 698 insertions(+) create mode 100644 src/ConfigUpdater.php create mode 100644 src/ConfigUpdaterInterface.php create mode 100644 src/ConfigUpdaterResult.php create mode 100644 src/ConfigUpdaterResultInterface.php create mode 100644 src/Tests/ConfigUpdaterTest.php create mode 100644 tests/modules/commerce_update_test/commerce_update_test.info.yml create mode 100644 tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml create mode 100644 tests/src/Unit/ConfigUpdaterResultTest.php diff --git a/commerce.services.yml b/commerce.services.yml index 466b938c00..8ef29d1f17 100644 --- a/commerce.services.yml +++ b/commerce.services.yml @@ -34,6 +34,10 @@ services: tags: - { name: service_collector, tag: commerce.availability_checker, call: addChecker } + commerce.config_update: + class: Drupal\commerce\ConfigUpdater + arguments: ['@entity_type.manager', '@config.storage', '@config.factory'] + cache_context.country: class: Drupal\commerce\Cache\Context\CountryCacheContext arguments: ['@commerce.country_context'] diff --git a/src/ConfigUpdater.php b/src/ConfigUpdater.php new file mode 100644 index 0000000000..27bd502107 --- /dev/null +++ b/src/ConfigUpdater.php @@ -0,0 +1,279 @@ +entityTypeManager = $entity_type_manager; + $this->activeConfigStorage = $active_config_storage; + $this->extensionConfigStorage = new ExtensionInstallStorage($active_config_storage, InstallStorage::CONFIG_INSTALL_DIRECTORY); + $this->extensionOptionalConfigStorage = new ExtensionInstallStorage($active_config_storage, InstallStorage::CONFIG_OPTIONAL_DIRECTORY); + $this->configFactory = $config_factory; + + foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) { + if ($definition->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $definition */ + $prefix = $definition->getConfigPrefix(); + $this->typesByPrefix[$prefix] = $entity_type; + } + } + } + + /** + * {@inheritdoc} + */ + public function import(array $config_names) { + $succeeded = []; + $failed = []; + + foreach ($config_names as $config_name) { + // Read the config from the file. + $value = $this->loadFromExtension($config_name); + if (!$value) { + $failed[$config_name] = $this->t('@config did not exist in extension storage', ['@config' => $config_name]); + continue; + } + + if ($this->loadFromActive($config_name)) { + $failed[$config_name] = $this->t('@config already exists, use revert to update', ['@config' => $config_name]); + continue; + } + + $type = $this->getConfigType($config_name); + + // Save it as a new config entity or simple config. + if ($type == 'system.simple') { + $this->configFactory->getEditable($config_name)->setData($value)->save(); + } + else { + /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager->getStorage($type); + $entity = $entity_storage->createFromStorageRecord($value); + $entity->save(); + } + + $succeeded[$config_name] = $this->t('@config was successfully imported', ['@config' => $config_name]); + } + + return new ConfigUpdaterResult($failed, $succeeded); + } + + /** + * {@inheritdoc} + */ + public function revert(array $config_names, $skip_modified = TRUE) { + $succeeded = []; + $failed = []; + + foreach ($config_names as $config_name) { + // Read the config from the file. + $value = $this->loadFromExtension($config_name); + if (!$value) { + $failed[$config_name] = $this->t('@config did not exist in extension storage', ['@config' => $config_name]); + continue; + } + + // Check the configuration object's hash and see if it has been modified. + if ($this->isModified($config_name) && $skip_modified) { + $failed[$config_name] = $this->t('@config has been modified and was not reverted', ['@config' => $config_name]); + continue; + } + + $type = $this->getConfigType($config_name); + if ($type == 'system.simple') { + // Load the current config and replace the value. + $this->configFactory->getEditable($config_name)->setData($value)->save(); + } + else { + // Load the current config entity and replace the value, with the + // old UUID. + $definition = $this->entityTypeManager->getDefinition($type); + $id_key = $definition->getKey('id'); + + $id = $value[$id_key]; + /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager->getStorage($type); + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + $entity = $entity_storage->load($id); + $uuid = $entity->get('uuid'); + $entity = $entity_storage->updateFromStorageRecord($entity, $value); + $entity->set('uuid', $uuid); + $entity->save(); + } + + $succeeded[$config_name] = $this->t('@config was successfully reverted', ['@config' => $config_name]); + } + + return new ConfigUpdaterResult($failed, $succeeded); + } + + /** + * {@inheritdoc} + */ + public function delete(array $config_names) { + $succeeded = []; + $failed = []; + + foreach ($config_names as $config_name) { + $value = $this->loadFromActive($config_name); + if (!$value) { + $failed[$config_name] = $this->t('@config did not exist in extension storage', ['@config' => $config_name]); + continue; + } + + // Check the configuration object's hash and see if it has been modified. + if ($this->isModified($config_name)) { + $failed[$config_name] = $this->t('@config has been modified and was not deleted', ['@config' => $config_name]); + continue; + } + + $type = $this->getConfigType($config_name); + if ($type == 'system.simple') { + $config = $this->configFactory->getEditable($config_name); + if (!$config) { + $failed[$config_name] = $this->t('@config did not exist in active storage', ['@config' => $config_name]); + continue; + } + $config->delete(); + } + else { + $definition = $this->entityTypeManager->getDefinition($type); + $id_key = $definition->getKey('id'); + $id = $value[$id_key]; + + /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager->getStorage($type); + $entity = $entity_storage->load($id); + $entity_storage->delete([$entity]); + } + + $succeeded[$config_name] = $this->t('@config was successfully deleted', ['@config' => $config_name]); + } + + return new ConfigUpdaterResult($failed, $succeeded); + } + + /** + * {@inheritdoc} + */ + public function loadFromActive($config_name) { + return $this->activeConfigStorage->read($config_name); + } + + /** + * {@inheritdoc} + */ + public function loadFromExtension($config_name) { + $value = $this->extensionConfigStorage->read($config_name); + if (!$value) { + $value = $this->extensionOptionalConfigStorage->read($config_name); + } + return $value; + } + + /** + * {@inheritdoc} + */ + public function isModified($config_name) { + // Read the configuration from active storage. + $active = $this->activeConfigStorage->read($config_name); + + // Get the hash created when the config was installed. + $original_hash = $active['_core']['default_config_hash']; + + // Remove export keys not used to generate default config hash. + unset($active['uuid']); + unset($active['_core']); + + // Recalculate the hash. + $active_hash = Crypt::hashBase64(serialize($active)); + + return $original_hash !== $active_hash; + } + + /** + * Returns the config type for a given config object. + * + * @param string $config_name + * Name of the config object. + * + * @return string + * Name of the config type. + */ + protected function getConfigType($config_name) { + foreach ($this->typesByPrefix as $prefix => $config_type) { + if (strpos($config_name, $prefix) === 0) { + return $config_type; + } + } + + return NULL; + } + +} diff --git a/src/ConfigUpdaterInterface.php b/src/ConfigUpdaterInterface.php new file mode 100644 index 0000000000..5cb2db4c95 --- /dev/null +++ b/src/ConfigUpdaterInterface.php @@ -0,0 +1,102 @@ +failed = $failed; + $this->succeeded = $succeeded; + } + + /** + * {@inheritdoc} + */ + public function getFailed() { + return $this->failed; + } + + /** + * {@inheritdoc} + */ + public function getSucceeded() { + return $this->succeeded; + } + +} diff --git a/src/ConfigUpdaterResultInterface.php b/src/ConfigUpdaterResultInterface.php new file mode 100644 index 0000000000..22c53c7648 --- /dev/null +++ b/src/ConfigUpdaterResultInterface.php @@ -0,0 +1,26 @@ +configUpdater = \Drupal::service('commerce.config_update'); + } + + /** + * Tests loading data from active storage. + */ + public function testLoadFromActive() { + $config_name = 'commerce_store.commerce_store_type.testing'; + $data = $this->configUpdater->loadFromActive($config_name); + $this->assertEqual($data['id'], 'testing'); + } + + /** + * Tests loading data from extension storage. + */ + public function testLoadFromExtension() { + $config_name = 'views.view.commerce_stores'; + $data = $this->configUpdater->loadFromExtension($config_name); + $this->assertEqual($data['id'], 'commerce_stores'); + } + + /** + * Test isModified on the commerce_store_type.default config object. + */ + public function testIsModified() { + $config_name = 'commerce_store.commerce_store_type.testing'; + $this->assertFalse($this->configUpdater->isModified($config_name)); + + /** @var \Drupal\commerce_store\Entity\StoreTypeInterface $store_type */ + $store_type = \Drupal::entityTypeManager()->getStorage('commerce_store_type') + ->load('testing'); + $store_type->setDescription('The default store'); + $store_type->save(); + + $this->assertTrue($this->configUpdater->isModified($config_name)); + } + + /** + * Tests delete when config is not modified. + */ + public function testDeleteNotModified() { + $config_name = 'commerce_store.commerce_store_type.testing'; + $result = $this->configUpdater->delete([ + $config_name, + ]); + + $failed = $result->getFailed(); + $succeeded = $result->getSucceeded(); + + $this->assertTrue(empty($failed)); + $this->assertEqual($succeeded[$config_name], "$config_name was successfully deleted"); + } + + /** + * Tests delete when config is not modified. + */ + public function testDeleteModified() { + $config_name = 'commerce_store.commerce_store_type.testing'; + + /** @var \Drupal\commerce_store\Entity\StoreTypeInterface $store_type */ + $store_type = \Drupal::entityTypeManager()->getStorage('commerce_store_type') + ->load('testing'); + $store_type->setDescription('The default store'); + $store_type->save(); + + $result = $this->configUpdater->delete([ + $config_name, + ]); + + $failed = $result->getFailed(); + $succeeded = $result->getSucceeded(); + + $this->assertTrue(empty($succeeded)); + $this->assertEqual($failed[$config_name], "$config_name has been modified and was not deleted"); + } + + /** + * Tests revert if config is modified, and forced revert. + */ + public function testRevertModified() { + $config_name = 'commerce_store.commerce_store_type.testing'; + + /** @var \Drupal\commerce_store\Entity\StoreTypeInterface $store_type */ + $store_type = \Drupal::entityTypeManager()->getStorage('commerce_store_type') + ->load('testing'); + $store_type->setDescription('The default store'); + $store_type->save(); + + $result = $this->configUpdater->revert([ + $config_name, + ]); + + $failed = $result->getFailed(); + $succeeded = $result->getSucceeded(); + + $this->assertTrue(empty($succeeded)); + $this->assertEqual($failed[$config_name], "$config_name has been modified and was not reverted"); + + $result = $this->configUpdater->revert([ + $config_name, + ], FALSE); + + $succeeded = $result->getSucceeded(); + + $this->assertFalse(empty($succeeded)); + $this->assertEqual($succeeded[$config_name], "$config_name was successfully reverted"); + + /** @var \Drupal\commerce_store\Entity\StoreTypeInterface $store_type */ + $store_type = \Drupal::entityTypeManager()->getStorage('commerce_store_type') + ->load('testing'); + $this->assertNull($store_type->getDescription()); + } + + /** + * Tests import. + */ + public function testImport() { + $config_name = 'commerce_store.commerce_store_type.testing'; + $this->configUpdater->delete([$config_name]); + + $result = $this->configUpdater->import([ + $config_name, + ]); + + $failed = $result->getFailed(); + $succeeded = $result->getSucceeded(); + + $this->assertTrue(empty($failed)); + $this->assertEqual($succeeded[$config_name], "$config_name was successfully imported"); + + $result = $this->configUpdater->import([ + $config_name, + ]); + + $failed = $result->getFailed(); + $succeeded = $result->getSucceeded(); + + $this->assertTrue(empty($succeeded)); + $this->assertEqual($failed[$config_name], "$config_name already exists, use revert to update"); + } + +} diff --git a/tests/modules/commerce_update_test/commerce_update_test.info.yml b/tests/modules/commerce_update_test/commerce_update_test.info.yml new file mode 100644 index 0000000000..f75cdad72b --- /dev/null +++ b/tests/modules/commerce_update_test/commerce_update_test.info.yml @@ -0,0 +1,7 @@ +name: 'Commerce update test' +type: module +description: 'Module for testing extension updates to configuration.' +package: Testing +core: 8.x +dependencies: + - commerce_store diff --git a/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml b/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml new file mode 100644 index 0000000000..9a96125096 --- /dev/null +++ b/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml @@ -0,0 +1,6 @@ +langcode: en +status: true +dependencies: { } +id: testing +label: Testing +description: null diff --git a/tests/src/Unit/ConfigUpdaterResultTest.php b/tests/src/Unit/ConfigUpdaterResultTest.php new file mode 100644 index 0000000000..cd618d5363 --- /dev/null +++ b/tests/src/Unit/ConfigUpdaterResultTest.php @@ -0,0 +1,53 @@ + $this->prophesize(MarkupInterface::class), + 'fake.config.name.two' => $this->prophesize(MarkupInterface::class), + 'fake.config.name.three' => $this->prophesize(MarkupInterface::class), + 'fake.config.name.four' => $this->prophesize(MarkupInterface::class), + ]; + + $result = new ConfigUpdaterResult($messages, []); + + $this->assertEquals(count($result->getFailed()), 4); + } + + /** + * @covers ::getSucceeded + */ + public function testGetSucceeded() { + $messages = [ + 'fake.config.name.one' => $this->prophesize(MarkupInterface::class), + 'fake.config.name.two' => $this->prophesize(MarkupInterface::class), + 'fake.config.name.three' => $this->prophesize(MarkupInterface::class), + 'fake.config.name.four' => $this->prophesize(MarkupInterface::class), + ]; + + $result = new ConfigUpdaterResult([], $messages); + + $this->assertEquals(count($result->getSucceeded()), 4); + } +}