From aa68fabc1ae11c3827fe48e0a5b6e73388ec0290 Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 21:41:38 -0700
Subject: [PATCH 01/12] #3230397 Normalize the way core does.

---
 config_normalizer.services.yml                |   4 +
 .../ConfigNormalizer/ConfigNormalizerSort.php |  81 +++---
 ...tity_view_display.node.article.default.yml |   0
 .../config/install/views.view.recipes.yml     | 275 ++++++++++++++++++
 tests/src/Kernel/ConfigNormalizerSortTest.php | 137 +++++++++
 5 files changed, 458 insertions(+), 39 deletions(-)
 create mode 100644 tests/fixtures/config/install/core.entity_view_display.node.article.default.yml
 create mode 100644 tests/fixtures/config/install/views.view.recipes.yml
 create mode 100644 tests/src/Kernel/ConfigNormalizerSortTest.php

diff --git a/config_normalizer.services.yml b/config_normalizer.services.yml
index dca7b34..3480b01 100644
--- a/config_normalizer.services.yml
+++ b/config_normalizer.services.yml
@@ -2,3 +2,7 @@ services:
   plugin.manager.config_normalizer:
     class: Drupal\config_normalizer\Plugin\ConfigNormalizerManager
     parent: default_plugin_manager
+  config_normalizer.config_sorter:
+    class: Drupal\config_normalizer\Config\ConfigSorter
+    arguments:
+      - '@config.typed'
diff --git a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
index e627454..e6b5cf7 100644
--- a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
+++ b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
@@ -3,12 +3,12 @@
 namespace Drupal\config_normalizer\Plugin\ConfigNormalizer;
 
 use Drupal\config_normalizer\Plugin\ConfigNormalizerBase;
+use Drupal\Core\Config\StorableConfigBase;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\Yaml\Inline;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Recursively sorts a configuration array.
- *
  * @ConfigNormalizer(
  *   id = "sort",
  *   label = @Translation("sort"),
@@ -18,47 +18,50 @@ use Symfony\Component\Yaml\Inline;
  */
 class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFactoryPluginInterface {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function normalize($name, array &$data, array $context) {
-    // Only sort if the normalization mode is default.
-    if ($this->isDefaultModeContext($context)) {
-      // Recursively normalize and return.
-      $data = $this->normalizeArray($data);
-    }
+  protected TypedConfigManagerInterface $typedConfigManager;
+
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, TypedConfigManagerInterface $typed_config_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->typedConfigManager = $typed_config_manager;
+  }
+
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('config.typed')
+    );
   }
 
