From fa39f06433eebd7f2b52bc48a33a7fba05d6f150 Mon Sep 17 00:00:00 2001
From: xjm <xjm@65776.no-reply.drupal.org>
Date: Sat, 4 Mar 2017 12:42:17 -0600
Subject: [PATCH] Issue #2361539 by alexpott, webflo, AjitS, himanshu-dixit,
 tameeshb, xjm: Config export key order is not predictable for sequences, add
 orderby property to config schema

---
 core/config/schema/core.data_types.schema.yml |  2 +-
 .../Config/Schema/SequenceDataDefinition.php  | 28 ++++++++
 .../Drupal/Core/Config/StorableConfigBase.php | 25 +++++++
 .../schema/config_schema_test.schema.yml      | 38 ++++++++++
 .../Core/Config/ConfigSchemaTest.php          | 70 +++++++++++++++++++
 5 files changed, 162 insertions(+), 1 deletion(-)
 create mode 100644 core/lib/Drupal/Core/Config/Schema/SequenceDataDefinition.php

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 0fbeeedbe039..a0bd01aa2435 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -46,7 +46,7 @@ mapping:
 sequence:
   label: Sequence
   class: '\Drupal\Core\Config\Schema\Sequence'
-  definition_class: '\Drupal\Core\TypedData\ListDataDefinition'
+  definition_class: '\Drupal\Core\Config\Schema\SequenceDataDefinition'
 
 # Simple extended data types:
 
