summaryrefslogtreecommitdiffstats
path: root/src/ConfigMerger.php
blob: 262b1d82435cc4b5975a35f9f98261fd0a889956 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?php

namespace Drupal\config_merge;

use Symfony\Component\Yaml\Inline;

/**
 * Provides helper functions for merging configuration items.
 */
class ConfigMerger {

  /**
   * Merges changes to a configuration item into the active storage.
   *
   * @param $previous
   *   The configuration item as previously provided (from snapshot).
   * @param $current
   *   The configuration item as currently provided by an extension.
   * @param $active
   *   The configuration item as present in the active storage.
   */
  public static function mergeConfigItemStates(array $previous, array $current, array $active) {
    // We are merging into the active configuration state.
    $result = $active;

    $states = [
      $previous,
      $current,
      $active,
    ];

    $is_associative = FALSE;

    foreach ($states as $array) {
      // Analyze the array to determine if we should preserve integer keys.
      if (Inline::isHash($array)) {
        // If any of the states is associative, treat the item as associative.
        $is_associative = TRUE;
        break;
      }
    }

    // Process associative arrays.
    // Find any differences between previous and current states.
    if ($is_associative) {
      // Detect and process removals.
      $removed = array_diff_key($previous, $current);
      foreach ($removed as $key => $value) {
        // Remove only if unchanged in the active state.
        if (isset($active[$key]) && $active[$key] === $previous[$key]) {
          unset($result[$key]);
        }
      }

      // Detect and handle additions.
      $added = array_diff_key($current, $previous);
      foreach ($added as $key => $value) {
        // Add only if the key hasn't already been set.
        if (!isset($active[$key])) {
          $result[$key] = $value;
        }
      }

      // Detect and process changes.
      foreach ($current as $key => $value) {
        if (isset($previous[$key]) && $previous[$key] !== $value) {
          // If we have an array, recurse.
          if (is_array($value) && is_array($previous[$key]) && isset($active[$key]) && is_array($active[$key])) {
            $result[$key] = self::mergeConfigItemStates($previous[$key], $value, $active[$key]);
          }
          else {
            // Accept the new value only if the item hasn't been customized.
            if (isset($active[$key]) && $active[$key] === $previous[$key]) {
              $result[$key] = $value;
            }
          }
        }
      }
    }
    // Process indexed arrays.
    else {
      // Detect and process removals.
      $remove = [];
      foreach($previous as $key => $value) {
        if (!in_array($value, $current)) {
          // If there is an unchanged value in the active configuration, remove
          // it.
          if (($key = array_search($value, $result, TRUE)) !== FALSE) {
            $remove[] = $key;
          }
        }
      }

      // Detect and process additions.
      foreach($current as $value) {
        if (!in_array($value, $previous) && !in_array($value, $result)) {
          // A change is an addition of a previously-deleted value. Attempt to
          // retain the previous key to minimize spurious diffs.
          if ($key = array_shift($remove)) {
            $result[$key] = $value;
          }
          else {
            $result[] = $value;
          }
        }
      }

      // Remove items that were not replaced.
      $result = array_diff_key($result, array_combine($remove, $remove));

      // Keep non-associative.
      $result = array_values($result);
    }

    return $result;
  }

}