-  /**
-   * Recursively sorts an array by key.
-   *
-   * @param array $array
-   *   An array to normalize.
-   *
-   * @return array
-   *   An array that is sorted by key, at each level of the array, with empty
-   *   arrays removed.
-   */
-  protected function normalizeArray(array $array) {
-    foreach ($array as $key => $value) {
-      if (is_array($value)) {
-        $new = $this->normalizeArray($value);
-        if (count($new)) {
-          $array[$key] = $new;
+  public function normalize($name, array &$data, array $context = []) {
+    $sorter = new class($this->typedConfigManager) extends StorableConfigBase {
+      public function anonymousSort(string $name, array $data): array {
+        self::validateName($name);
+        $this->validateKeys($data);
+        $this->setName($name)->initWithData($data);
+
+        if ($this->typedConfigManager->hasConfigSchema($this->name)) {
+          $this->data = $this->castValue(NULL, $this->data);
+        }
+        else {
+          foreach ($this->data as $key => $value) {
+            $this->validateValue($key, $value);
+          }
         }
+        return $this->data;
       }
-    }
 
-    // If the array is associative, sort by key.
-    if (Inline::isHash($array)) {
-      ksort($array);
-    }
-    // Otherwise, sort by value.
-    else {
-      sort($array);
-    }
+      public function save($has_trusted_data = FALSE) {
+        throw new \LogicException('Save not supported.');
+      }
 
-    return $array;
-  }
+      public function delete() {
+        throw new \LogicException('Delete not supported.');
+      }
+    };
 
+    $data = $sorter->anonymousSort($name, $data);
+  }
 }
+
diff --git a/tests/fixtures/config/install/core.entity_view_display.node.article.default.yml b/tests/fixtures/config/install/core.entity_view_display.node.article.default.yml
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/config/install/views.view.recipes.yml b/tests/fixtures/config/install/views.view.recipes.yml
new file mode 100644
index 0000000..3a7aae6
--- /dev/null
+++ b/tests/fixtures/config/install/views.view.recipes.yml
@@ -0,0 +1,275 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_view_mode.node.card
+    - node.type.recipe
+    - system.menu.main
+  module:
+    - node
+    - user
+id: recipes
+label: Recipes
+module: views
+description: 'Recipes listing'
+tag: ''
+base_table: node_field_data
+base_field: nid
+display:
+  default:
+    id: default
+    display_title: Default
+    display_plugin: default
+    position: 0
+    display_options:
+      title: Recipes
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: title
+          plugin_id: field
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            trim: false
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      pager:
+        type: mini
+        options:
+          offset: 0
+          pagination_heading_level: h4
+          items_per_page: 12
+          total_pages: null
+          id: 0
+          tags:
+            next: ››
+            previous: ‹‹
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: perm
+        options:
+          perm: 'access content'
+      cache:
+        type: tag
+        options: {  }
+      empty: {  }
+      sorts:
+        created:
+          id: created
+          table: node_field_data
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: created
+          plugin_id: date
+          order: DESC
+          expose:
+            label: ''
+            field_identifier: created
+          exposed: false
+          granularity: second
+        nid:
+          id: nid
+          table: node_field_data
+          field: nid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: nid
+          plugin_id: standard
+          order: ASC
+          expose:
+            label: ''
+            field_identifier: nid
+          exposed: false
+      arguments: {  }
+      filters:
+        status:
+          id: status
+          table: node_field_data
+          field: status
+          entity_type: node
+          entity_field: status
+          plugin_id: boolean
+          value: '1'
+          group: 1
+          expose:
+            operator: ''
+            operator_limit_selection: false
+            operator_list: {  }
+        type:
+          id: type
+          table: node_field_data
+          field: type
+          entity_type: node
+          entity_field: type
+          plugin_id: bundle
+          value:
+            recipe: recipe
+          group: 1
+          expose:
+            operator_limit_selection: false
+            operator_list: {  }
+        default_langcode:
+          id: default_langcode
+          table: node_field_data
+          field: default_langcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: default_langcode
+          plugin_id: boolean
+          operator: '='
+          value: '1'
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: ''
+            description: ''
+            use_operator: false
+            operator: ''
+            operator_limit_selection: false
+            operator_list: {  }
+            identifier: ''
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+      filter_groups:
+        operator: AND
+        groups:
+          1: AND
+      style:
+        type: grid_responsive
+        options:
+          uses_fields: false
+          columns: 4
+          cell_min_width: 240
+          grid_gutter: 14
+          alignment: horizontal
+      row:
+        type: 'entity:node'
+        options:
+          relationship: none
+          view_mode: card
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships: {  }
+      css_class: ''
+      header: {  }
+      footer: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
+  page_1:
+    id: page_1
+    display_title: Page
+    display_plugin: page
+    position: 1
+    display_options:
+      rendering_language: '***LANGUAGE_language_content***'
+      display_extenders: {  }
+      path: recipes
+      menu:
+        type: normal
+        title: Recipes
+        description: ''
+        weight: 30
+        expanded: false
+        menu_name: main
+        parent: ''
+        context: '0'
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_interface'
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
diff --git a/tests/src/Kernel/ConfigNormalizerSortTest.php b/tests/src/Kernel/ConfigNormalizerSortTest.php
new file mode 100644
index 0000000..fe97380
--- /dev/null
+++ b/tests/src/Kernel/ConfigNormalizerSortTest.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\Tests\config_normalizer\Kernel;
+
+use Symfony\Component\Yaml\Yaml;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests sorting configuration.
+ *
+ * @group config_normalizer
+ */
+class ConfigNormalizerSortTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'system',
+    'user',
+    'node',
+    'views',
+    'field',
+    'filter',
+    'text',
+    'media',
+    'image',
+    'file',
+    'menu_link_content',
+    'block',
+    'block_content',
+    'layout_builder',
+    'config_normalizer',
+  ];
+
+  /**
+   * @var \Drupal\config_normalizer\Config\ConfigSorter
+   */
+  protected $configSorter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('media');
+    $this->installEntitySchema('file');
+    $this->installEntitySchema('block_content');
+
+    $this->installConfig([
+      'system', 'node', 'views', 'image', 'media', 'text',
+      'user', 'filter', 'block_content', 'layout_builder',
+    ]);
+
+    // Explicitly copy fixtures to sync storage:
+    foreach ([
+      'core.entity_view_display.node.article.default',
+      'views.view.recipes',
+    ] as $config_name) {
+      $this->copyFixtureToSyncStorage($config_name);
+      $data = $this->container->get('config.storage.sync')->read($config_name);
+      $this->config($config_name)->setData($data)->save(TRUE);
+    }
+    // Inject sorter properly after container rebuild:
+    $this->configSorter = $this->container->get('config_normalizer.config_sorter');
+
+    // Ensure container is properly built (critical step):
+    $this->container->get('kernel')->rebuildContainer();
+
+  }
+  /**
+   * Tests that configuration is normalized in a consistent manner.
+   *
+   * This ensures that configuration arrays are always sorted the same way,
+   * regardless of the original order in which they were defined or exported.
+   * The test loads known configuration from the Umami profile, shuffles its
+   * keys, and then verifies that sorting restores it to the expected order.
+   */
+  public function testUmamiConfigurationSorting(): void {
+    $config_names = [
+      'views.view.recipes',
+      'core.entity_view_display.node.article.default',
+    ];
+
+    foreach ($config_names as $config_name) {
+      // Retrieve the original configuration data.
+      $original = $this->config($config_name)->getRawData();
+      // Apply the sorting mechanism to the original data.
+      $sorted_original = $this->configSorter->sort($config_name, $original);
+
+      // Shuffle the configuration data to simulate disorder.
+      $shuffled = $this->shuffleAssocArray($original);
+      // Apply the sorting mechanism to the shuffled data.
+      $sorted_shuffled = $this->configSorter->sort($config_name, $shuffled);
+
+      // Assert that sorting restores the original, expected order.
+      $this->assertSame(
+        $sorted_original,
+        $sorted_shuffled,
+        sprintf('Normalized config matches for "%s".', $config_name)
+      );
+    }
+  }
+
+  /**
+   * Helper to shuffle associative arrays.
+   */
+  protected function shuffleAssocArray(array $array): array {
+    $keys = array_keys($array);
+    shuffle($keys);
+    $shuffled = [];
+    foreach ($keys as $key) {
+      $shuffled[$key] = is_array($array[$key]) ? $this->shuffleAssocArray($array[$key]) : $array[$key];
+    }
+    return $shuffled;
+  }
+
+  /**
+   * Copy fixture to sync storage.
+   */
+  protected function copyFixtureToSyncStorage(string $config_name): void {
+    $module_path = $this->container->get('extension.list.module')->getPath('config_normalizer');
+    $source = DRUPAL_ROOT . "/$module_path/tests/fixtures/config/install/$config_name.yml";
+
+    if (!file_exists($source)) {
+      $this->fail("Fixture file not found: $source");
+    }
+
+    $data = Yaml::parseFile($source);
+    $sync_storage = $this->container->get('config.storage.sync');
+    $sync_storage->write($config_name, $data);
+  }
+
+}
-- 
GitLab


