Commit af4e4bb3 authored by mglaman's avatar mglaman Committed by bojanz

Add the ConfigUpdater.

parent 1826727e
......@@ -34,6 +34,10 @@ services:
tags:
- { name: service_collector, tag: commerce.availability_checker, call: addChecker }
commerce.config_updater:
class: Drupal\commerce\Config\ConfigUpdater
arguments: ['@entity_type.manager', '@config.storage', '@config.factory']
cache_context.country:
class: Drupal\commerce\Cache\Context\CountryCacheContext
arguments: ['@commerce.country_context']
......
<?php
namespace Drupal\commerce\Config;
/**
* Result object for configuration updates.
*/
class ConfigUpdateResult {
/**
* The success messages keyed by config name.
*
* @var string[]
*/
protected $succeeded = [];
/**
* The failure messages keyed by config name.
*
* @var string[]
*/
protected $failed = [];
/**
* Constructs a new ConfigUpdateResult object.
*
* @param string[] $succeeded
* The success messages keyed by config name.
* @param string[] $failed
* The failure messages keyed by config name.
*/
public function __construct(array $succeeded, array $failed) {
$this->succeeded = $succeeded;
$this->failed = $failed;
}
/**
* Gets the success messages.
*
* @return string[]
* The success messages keyed by config name.
*/
public function getSucceeded() {
return $this->succeeded;
}
/**
* Gets the failure messages.
*
* @return string[]
* The failure messages keyed by config name.
*/
public function getFailed() {
return $this->failed;
}
}
<?php
namespace Drupal\commerce\Config;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ExtensionInstallStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Default implementation of the ConfigUpdaterInterface.
*/
class ConfigUpdater implements ConfigUpdaterInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The active config storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $activeConfigStorage;
/**
* The extension config storage for config/install config items.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $extensionConfigStorage;
/**
* The extension config storage for config/optional config items.
*
* @var \Drupal\Core\Config\ExtensionInstallStorage
*/
protected $extensionOptionalConfigStorage;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* List of current config entity types, keyed by prefix.
*
* @var string[]
*/
protected $typesByPrefix = [];
/**
* Constructs a new ConfigUpdater object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\StorageInterface $active_config_storage
* The active config storage.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, StorageInterface $active_config_storage, ConfigFactoryInterface $config_factory) {
$this->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) {
$config_data = $this->loadFromExtension($config_name);
if (!$config_data) {
$failed[$config_name] = $this->t('@config does 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;
}
$config_type = $this->getConfigType($config_name);
if ($config_type == 'system.simple') {
$this->configFactory->getEditable($config_name)->setData($config_data)->save();
}
else {
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
$entity_storage = $this->entityTypeManager->getStorage($config_type);
$entity = $entity_storage->createFromStorageRecord($config_data);
$entity->save();
}
$succeeded[$config_name] = $this->t('@config was successfully imported', ['@config' => $config_name]);
}
return new ConfigUpdateResult($succeeded, $failed);
}
/**
* {@inheritdoc}
*/
public function revert(array $config_names, $skip_modified = TRUE) {
$succeeded = [];
$failed = [];
foreach ($config_names as $config_name) {
$config_data = $this->loadFromExtension($config_name);
if (!$config_data) {
$failed[$config_name] = $this->t('@config does not exist in extension storage', ['@config' => $config_name]);
continue;
}
$active_config_data = $this->loadFromActive($config_name);
if (!$active_config_data) {
$failed[$config_name] = $this->t('@config does not exist in active storage', ['@config' => $config_name]);
continue;
}
if ($this->isModified($active_config_data) && $skip_modified) {
$failed[$config_name] = $this->t('@config could not be reverted because it was modified by the user', ['@config' => $config_name]);
continue;
}
$config_type = $this->getConfigType($config_name);
if ($config_type == 'system.simple') {
$this->configFactory->getEditable($config_name)->setData($config_data)->save();
}
else {
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
$entity_type = $this->entityTypeManager->getDefinition($config_type);
$id = substr($config_name, strlen($entity_type->getConfigPrefix()) + 1);
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
$entity_storage = $this->entityTypeManager->getStorage($config_type);
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$entity = $entity_storage->load($id);
// The UUID must remain unchanged between updates.
$uuid = $entity->uuid();
$entity = $entity_storage->updateFromStorageRecord($entity, $config_data);
$entity->set('uuid', $uuid);
$entity->save();
}
$succeeded[$config_name] = $this->t('@config was successfully reverted', ['@config' => $config_name]);
}
return new ConfigUpdateResult($succeeded, $failed);
}
/**
* {@inheritdoc}
*/
public function delete(array $config_names) {
$succeeded = [];
$failed = [];
foreach ($config_names as $config_name) {
// Ensure there's something to delete.
if (!$this->loadFromActive($config_name)) {
$failed[$config_name] = $this->t('@config does not exist in active storage', ['@config' => $config_name]);
continue;
}
$config_type = $this->getConfigType($config_name);
if ($config_type == 'system.simple') {
$this->configFactory->getEditable($config_name)->delete();
}
else {
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
$entity_type = $this->entityTypeManager->getDefinition($config_type);
$id = substr($config_name, strlen($entity_type->getConfigPrefix()) + 1);
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
$entity_storage = $this->entityTypeManager->getStorage($config_type);
$entity = $entity_storage->load($id);
$entity_storage->delete([$entity]);
}
$succeeded[$config_name] = $this->t('@config was successfully deleted', ['@config' => $config_name]);
}
return new ConfigUpdateResult($succeeded, $failed);
}
/**
* {@inheritdoc}
*/
public function loadFromActive($config_name) {
return $this->activeConfigStorage->read($config_name);
}
/**
* {@inheritdoc}
*/
public function loadFromExtension($config_name) {
$data = $this->extensionConfigStorage->read($config_name);
if (!$data) {
$data = $this->extensionOptionalConfigStorage->read($config_name);
}
return $data;
}
/**
* {@inheritdoc}
*/
public function isModified(array $config) {
$original_hash = $config['_core']['default_config_hash'];
// Create a new hash based on current values.
unset($config['uuid']);
unset($config['_core']);
$current_hash = Crypt::hashBase64(serialize($config));
return $original_hash !== $current_hash;
}
/**
* Gets the config type for a given config object.
*
* @param string $config_name
* Name of the config object.
*
* @return string
* Name of the config type. Either 'system.simple' or an entity type id.
*/
protected function getConfigType($config_name) {
foreach ($this->typesByPrefix as $prefix => $config_type) {
if (strpos($config_name, $prefix) === 0) {
return $config_type;
}
}
return NULL;
}
}
<?php
namespace Drupal\commerce\Config;
/**
* Performs configuration updates.
*
* Allows an extension to import, revert, delete configuration.
* Needs to be used from hook_post_update_NAME(), since it uses the entity API.
*
* @see hook_post_update_NAME()
*/
interface ConfigUpdaterInterface {
/**
* Imports configuration from extension storage to active storage.
*
* @param string[] $config_names
* The configuration names.
*
* @return \Drupal\commerce\Config\ConfigUpdateResult
* The result.
*/
public function import(array $config_names);
/**
* Reverts configuration to the values from extension storage.
*
* @param string[] $config_names
* The configuration names.
* @param bool $skip_modified
* Whether to skip modified configuration.
*
* @return \Drupal\commerce\Config\ConfigUpdateResult
* The result.
*/
public function revert(array $config_names, $skip_modified = TRUE);
/**
* Deletes configuration.
*
* @param string[] $config_names
* The configuration names.
*
* @return \Drupal\commerce\Config\ConfigUpdateResult
* The result.
*/
public function delete(array $config_names);
/**
* Loads configuration from active storage.
*
* @param string $config_name
* The configuration name.
*
* @return array|false
* The configuration data, or FALSE if not found.
*/
public function loadFromActive($config_name);
/**
* Loads configuration from extension storage.
*
* Extension storage represents the config/install or config/optional
* directory of a module, theme, or install profile.
*
* @param string $config_name
* The configuration name.
*
* @return array|false
* The configuration data, or FALSE if not found.
*/
public function loadFromExtension($config_name);
/**
* Checks whether the configuration was modified since the initial import.
*
* @param array $config
* The configuration data.
*
* @return bool
* TRUE if the configuration was modified, FALSE otherwise.
*/
public function isModified(array $config);
}
<?php
namespace Drupal\commerce\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the ConfigUpdater class.
*
* @group commerce
*/
class ConfigUpdaterTest extends WebTestBase {
public static $modules = ['commerce_update_test'];
/**
* The config updater service.
*
* @var \Drupal\commerce\Config\ConfigUpdaterInterface
*/
protected $configUpdater;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->configUpdater = \Drupal::service('commerce.config_updater');
}
/**
* Tests loading configuration 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 configuration from extension storage.
*/
public function testLoadFromExtension() {
$config_name = 'views.view.commerce_stores';
$data = $this->configUpdater->loadFromExtension($config_name);
$this->assertEqual($data['id'], 'commerce_stores');
}
/**
* Tests checking whether configuration was modified.
*/
public function testIsModified() {
$config_name = 'commerce_store.commerce_store_type.testing';
$config = $this->configUpdater->loadFromActive($config_name);
$this->assertFalse($this->configUpdater->isModified($config));
/** @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();
$config = $this->configUpdater->loadFromActive($config_name);
$this->assertTrue($this->configUpdater->isModified($config));
}
/**
* Tests importing configuration.
*/
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");
}
/**
* Tests reverting configuration.
*/
public function testRevert() {
$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 could not be reverted because it was modified by the user");
$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 deleting configuration.
*/
public function testDelete() {
$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");
}
}
name: 'Commerce update test'
type: module
description: 'Module for testing extension updates to configuration.'
package: Testing
core: 8.x
dependencies:
- commerce_store
langcode: en
status: true
dependencies: { }
id: testing
label: Testing
description: null
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment