diff --git a/config_normalizer.services.yml b/config_normalizer.services.yml index dca7b3450b6b35d15bf46994b73868484a5d87b8..d7bfc5d62e418852eb7826604d1dd5f2c3f24901 100644 --- a/config_normalizer.services.yml +++ b/config_normalizer.services.yml @@ -1,4 +1,11 @@ services: + + config_normalizer.normalizer: + class: Drupal\config_normalizer\ConfigNormalizer + arguments: + - "@config.typed" + + # deprecated plugin.manager.config_normalizer: class: Drupal\config_normalizer\Plugin\ConfigNormalizerManager parent: default_plugin_manager diff --git a/src/Annotation/ConfigNormalizer.php b/src/Annotation/ConfigNormalizer.php index 4dff6dc848835a37164b85459ba570425c177b30..ef50a2c31e0f456b219a2744973fa6cf1f645045 100644 --- a/src/Annotation/ConfigNormalizer.php +++ b/src/Annotation/ConfigNormalizer.php @@ -10,6 +10,9 @@ use Drupal\Component\Annotation\Plugin; * @see \Drupal\config_normalizer\Plugin\ConfigNormalizerManager * @see plugin_api * + * @deprecated in config_normalizer:2.0.0-alpha1 and is removed from config_normalizer:2.0.0. No replacement. + * @see https://www.drupal.org/project/config_normalizer/issues/3230398 + * * @Annotation */ class ConfigNormalizer extends Plugin { diff --git a/src/Config/NormalizedStorageComparerTrait.php b/src/Config/NormalizedStorageComparerTrait.php index 8151236afe752f21451d7f9880853922a21d84ab..f72204ce4d22393e545ff3b8ce78a1115ca9fd5f 100644 --- a/src/Config/NormalizedStorageComparerTrait.php +++ b/src/Config/NormalizedStorageComparerTrait.php @@ -2,6 +2,7 @@ namespace Drupal\config_normalizer\Config; +use Drupal\config_normalizer\ConfigNormalizerInterface; use Drupal\config_normalizer\Plugin\ConfigNormalizerManager; use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\StorageComparer; @@ -11,25 +12,16 @@ use Drupal\Core\Config\StorageInterface; * Using this trait will add a ::createStorageComparer() method to the class. * * If the class is capable of injecting services from the container, it should - * inject the 'config.manager' service by calling $this->setConfigManager() and - * the 'plugin.manager.config_normalizer' service by calling - * $this->setNormalizerManager(). + * inject the 'config_normalizer.normalizer' service and call setNormalizer() */ trait NormalizedStorageComparerTrait { /** - * The normalizer plugin manager. + * The config normalizer service. * - * @var \Drupal\config_normalizer\Plugin\ConfigNormalizerManager + * @var \Drupal\config_normalizer\ConfigNormalizerInterface */ - protected $normalizerManager; - - /** - * The configuration manager. - * - * @var \Drupal\Core\Config\ConfigManagerInterface - */ - protected $configManager; + protected $configNormalizer; /** * Creates and returns a storage comparer. @@ -39,43 +31,61 @@ trait NormalizedStorageComparerTrait { * @param \Drupal\Core\Config\StorageInterface $target_storage * The target storage. * @param string $mode - * (optional) The normalization mode. + * (optional, deprecated) The normalization mode. * * @return \Drupal\Core\Config\StorageComparer * A storage comparer. */ - protected function createStorageComparer(StorageInterface $source_storage, StorageInterface $target_storage, $mode = NormalizedReadOnlyStorageInterface::DEFAULT_NORMALIZATION_MODE) { - $source_context = [ - 'normalization_mode' => $mode, - 'reference_storage_service' => $target_storage, - ]; - - $target_context = [ - 'normalization_mode' => $mode, - 'reference_storage_service' => $source_storage, - ]; - + protected function createStorageComparer(StorageInterface $source_storage, StorageInterface $target_storage, $mode = NULL) { // Set up a storage comparer using normalized storages. $storage_comparer = new StorageComparer( - new NormalizedReadOnlyStorage($source_storage, $this->getNormalizerManager(), $source_context), - new NormalizedReadOnlyStorage($target_storage, $this->getNormalizerManager(), $target_context), - $this->getConfigManager() + new NormalizedReadOnlyStorage($source_storage, $this->getNormalizer()), + new NormalizedReadOnlyStorage($target_storage, $this->getNormalizer()) ); return $storage_comparer; } + /** + * Gets the normalizer service. + * + * @return \Drupal\config_normalizer\ConfigNormalizerInterface + * The configuration normalizer. + */ + protected function getNormalizer() { + if (!$this->configNormalizer) { + $this->configNormalizer = \Drupal::service('config_normalizer.normalizer'); + } + return $this->configNormalizer; + } + + /** + * Sets the normalizer manager service to use. + * + * @param \Drupal\config_normalizer\ConfigNormalizerInterface $normalizer + * The normalizer service. + * + * @return $this + * + * @deprecated in config_normalizer:2.0.0-alpha1 and is removed from config_normalizer:2.0.0. No replacement. + * @see https://www.drupal.org/project/config_normalizer/issues/3230398 + */ + public function setNormalizer(ConfigNormalizerInterface $normalizer) { + $this->configNormalizer = $normalizer; + return $this; + } + /** * Gets the configuration manager service. * * @return \Drupal\Core\Config\ConfigManagerInterface * The configuration manager. + * + * @deprecated in config_normalizer:2.0.0-alpha1 and is removed from config_normalizer:2.0.0. No replacement. + * @see https://www.drupal.org/project/config_normalizer/issues/3230398 */ protected function getConfigManager() { - if (!$this->configManager) { - $this->configManager = \Drupal::service('config.manager'); - } - return $this->configManager; + return \Drupal::service('config.manager'); } /** @@ -85,9 +95,11 @@ trait NormalizedStorageComparerTrait { * The configuration manager service. * * @return $this + * + * @deprecated in config_normalizer:2.0.0-alpha1 and is removed from config_normalizer:2.0.0. No replacement. + * @see https://www.drupal.org/project/config_normalizer/issues/3230398 */ public function setConfigManager(ConfigManagerInterface $config_manager) { - $this->configManager = $config_manager; return $this; } @@ -96,12 +108,12 @@ trait NormalizedStorageComparerTrait { * * @return \Drupal\config_normalizer\Plugin\ConfigNormalizerManager * The normalizer manager. + * + * @deprecated in config_normalizer:2.0.0-alpha1 and is removed from config_normalizer:2.0.0. No replacement. + * @see https://www.drupal.org/project/config_normalizer/issues/3230398 */ protected function getNormalizerManager() { - if (!$this->normalizerManager) { - $this->normalizerManager = \Drupal::service('plugin.manager.config_normalizer'); - } - return $this->normalizerManager; + return \Drupal::service('plugin.manager.config_normalizer'); } /** @@ -111,9 +123,11 @@ trait NormalizedStorageComparerTrait { * The normalizer manager service. * * @return $this + * + * @deprecated in config_normalizer:2.0.0-alpha1 and is removed from config_normalizer:2.0.0. No replacement. + * @see https://www.drupal.org/project/config_normalizer/issues/3230398 */ public function setNormalizerManager(ConfigNormalizerManager $normalizer_manager) { - $this->normalizerManager = $normalizer_manager; return $this; } diff --git a/src/ConfigNormalizer.php b/src/ConfigNormalizer.php new file mode 100644 index 0000000000000000000000000000000000000000..5ad4a27b92680e19397abf6bc7383d5028d0aec4 --- /dev/null +++ b/src/ConfigNormalizer.php @@ -0,0 +1,203 @@ +<?php + +namespace Drupal\config_normalizer; + +use Drupal\Core\Config\Schema\Ignore; +use Drupal\Core\Config\Schema\Mapping; +use Drupal\Core\Config\Schema\Sequence; +use Drupal\Core\Config\Schema\SequenceDataDefinition; +use Drupal\Core\Config\Schema\Undefined; +use Drupal\Core\Config\StorableConfigBase; +use Drupal\Core\Config\TypedConfigManagerInterface; +use Drupal\Core\Config\UnsupportedDataTypeConfigException; +use Drupal\Core\TypedData\PrimitiveInterface; +use Drupal\Core\TypedData\Type\FloatInterface; +use Drupal\Core\TypedData\Type\IntegerInterface; + +/** + * Class responsible for performing configuration normalization. + */ +class ConfigNormalizer implements ConfigNormalizerInterface { + + /** + * The typed config manager to get the schema from. + * + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected $typedConfigManager; + + /** + * ConfigCaster constructor. + * + * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager + * The typed config manager to look up the schema. + */ + public function __construct(TypedConfigManagerInterface $typedConfigManager) { + $this->typedConfigManager = $typedConfigManager; + } + + /** + * {@inheritdoc} + */ + public function normalize($name, array $data) { + // The sorter is an anonymous class extending from StorableConfigBase. + // We need to do this because the logic for sorting is in a class meant + // for config objects and not for services. + $sorter = new class($this->typedConfigManager) extends StorableConfigBase { + + /** + * Sort the config. + * + * This method is named to make it unlikely that it is overriding a core + * method. + * + * @param string $name + * The config name. + * @param array $data + * The data. + * + * @return array + * The sorted array. + */ + public function anonymousSort(string $name, array $data): array { + // Set the object up. + self::validateName($name); + $this->validateKeys($data); + $this->setName($name)->initWithData($data); + + // This is essentially what \Drupal\Core\Config\Config::save does when + // there is untrusted data before persisting it and dispatching events. + if ($this->typedConfigManager->hasConfigSchema($this->name)) { + // We use the patched version of the method. + $this->data = $this->castValue2852557(NULL, $this->data); + } + else { + foreach ($this->data as $key => $value) { + $this->validateValue($key, $value); + } + } + + // That should be it. + return $this->data; + } + + /** + * Casts the value to correct data type using the configuration schema. + * + * This is the patched version from + * https://www.drupal.org/project/drupal/issues/2852557 + * + * @param string|null $key + * A string that maps to a key within the configuration data. If NULL + * the top level mapping will be processed. + * @param mixed $value + * Value to associate with the key. + * + * @return mixed + * The value cast to the type indicated in the schema. + * + * @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException + * If the value is unsupported in configuration. + */ + protected function castValue2852557($key, $value) { + $element = $this->getSchemaWrapper(); + if ($key !== NULL) { + $element = $element->get($key); + } + + // Do not cast value if it is unknown or defined to be ignored. + if ($element && ($element instanceof Undefined || $element instanceof Ignore)) { + $this->validateValue($key, $value); + return $value; + } + if (is_scalar($value) || $value === NULL) { + if ($element && $element instanceof PrimitiveInterface) { + $empty_value = $value === '' && ($element instanceof IntegerInterface || $element instanceof FloatInterface); + + if ($value === NULL || $empty_value) { + $value = NULL; + } + else { + $value = $element->getCastedValue(); + } + } + } + else { + // Throw exception on any non-scalar or non-array value. + if (!is_array($value)) { + throw new UnsupportedDataTypeConfigException("Invalid data type for config element {$this->getName()}:$key"); + } + // Recurse into any nested keys. + foreach ($value as $nested_value_key => $nested_value) { + $lookup_key = $key ? $key . '.' . $nested_value_key : $nested_value_key; + $value[$nested_value_key] = $this->castValue2852557($lookup_key, $nested_value); + } + + // Only sort maps when we have more than 1 element to sort. + if ($element instanceof Mapping && count($value) > 1) { + $mapping = $element->getDataDefinition()['mapping']; + if (is_array($mapping)) { + // Only sort the keys in $value. + $mapping = array_intersect_key($mapping, $value); + // Sort the array in $value using the mapping definition. + $value = array_replace($mapping, $value); + } + } + + if ($element instanceof Sequence) { + $data_definition = $element->getDataDefinition(); + if ($data_definition instanceof SequenceDataDefinition) { + // Apply any sorting defined on the schema. + switch ($data_definition->getOrderBy()) { + case 'key': + ksort($value); + break; + + case 'value': + // The PHP documentation notes that "Be careful when sorting + // arrays with mixed types values because sort() can produce + // unpredictable results". There is no risk here because + // \Drupal\Core\Config\StorableConfigBase::castValue() has + // already cast all values to the same type using the + // configuration schema. + sort($value); + break; + + } + } + } + } + return $value; + } + + /** + * The constructor for passing the TypedConfigManager. + * + * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager + * The taped config manager. + */ + public function __construct(TypedConfigManagerInterface $typedConfigManager) { + $this->typedConfigManager = $typedConfigManager; + } + + /** + * {@inheritdoc} + */ + public function save($has_trusted_data = FALSE) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function delete() { + throw new \LogicException(); + } + + }; + + // Sort the data using the core class we extended. + return $sorter->anonymousSort($name, $data); + } + +} diff --git a/src/ConfigNormalizerInterface.php b/src/ConfigNormalizerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..7460da29564cc7e4d591f457b9e41763c9cec4de --- /dev/null +++ b/src/ConfigNormalizerInterface.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\config_normalizer; + +/** + * Defines an interface for config item normalizers. + * + * @api This is the main API of this module. + */ +interface ConfigNormalizerInterface { + + /** + * Normalizes config for comparison. + * + * Normalization can help ensure that config from different storages can be + * compared meaningfully. + * + * @param string $name + * The name of a configuration object to normalize. + * @param array $data + * Configuration array to normalize. + * + * @return array + * Normalized configuration array. + */ + public function normalize($name, array $data); + +}