From de8296e5eaedb6da50b736342b8adc5857235f1b Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 21:43:40 -0700
Subject: [PATCH 02/12] add new code.

---
 src/Config/ConfigSorter.php                   |  0
 .../ConfigNormalizer/ConfigNormalizerSort.php | 74 +++++++++++--------
 2 files changed, 42 insertions(+), 32 deletions(-)
 create mode 100644 src/Config/ConfigSorter.php

diff --git a/src/Config/ConfigSorter.php b/src/Config/ConfigSorter.php
new file mode 100644
index 0000000..e69de29
diff --git a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
index e6b5cf7..df5778d 100644
--- a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
+++ b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
@@ -3,7 +3,7 @@
 namespace Drupal\config_normalizer\Plugin\ConfigNormalizer;
 
 use Drupal\config_normalizer\Plugin\ConfigNormalizerBase;
-use Drupal\Core\Config\StorableConfigBase;
+use Drupal\config_normalizer\Config\ConfigSorter;
 use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -18,50 +18,60 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  */
 class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFactoryPluginInterface {
 
+  /**
+   * @var \Drupal\Core\Config\TypedConfigManagerInterface
+   */
   protected TypedConfigManagerInterface $typedConfigManager;
 
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, TypedConfigManagerInterface $typed_config_manager) {
+  /**
+   * @var \Drupal\config_normalizer\Config\ConfigSorter
+   */
+  protected ConfigSorter $configSorter;
+
+  /**
+   * ConfigNormalizerSort constructor.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
+   *   The typed config manager service.
+   * @param \Drupal\config_normalizer\Config\ConfigSorter $config_sorter
+   *   *   The config sorter service.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    TypedConfigManagerInterface $typed_config_manager,
+    ConfigSorter $config_sorter,
+  ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->typedConfigManager = $typed_config_manager;
+    $this->configSorter = $config_sorter;
   }
 
+  /**
+   *
+   */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     return new static(
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('config.typed')
+      $container->get('config.typed'),
+      $container->get('config_normalizer.config_sorter')
     );
   }
 