diff --git a/core/lib/Drupal/Core/Config/Schema/SequenceDataDefinition.php b/core/lib/Drupal/Core/Config/Schema/SequenceDataDefinition.php
new file mode 100644
index 000000000000..c317e29e02cf
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Schema/SequenceDataDefinition.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\Config\Schema;
+
+use Drupal\Core\TypedData\ListDataDefinition;
+
+/**
+ * A typed data definition class for defining sequences in configuration.
+ */
+class SequenceDataDefinition extends ListDataDefinition {
+
+  /**
+   * Gets the description of how the sequence should be sorted.
+   *
+   * Only the top level of the array should be sorted. Top-level keys should be
+   * discarded when using 'value' sorting. If the sequence is an associative
+   * array 'key' sorting is recommended, if not 'value' sorting is recommended.
+   *
+   * @return string|null
+   *   May be 'key' (to sort by key), 'value' (to sort by value, discarding
+   *   keys), or NULL (if the schema does not describe how the sequence should
+   *   be sorted).
+   */
+  public function getOrderBy() {
+    return isset($this->definition['orderby']) ? $this->definition['orderby'] : NULL;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php
index 4b227c9efbdc..0751e9f59f2a 100644
--- a/core/lib/Drupal/Core/Config/StorableConfigBase.php
+++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php
@@ -3,6 +3,8 @@
 namespace Drupal\Core\Config;
 
 use Drupal\Core\Config\Schema\Ignore;
+use Drupal\Core\Config\Schema\Sequence;
+use Drupal\Core\Config\Schema\SequenceDataDefinition;
 use Drupal\Core\TypedData\PrimitiveInterface;
 use Drupal\Core\TypedData\Type\FloatInterface;
 use Drupal\Core\TypedData\Type\IntegerInterface;
@@ -210,6 +212,29 @@ protected function castValue($key, $value) {
       foreach ($value as $nested_value_key => $nested_value) {
         $value[$nested_value_key] = $this->castValue($key . '.' . $nested_value_key, $nested_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;
   }
diff --git a/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml b/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml
index c10e2686bd8d..ce894c243959 100644
--- a/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml
+++ b/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml
@@ -297,3 +297,41 @@ test.double_brackets.breed:
   mapping:
     breed:
       type: string
+
+config_schema_test.schema_sequence_sort:
+  type: config_object
+  mapping:
+    keyed_sort:
+      type: sequence
+      orderby: key
+      sequence:
+        - type: string
+    value_sort:
+      type: sequence
+      orderby: value
+      sequence:
+        - type: string
+    no_sort:
+      type: sequence
+      sequence:
+        - type: string
+    complex_sort_value:
+      type: sequence
+      orderby: value
+      sequence:
+        - type: mapping
+          mapping:
+            foo:
+              type: string
+            bar:
+              type: string
+    complex_sort_key:
+      type: sequence
+      orderby: key
+      sequence:
+        - type: mapping
+          mapping:
+            foo:
+              type: string
+            bar:
+              type: string
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
index 84aa27132672..95737d0d87d1 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
@@ -395,6 +395,76 @@ public function testConfigSaveWithSchema() {
     $this->assertIdentical($installed_data, $original_data);
   }
 
+  /**
+   * Tests configuration sequence sorting using schemas.
+   */
+  public function testConfigSaveWithSequenceSorting() {
+    $data = [
+      'keyed_sort' => [
+        'b' => '1',
+        'a' => '2',
+      ],
+      'no_sort' => [
+        'b' => '2',
+        'a' => '1',
+      ],
+    ];
+    // Save config which has a schema that enforces sorting.
+    $this->config('config_schema_test.schema_sequence_sort')
+      ->setData($data)
+      ->save();
+    $this->assertSame(['a' => '2', 'b' => '1'], $this->config('config_schema_test.schema_sequence_sort')->get('keyed_sort'));
+    $this->assertSame(['b' => '2', 'a' => '1'], $this->config('config_schema_test.schema_sequence_sort')->get('no_sort'));
+
+    $data = [
+      'value_sort' => ['b', 'a'],
+      'no_sort' => ['b', 'a'],
+    ];
+    // Save config which has a schema that enforces sorting.
+    $this->config('config_schema_test.schema_sequence_sort')
+      ->setData($data)
+      ->save();
+
+    $this->assertSame(['a', 'b'], $this->config('config_schema_test.schema_sequence_sort')->get('value_sort'));
+    $this->assertSame(['b', 'a'], $this->config('config_schema_test.schema_sequence_sort')->get('no_sort'));
+
+    // Value sort does not preserve keys - this is intentional.
+    $data = [
+      'value_sort' => [1 => 'b', 2 => 'a'],
+      'no_sort' => [1 => 'b', 2 => 'a'],
+    ];
+    // Save config which has a schema that enforces sorting.
+    $this->config('config_schema_test.schema_sequence_sort')
+      ->setData($data)
+      ->save();
+
+    $this->assertSame(['a', 'b'], $this->config('config_schema_test.schema_sequence_sort')->get('value_sort'));
+    $this->assertSame([1 => 'b', 2 => 'a'], $this->config('config_schema_test.schema_sequence_sort')->get('no_sort'));
+
+    // Test sorts do not destroy complex values.
+    $data = [
+      'complex_sort_value' => [['foo' => 'b', 'bar' => 'b'] , ['foo' => 'a', 'bar' => 'a']],
+      'complex_sort_key' => ['b' => ['foo' => '1', 'bar' => '1'] , 'a' => ['foo' => '2', 'bar' => '2']],
+    ];
+    $this->config('config_schema_test.schema_sequence_sort')
+      ->setData($data)
+      ->save();
+    $this->assertSame([['foo' => 'a', 'bar' => 'a'], ['foo' => 'b', 'bar' => 'b']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_value'));
+    $this->assertSame(['a' => ['foo' => '2', 'bar' => '2'], 'b' => ['foo' => '1', 'bar' => '1']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_key'));
+
+    // Swap the previous test scenario around.
+    $data = [
+      'complex_sort_value' => ['b' => ['foo' => '1', 'bar' => '1'] , 'a' => ['foo' => '2', 'bar' => '2']],
+      'complex_sort_key' => [['foo' => 'b', 'bar' => 'b'] , ['foo' => 'a', 'bar' => 'a']],
+    ];
+    $this->config('config_schema_test.schema_sequence_sort')
+      ->setData($data)
+      ->save();
+    $this->assertSame([['foo' => '1', 'bar' => '1'], ['foo' => '2', 'bar' => '2']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_value'));
+    $this->assertSame([['foo' => 'b', 'bar' => 'b'], ['foo' => 'a', 'bar' => 'a']], $this->config('config_schema_test.schema_sequence_sort')->get('complex_sort_key'));
+
+  }
+
   /**
    * Tests fallback to a greedy wildcard.
    */
-- 
GitLab