Skip to content

Commit

Permalink
WIP ConfigUpdater service
Browse files Browse the repository at this point in the history
Because hook_update_N + config == hard.
  • Loading branch information
mglaman committed Mar 4, 2016
1 parent 3b0a9ab commit 2a03c19
Show file tree
Hide file tree
Showing 9 changed files with 698 additions and 0 deletions.
4 changes: 4 additions & 0 deletions commerce.services.yml
Expand Up @@ -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']
Expand Down
279 changes: 279 additions & 0 deletions src/ConfigUpdater.php
@@ -0,0 +1,279 @@
<?php

namespace Drupal\commerce;


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;

/**
* Class ConfigUpdater.
*/
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 ConfigReverter.
*
* @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) {
// 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;
}

}
102 changes: 102 additions & 0 deletions src/ConfigUpdaterInterface.php
@@ -0,0 +1,102 @@
<?php

namespace Drupal\commerce;

/**
* Performs configuration updates on behalf of an extension.
*
* Service to provide methods for extensions to import, revert, or delete
* configuration once they have been installed. Intended to be run from
* hook_post_update_NAME(), since this involves the modification of entities.
*
* @see hook_post_update_NAME()
*/
interface ConfigUpdaterInterface {

/**
* Imports configuration from extension storage to active storage.
*
* This action triggers a ConfigRevertInterface::IMPORT event if the
* configuration could be imported.
*
* @param string[] $config_names
* An array of configuration names.
*
* @return \Drupal\commerce\ConfigUpdaterResultInterface
* Object containing failed and succeeded configuration object update.
* May also throw exceptions if there is a problem during saving the
* configuration.
*/
public function import(array $config_names);

/**
* Reverts configuration to the value from extension storage.
*
* This action triggers a ConfigRevertInterface::REVERT event.
*
* @param string[] $config_names
* An array of configuration names.
* @param bool $skip_modified
* Whether to skip modified configuration, defaults to TRUE. If this is
* set to false, it will revert active configuration that has been
* modified to the configuration values in the extension.
*
* @return \Drupal\commerce\ConfigUpdaterResultInterface
* Object containing failed and succeeded configuration object update.
* May also throw exceptions if there is a problem during saving the
* configuration.
*/
public function revert(array $config_names, $skip_modified = TRUE);

/**
* Deletes a configuration item.
*
* This action triggers a ConfigDeleteInterface::DELETE event.
*
* @param string[] $config_names
* An array of configuration names.
*
* @return \Drupal\commerce\ConfigUpdaterResultInterface
* Object containing failed and succeeded configuration object update.
* May also throw exceptions if there is a problem during saving the
* configuration.
*/
public function delete(array $config_names);

/**
* Gets the current active value of configuration.
*
* @param string $config_name
* The configuration item's full name.
*
* @return array
* The configuration value.
*/
public function loadFromActive($config_name);

/**
* Gets the extension storage value of configuration.
*
* This is the value from a file in the config/install or config/optional
* directory of a module, theme, or install profile.
*
* @param string $config_name
* The configuration item's full name.
*
* @return array|false
* The configuration value, or FALSE if it could not be located.
*/
public function loadFromExtension($config_name);

/**
* Compares a config item's has been modified since its installation.
*
* @param string $config_name
* The configuration item's full name.
*
* @return bool
* Returns TRUE is modified, FALSE if original configuration.
*/
public function isModified($config_name);

}

0 comments on commit 2a03c19

Please sign in to comment.