-  public function normalize($name, array &$data, array $context = []) {
-    $sorter = new class($this->typedConfigManager) extends StorableConfigBase {
-      public function anonymousSort(string $name, array $data): array {
-        self::validateName($name);
-        $this->validateKeys($data);
-        $this->setName($name)->initWithData($data);
-
-        if ($this->typedConfigManager->hasConfigSchema($this->name)) {
-          $this->data = $this->castValue(NULL, $this->data);
-        }
-        else {
-          foreach ($this->data as $key => $value) {
-            $this->validateValue($key, $value);
-          }
-        }
-        return $this->data;
-      }
-
-      public function save($has_trusted_data = FALSE) {
-        throw new \LogicException('Save not supported.');
-      }
-
-      public function delete() {
-        throw new \LogicException('Delete not supported.');
-      }
-    };
-
-    $data = $sorter->anonymousSort($name, $data);
+  /**
+   *
+   */
+  public function normalize($name, array &$data, array $context = []): array {
+    $data = $this->configSorter->sort($name, $data);
   }
-}
 
+}
-- 
GitLab


From 1faafa608d30e5f166b1fccfb64c14fe61db95be Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 21:44:25 -0700
Subject: [PATCH 03/12] add new code.

---
 src/Config/ConfigSorter.php | 78 +++++++++++++++++++++++++++++++++++++
 1 file changed, 78 insertions(+)

diff --git a/src/Config/ConfigSorter.php b/src/Config/ConfigSorter.php
index e69de29..cc1a740 100644
--- a/src/Config/ConfigSorter.php
+++ b/src/Config/ConfigSorter.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\config_normalizer\Config;
+
+use Drupal\Core\Config\StorableConfigBase;
+use Drupal\Core\Config\TypedConfigManagerInterface;
+
+/**
+ * Provides schema-based sorting of configuration arrays.
+ *
+ * @internal Uses core's internal sorting logic introduced by #2852557.
+ */
+class ConfigSorter {
+
+  protected TypedConfigManagerInterface $typedConfigManager;
+
+  public function __construct(TypedConfigManagerInterface $typedConfigManager) {
+    $this->typedConfigManager = $typedConfigManager;
+  }
+
+  /**
+   *
+   */
+  public function sort(string $name, array $data): array {
+    $sorter = new class($this->typedConfigManager) extends StorableConfigBase {
+
+      public function __construct(TypedConfigManagerInterface $typedConfigManager) {
+        $this->typedConfigManager = $typedConfigManager;
+      }
+
+      /**
+       *
+       */
+      protected function getSchemaWrapper() {
+        if (!isset($this->schemaWrapper)) {
+          $this->schemaWrapper = $this->typedConfigManager->createFromNameAndData($this->name, $this->data);
+        }
+        return $this->schemaWrapper;
+      }
+
+      /**
+       *
+       */
+      public function anonymousSort(string $name, array $data): array {
+        $this->setName($name)->initWithData($data);
+
+        if ($this->typedConfigManager->hasConfigSchema($name)) {
+          $this->data = $this->castValue(NULL, $this->data);
+        }
+        else {
+          foreach ($this->data as $key => $value) {
+            $this->validateValue($key, $value);
+          }
+        }
+
+        return $this->data;
+      }
+
+      /**
+       *
+       */
+      public function save($has_trusted_data = FALSE) {
+        throw new \LogicException('Saving is not supported.');
+      }
+
+      /**
+       *
+       */
+      public function delete() {
+        throw new \LogicException('Deletion is not supported.');
+      }
+
+    };
+
+    return $sorter->anonymousSort($name, $data);
+  }
+
+}
-- 
GitLab


From 813c4ccbd93654ce98ce2fe39a430f1c6061be5b Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 21:50:18 -0700
Subject: [PATCH 04/12] Update code comments'

---
 src/Config/ConfigSorter.php | 73 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 72 insertions(+), 1 deletion(-)

