summaryrefslogtreecommitdiffstats
path: root/src/ConfigDiffer.php
blob: 75b5fecbc91bba7352488fe26f43590ab59213d8 (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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
<?php

namespace Drupal\config_update;

use Drupal\Component\Diff\Diff;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;

/**
 * Provides methods related to config differences.
 */
class ConfigDiffer implements ConfigDiffInterface {

  use StringTranslationTrait;

  /**
   * List of elements to ignore on top level when comparing config.
   *
   * @var string[]
   *
   * @see ConfigDiffer::format().
   */
  protected $ignore;

  /**
   * Prefix to use to indicate config hierarchy.
   *
   * @var string
   *
   * @see ConfigDiffer::format().
   */
  protected $hierarchyPrefix;

  /**
   * Prefix to use to indicate config values.
   *
   * @var string
   *
   * @see ConfigDiffer::format().
   */
  protected $valuePrefix;

  /**
   * Constructs a ConfigDiffer.
   *
   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
   *   String translation service.
   * @param string[] $ignore
   *   Config components to ignore at the top level.
   * @param string $hierarchy_prefix
   *   Prefix to use in diffs for array hierarchy.
   * @param string $value_prefix
   *   Prefix to use in diffs for array value.
   */
  public function __construct(TranslationInterface $translation, array $ignore = ['uuid', '_core'], $hierarchy_prefix = '::', $value_prefix = ' : ') {
    $this->stringTranslation = $translation;
    $this->hierarchyPrefix = $hierarchy_prefix;
    $this->valuePrefix = $value_prefix;
    $this->ignore = $ignore;
  }

  /**
   * Normalizes config for comparison.
   *
   * Removes elements in the ignore list from the top level of configuration,
   * and at each level of the array, removes empty arrays and sorts by array
   * key, so that config from different storage can be compared meaningfully.
   *
   * @param array|null $config
   *   Configuration array to normalize.
   *
   * @return array
   *   Normalized configuration array.
   *
   * @see ConfigDiffer::format()
   * @see ConfigDiffer::$ignore
   */
  protected function normalize($config) {
    if (empty($config)) {
      return [];
    }

    // Remove "ignore" elements, only at the top level.
    foreach ($this->ignore as $element) {
      unset($config[$element]);
    }

    // Recursively normalize and return.
    return $this->normalizeArray($config);
  }

  /**
   * Recursively sorts an array by key, and removes empty arrays.
   *
   * @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;
        }
        else {
          unset($array[$key]);
        }
      }
    }

    ksort($array);
    return $array;
  }

  /**
   * {@inheritdoc}
   */
  public function same($source, $target) {
    $source = $this->normalize($source);
    $target = $this->normalize($target);
    return $source === $target;
  }

  /**
   * Formats config for showing differences.
   *
   * To compute differences, we need to separate the config into lines and use
   * line-by-line differencer. The obvious way to split into lines is:
   * @code
   * explode("\n", Yaml::encode($config))
   * @endcode
   * But this would highlight meaningless differences due to the often different
   * order of config files, and also loses the indentation and context of the
   * config hierarchy when differences are computed, making the difference
   * difficult to interpret.
   *
   * So, what we do instead is to take the YAML hierarchy and format it so that
   * the hierarchy is shown on each line. So, if you're in element
   * $config['foo']['bar'] and the value is 'value', you will see
   * 'foo::bar : value'.
   *
   * @param array $config
   *   Config array to format. Normalize it first if you want to do diffs.
   * @param string $prefix
   *   (optional) When called recursively, the prefix to put on each line. Omit
   *   when initially calling this function.
   *
   * @return string[]
   *   Array of config lines formatted so that a line-by-line diff will show the
   *   context in each line, and meaningful differences will be computed.
   *
   * @see ConfigDiffer::normalize()
   * @see ConfigDiffer::$hierarchyPrefix
   * @see ConfigDiffer::$valuePrefix
   */
  protected function format(array $config, $prefix = '') {
    $lines = [];

    foreach ($config as $key => $value) {
      $section_prefix = ($prefix) ? $prefix . $this->hierarchyPrefix . $key : $key;

      if (is_array($value)) {
        $lines[] = $section_prefix;
        $newlines = $this->format($value, $section_prefix);
        foreach ($newlines as $line) {
          $lines[] = $line;
        }
      }
      elseif (is_null($value)) {
        $lines[] = $section_prefix . $this->valuePrefix . $this->t('(NULL)');
      }
      else {
        $lines[] = $section_prefix . $this->valuePrefix . $value;
      }
    }

    return $lines;
  }

  /**
   * {@inheritdoc}
   */
  public function diff($source, $target) {
    $source = $this->normalize($source);
    $target = $this->normalize($target);

    $source_lines = $this->format($source);
    $target_lines = $this->format($target);

    return new Diff($source_lines, $target_lines);
  }

}