You are here

ConfigSorter.php in Configuration Split 2.0.x

File

src/Config/ConfigSorter.php
View source
<?php

namespace Drupal\config_split\Config;

use Drupal\Core\Config\StorableConfigBase;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;

/**
 * The config sorter service core should have had.
 *
 * @internal This is not an API, anything here might change without notice. Use config_normalizer 2.x instead.
 */
class ConfigSorter {

  /**
   * The typed config manager to get the schema from.
   *
   * @var \Drupal\Core\Config\TypedConfigManagerInterface
   */
  protected $typedConfigManager;

  /**
   * The active storage to help with the sorting.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $active;

  /**
   * ConfigCaster constructor.
   *
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
   *   The typed config manager to look up the schema.
   * @param \Drupal\Core\Config\StorageInterface $active
   *   The active storage to help with the sorting.
   */
  public function __construct(TypedConfigManagerInterface $typedConfigManager, StorageInterface $active) {
    $this->typedConfigManager = $typedConfigManager;
    $this->active = $active;
  }

  /**
   * Cast and sort the config data in a normalised way depending on its schema.
   *
   * @param string $name
   *   The config name.
   * @param array $data
   *   The config data.
   *
   * @return array
   *   The cast and sorted data.
   */
  public function sort(string $name, array $data) : array {

    // The sorter is an object extending from the core config class but doing
    // the casting and sorting only.
    // This is an anonymous class so that we are sure each object gets used only
    // once and nobody uses it for anything else. We extend the core class so
    // that we can access the methods and inherit the improvements made to it.
    $sorter = new class($this->typedConfigManager) extends StorableConfigBase {

      /**
       * Sort the config.
       *
       * @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)) {

          // Once https://www.drupal.org/project/drupal/issues/2852557 is fixed
          // we do just: $this->data = $this->castValue(NULL, $this->data);.
          foreach ($this->data as $key => $value) {
            $this->data[$key] = $this
              ->castValue($key, $value);
          }
        }
        else {
          foreach ($this->data as $key => $value) {
            $this
              ->validateValue($key, $value);
          }
        }

        // This should now produce the same data as if the config object had
        // been saved and loaded. So we can return it.
        return $this->data;
      }

      /**
       * 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.
    $data = $sorter
      ->anonymousSort($name, $data);

    // Unfortunately Drupal core does not let one easily sort config.
    // Only when entities are saved some order is assured, for config objects
    // there is no sorting and both of these things can not easily be addressed.
    // @see https://www.drupal.org/project/drupal/issues/3230826
    if ($this->active
      ->exists($name)) {

      // Since we are only concerned about sorting to prevent unnecessary diffs
      // we don't sort when the config doesn't exist in the active storage.
      $data = $this
        ->sortDeep($data, $this->active
        ->read($name));
    }
    return $data;
  }

  /**
   * Sort one array with the sorting order of another.
   *
   * @param array $config
   *   The array to sort.
   * @param array $model
   *   The array to get the sorting order from.
   *
   * @return array
   *   The sorted array.
   */
  protected function sortDeep(array $config, array $model) : array {
    if ($config === $model) {

      // Shortcut.
      return $config;
    }
    $sorted = [];
    $common = array_intersect_key($model, $config);
    $unique = array_diff_key($config, $model);
    foreach ($common as $key => $modelValue) {
      $value = $config[$key];

      // We maybe need to differentiate between mappings and sequences, use the
      // config schema and all. But as long as core doesn't give us any help we
      // just sort in the most crude way to get the job done.
      if (is_array($modelValue) && is_array($value) && !empty($value)) {

        // Recurse into nested values.
        $value = $this
          ->sortDeep($value, $modelValue);
      }

      // Fill the $sorted array in the same order as the model.
      $sorted[$key] = $value;
    }
    foreach ($unique as $key => $value) {

      // The values that do not exist in the model do not need to be sorted.
      $sorted[$key] = $value;
    }
    return $sorted;
  }

}

Classes

Namesort descending Description
ConfigSorter The config sorter service core should have had.