diff --git a/src/Config/ConfigSorter.php b/src/Config/ConfigSorter.php
index cc1a740..e79c500 100644
--- a/src/Config/ConfigSorter.php
+++ b/src/Config/ConfigSorter.php
@@ -8,63 +8,133 @@ use Drupal\Core\Config\TypedConfigManagerInterface;
 /**
  * Provides schema-based sorting of configuration arrays.
  *
- * @internal Uses core's internal sorting logic introduced by #2852557.
+ * This class leverages Drupal's internal sorting logic introduced in
+ * https://www.drupal.org/project/drupal/issues/2852557 to sort configuration
+ * data based on its schema.
  */
 class ConfigSorter {
 
+  /**
+   * The typed configuration manager service.
+   *
+   * @var \Drupal\Core\Config\TypedConfigManagerInterface
+   */
   protected TypedConfigManagerInterface $typedConfigManager;
 
+  /**
+   * Constructs a ConfigSorter object.
+   *
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed configuration manager service to manage configuration schemas.
+   */
   public function __construct(TypedConfigManagerInterface $typedConfigManager) {
     $this->typedConfigManager = $typedConfigManager;
   }
 
   /**
+   * Sorts a configuration array based on its schema.
+   *
+   * This method creates an anonymous class that extends StorableConfigBase
+   * and uses the internal sorting mechanism to sort the provided configuration
+   * data according to the schema for the given configuration name.
    *
+   * @param string $name
+   *   The name of the configuration to be sorted.
+   * @param array $data
+   *   The configuration data to be sorted.
+   *
+   * @return array
+   *   The sorted configuration data.
    */
   public function sort(string $name, array $data): array {
+    // Create an anonymous class that extends StorableConfigBase
+    // for sorting the configuration data based on its schema.
     $sorter = new class($this->typedConfigManager) extends StorableConfigBase {
 
+      /**
+       * Constructs the anonymous class instance.
+       *
+       * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+       *   The typed configuration manager service.
+       */
       public function __construct(TypedConfigManagerInterface $typedConfigManager) {
         $this->typedConfigManager = $typedConfigManager;
       }
 
       /**
+       * Retrieves the schema wrapper for the configuration data.
        *
+       * This method creates and returns the schema wrapper for the given
+       * configuration data using the TypedConfigManager service.
+       *
+       * @return \Drupal\Core\Config\ConfigSchemaInterface
+       *   The schema wrapper for the configuration data.
        */
       protected function getSchemaWrapper() {
         if (!isset($this->schemaWrapper)) {
+          // Create the schema wrapper if it hasn't been created already.
           $this->schemaWrapper = $this->typedConfigManager->createFromNameAndData($this->name, $this->data);
         }
         return $this->schemaWrapper;
       }
 
       /**
+       * Sorts the configuration data based on the schema.
+       *
+       * This method checks if the configuration has a schema, casts the value if
+       * necessary, and validates the data values before returning the sorted data.
        *
+       * @param string $name
+       *   The name of the configuration to be sorted.
+       * @param array $data
+       *   The configuration data to be sorted.
+       *
+       * @return array
+       *   The sorted configuration data.
        */
       public function anonymousSort(string $name, array $data): array {
+        // Set the name and initialize the data for sorting.
         $this->setName($name)->initWithData($data);
 
+        // If a schema exists, cast the value according to the schema.
         if ($this->typedConfigManager->hasConfigSchema($name)) {
           $this->data = $this->castValue(NULL, $this->data);
         }
         else {
+          // If no schema, validate the data values.
           foreach ($this->data as $key => $value) {
             $this->validateValue($key, $value);
           }
         }
 
+        // Return the sorted configuration data.
         return $this->data;
       }
 
       /**
+       * Throws a LogicException when attempting to save the configuration.
+       *
+       * This method is not supported in the current class and will always throw
+       * an exception.
        *
+       * @param bool $has_trusted_data
+       *   Whether the data has been trusted. Not used in this case.
+       *
+       * @throws \LogicException
+       *   Always throws an exception.
        */
       public function save($has_trusted_data = FALSE) {
         throw new \LogicException('Saving is not supported.');
       }
 
       /**
+       * Throws a LogicException when attempting to delete the configuration.
+       *
+       * This method is not supported in the current class and will always throw
+       * an exception.
        *
+       * @throws \LogicException
+       *   Always throws an exception.
        */
       public function delete() {
         throw new \LogicException('Deletion is not supported.');
@@ -72,6 +142,7 @@ class ConfigSorter {
 
     };
 
+    // Return the sorted configuration data.
     return $sorter->anonymousSort($name, $data);
   }
 
-- 
GitLab


From da6aad8ffea49aa7976842700533cd7092cdc129 Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 21:52:49 -0700
Subject: [PATCH 05/12] Update code comments'

---
 ...tity_view_display.node.article.default.yml | 55 +++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/tests/fixtures/config/install/core.entity_view_display.node.article.default.yml b/tests/fixtures/config/install/core.entity_view_display.node.article.default.yml
index e69de29..f2665c8 100644
--- a/tests/fixtures/config/install/core.entity_view_display.node.article.default.yml
+++ b/tests/fixtures/config/install/core.entity_view_display.node.article.default.yml
@@ -0,0 +1,55 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.article.field_body
+    - field.field.node.article.field_media_image
+    - field.field.node.article.field_tags
+    - field.field.node.article.layout_builder__layout
+    - node.type.article
+  module:
+    - layout_builder
+    - text
+    - user
+third_party_settings:
+  layout_builder:
+    enabled: false
+    allow_custom: false
+id: node.article.default
+targetEntityType: node
+bundle: article
+mode: default
+content:
+  field_body:
+    type: text_default
+    label: hidden
+    settings: {  }
+    third_party_settings: {  }
+    weight: 3
+    region: content
+  field_media_image:
+    type: entity_reference_entity_view
+    label: hidden
+    settings:
+      view_mode: responsive_3x2
+      link: false
+    third_party_settings: {  }
+    weight: 2
+    region: content
+  field_tags:
+    type: entity_reference_label
+    label: above
+    settings:
+      link: true
+    third_party_settings: {  }
+    weight: 0
+    region: content
+  links:
+    settings: {  }
+    third_party_settings: {  }
+    weight: 4
+    region: content
+hidden:
+  content_moderation_control: true
+  langcode: true
+  layout_builder__layout: true
-- 
GitLab


From e2f0ba3b0f15b29fd8787fde7b995e994714136a Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 22:00:06 -0700
Subject: [PATCH 06/12] Code quality updates

---
 src/Config/ConfigSorter.php                   |  5 ++--
 .../ConfigNormalizer/ConfigNormalizerSort.php | 26 ++++++++++++++++---
 2 files changed, 26 insertions(+), 5 deletions(-)

diff --git a/src/Config/ConfigSorter.php b/src/Config/ConfigSorter.php
index e79c500..b767219 100644
--- a/src/Config/ConfigSorter.php
+++ b/src/Config/ConfigSorter.php
@@ -81,8 +81,9 @@ class ConfigSorter {
       /**
        * Sorts the configuration data based on the schema.
        *
-       * This method checks if the configuration has a schema, casts the value if
-       * necessary, and validates the data values before returning the sorted data.
+       * This method checks if the configuration has a schema, casts the value
+       * if necessary, and validates the data values before returning
+       * the sorted data.
        *
        * @param string $name
        *   The name of the configuration to be sorted.
diff --git a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
index df5778d..12af75e 100644
--- a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
+++ b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
@@ -9,6 +9,8 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
+ * ConfigNormalizer plugin to recursively sort a configuration array.
+ *
  * @ConfigNormalizer(
  *   id = "sort",
  *   label = @Translation("sort"),
@@ -19,11 +21,15 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFactoryPluginInterface {
 
   /**
+   * The typed configuration manager service.
+   *
    * @var \Drupal\Core\Config\TypedConfigManagerInterface
    */
   protected TypedConfigManagerInterface $typedConfigManager;
 
   /**
+   * The configuration sorter service.
+   *
    * @var \Drupal\config_normalizer\Config\ConfigSorter
    */
   protected ConfigSorter $configSorter;
@@ -40,14 +46,14 @@ class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFact
    * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
    *   The typed config manager service.
    * @param \Drupal\config_normalizer\Config\ConfigSorter $config_sorter
-   *   *   The config sorter service.
+   *   The config sorter service.
    */
   public function __construct(
     array $configuration,
     $plugin_id,
     $plugin_definition,
     TypedConfigManagerInterface $typed_config_manager,
-    ConfigSorter $config_sorter,
+    ConfigSorter $config_sorter
   ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->typedConfigManager = $typed_config_manager;
@@ -67,11 +73,25 @@ class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFact
     );
   }
 
+
   /**
+   * Normalizes the configuration data by sorting it recursively.
+   *
+   * This method is called to sort the configuration data based on its schema.
+   * It uses the ConfigSorter service to perform the sorting operation.
+   *
+   * @param string $name
+   *   The name of the configuration.
+   * @param array &$data
+   *   The configuration data to be normalized (sorted).
+   * @param array $context
+   *   The context in which the normalization takes place.
    *
+   * @return array
+   *   The sorted configuration data.
    */
   public function normalize($name, array &$data, array $context = []): array {
-    $data = $this->configSorter->sort($name, $data);
+    return $this->configSorter->sort($name, $data);
   }
 
 }
-- 
GitLab


From 4ee379d0d8ed6fc19a9e7249bc4650335a088a1b Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 22:06:07 -0700
Subject: [PATCH 07/12] Code quality updates

---
 src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php | 5 ++---
 tests/src/Kernel/ConfigNormalizerSortTest.php        | 3 +++
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
index 12af75e..20fff3e 100644
--- a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
+++ b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
@@ -53,7 +53,7 @@ class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFact
     $plugin_id,
     $plugin_definition,
     TypedConfigManagerInterface $typed_config_manager,
-    ConfigSorter $config_sorter
+    ConfigSorter $config_sorter,
   ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->typedConfigManager = $typed_config_manager;
@@ -61,7 +61,7 @@ class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFact
   }
 
   /**
-   *
+   * Creates an instance of the ConfigNormalizerSort plugin.
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     return new static(
@@ -73,7 +73,6 @@ class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFact
     );
   }
 
-
   /**
    * Normalizes the configuration data by sorting it recursively.
    *
diff --git a/tests/src/Kernel/ConfigNormalizerSortTest.php b/tests/src/Kernel/ConfigNormalizerSortTest.php
index fe97380..a5207b3 100644
--- a/tests/src/Kernel/ConfigNormalizerSortTest.php
+++ b/tests/src/Kernel/ConfigNormalizerSortTest.php
@@ -34,6 +34,8 @@ class ConfigNormalizerSortTest extends KernelTestBase {
   ];
 
   /**
+   * The configuration sorter service.
+   *
    * @var \Drupal\config_normalizer\Config\ConfigSorter
    */
   protected $configSorter;
@@ -71,6 +73,7 @@ class ConfigNormalizerSortTest extends KernelTestBase {
     $this->container->get('kernel')->rebuildContainer();
 
   }
+
   /**
    * Tests that configuration is normalized in a consistent manner.
    *
-- 
GitLab


From eab73a69422f183735167430d598a2b8738831e8 Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 22:07:52 -0700
Subject: [PATCH 08/12] Code quality updates

---
 src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
index 20fff3e..c9960a2 100644
--- a/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
+++ b/src/Plugin/ConfigNormalizer/ConfigNormalizerSort.php
@@ -55,7 +55,12 @@ class ConfigNormalizerSort extends ConfigNormalizerBase implements ContainerFact
     TypedConfigManagerInterface $typed_config_manager,
     ConfigSorter $config_sorter,
   ) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    parent::__construct(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $typed_config_manager,
+    );
     $this->typedConfigManager = $typed_config_manager;
     $this->configSorter = $config_sorter;
   }
-- 
GitLab


From dbed00f58b06a84fb93212a1ab9b402de04cc5af Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 22:50:19 -0700
Subject: [PATCH 09/12] tests

---
 tests/src/Kernel/ConfigNormalizerSortTest.php | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/tests/src/Kernel/ConfigNormalizerSortTest.php b/tests/src/Kernel/ConfigNormalizerSortTest.php
index a5207b3..39f5ea8 100644
--- a/tests/src/Kernel/ConfigNormalizerSortTest.php
+++ b/tests/src/Kernel/ConfigNormalizerSortTest.php
@@ -99,12 +99,27 @@ class ConfigNormalizerSortTest extends KernelTestBase {
       // Apply the sorting mechanism to the shuffled data.
       $sorted_shuffled = $this->configSorter->sort($config_name, $shuffled);
 
+      // Assert shuffled data is not equal to the original.
+      $this->assertNotEqual(
+        $sorted_original,
+        $sorted_shuffled,
+        sprintf('Normalized config should not match for "%s".', $config_name)
+      );
+
       // Assert that sorting restores the original, expected order.
+      $this->assertEqual(
+        $sorted_original,
+        $sorted_shuffled,
+        sprintf('Normalized config should match for "%s".', $config_name)
+      );
+
+      // assertSame is more strict than assertEqual, ensuring both type and value match.
       $this->assertSame(
         $sorted_original,
         $sorted_shuffled,
-        sprintf('Normalized config matches for "%s".', $config_name)
+        sprintf('Normalized config should match for "%s".', $config_name)
       );
+
     }
   }
 
-- 
GitLab


From 57b24b2362294ac249caa3a62e89074b6356a21b Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 22:52:21 -0700
Subject: [PATCH 10/12] tests

---
 tests/src/Kernel/ConfigNormalizerSortTest.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/src/Kernel/ConfigNormalizerSortTest.php b/tests/src/Kernel/ConfigNormalizerSortTest.php
index 39f5ea8..6c59b0b 100644
--- a/tests/src/Kernel/ConfigNormalizerSortTest.php
+++ b/tests/src/Kernel/ConfigNormalizerSortTest.php
@@ -100,14 +100,14 @@ class ConfigNormalizerSortTest extends KernelTestBase {
       $sorted_shuffled = $this->configSorter->sort($config_name, $shuffled);
 
       // Assert shuffled data is not equal to the original.
-      $this->assertNotEqual(
+      $this->assertNotEquals(
         $sorted_original,
         $sorted_shuffled,
         sprintf('Normalized config should not match for "%s".', $config_name)
       );
 
       // Assert that sorting restores the original, expected order.
-      $this->assertEqual(
+      $this->assertEquals(
         $sorted_original,
         $sorted_shuffled,
         sprintf('Normalized config should match for "%s".', $config_name)
-- 
GitLab


From 1d80e3be78de8185989698f2c6fc697a64f5eeac Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Tue, 18 Mar 2025 23:03:32 -0700
Subject: [PATCH 11/12] tests

---
 tests/src/Kernel/ConfigNormalizerSortTest.php | 28 +++++++++----------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/tests/src/Kernel/ConfigNormalizerSortTest.php b/tests/src/Kernel/ConfigNormalizerSortTest.php
index 6c59b0b..72e91d8 100644
--- a/tests/src/Kernel/ConfigNormalizerSortTest.php
+++ b/tests/src/Kernel/ConfigNormalizerSortTest.php
@@ -100,25 +100,25 @@ class ConfigNormalizerSortTest extends KernelTestBase {
       $sorted_shuffled = $this->configSorter->sort($config_name, $shuffled);
 
       // Assert shuffled data is not equal to the original.
-      $this->assertNotEquals(
-        $sorted_original,
-        $sorted_shuffled,
-        sprintf('Normalized config should not match for "%s".', $config_name)
-      );
-
-      // Assert that sorting restores the original, expected order.
       $this->assertEquals(
         $sorted_original,
         $sorted_shuffled,
-        sprintf('Normalized config should match for "%s".', $config_name)
+        sprintf('The sorted shuffled version and sorted original of "%s" are not equal.', $config_name)
       );
 
-      // assertSame is more strict than assertEqual, ensuring both type and value match.
-      $this->assertSame(
-        $sorted_original,
-        $sorted_shuffled,
-        sprintf('Normalized config should match for "%s".', $config_name)
-      );
+      // // Assert that sorting restores the original, expected order.
+      // $this->assertEquals(
+      //   $sorted_original,
+      //   $sorted_shuffled,
+      //   sprintf('Normalized config should match for "%s".', $config_name)
+      // );
+
+      // // assertSame is more strict than assertEqual, ensuring both type and value match.
+      // $this->assertSame(
+      //   $sorted_original,
+      //   $sorted_shuffled,
+      //   sprintf('Normalized config should match for "%s".', $config_name)
+      // );
 
     }
   }
-- 
GitLab


From 4064b408b697a25b3cbf86b9d3cb8daca5fd7324 Mon Sep 17 00:00:00 2001
From: Chris Green <chrisgreen@arizona.edu>
Date: Mon, 17 Mar 2025 21:29:27 -0700
Subject: [PATCH 12/12] tests

---
 tests/src/Kernel/ConfigNormalizerSortTest.php | 20 +++++++------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/tests/src/Kernel/ConfigNormalizerSortTest.php b/tests/src/Kernel/ConfigNormalizerSortTest.php
index 72e91d8..2c92134 100644
--- a/tests/src/Kernel/ConfigNormalizerSortTest.php
+++ b/tests/src/Kernel/ConfigNormalizerSortTest.php
@@ -106,19 +106,13 @@ class ConfigNormalizerSortTest extends KernelTestBase {
         sprintf('The sorted shuffled version and sorted original of "%s" are not equal.', $config_name)
       );
 
-      // // Assert that sorting restores the original, expected order.
-      // $this->assertEquals(
-      //   $sorted_original,
-      //   $sorted_shuffled,
-      //   sprintf('Normalized config should match for "%s".', $config_name)
-      // );
-
-      // // assertSame is more strict than assertEqual, ensuring both type and value match.
-      // $this->assertSame(
-      //   $sorted_original,
-      //   $sorted_shuffled,
-      //   sprintf('Normalized config should match for "%s".', $config_name)
-      // );
+      // The assertSame method is more strict than assertEqual,
+      // ensuring both type and value match.
+      $this->assertSame(
+        $sorted_original,
+        $sorted_shuffled,
+          sprintf('The sorted shuffled version and sorted original of "%s" are not the same.', $config_name)
+      );
 
     }
   }
-- 
GitLab