ce866706bac3cd93918afee9dc847f7bac15c1d7
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Mvc / Configuration / InheritancesResolverService.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Form\Mvc\Configuration;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Utility\ArrayUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Extbase\Object\ObjectManager;
21 use TYPO3\CMS\Form\Mvc\Configuration\Exception\CycleInheritancesException;
22
23 /**
24 * Resolve declared inheritances within an configuration array
25 *
26 * Scope: frontend / backend
27 * @internal
28 */
29 class InheritancesResolverService
30 {
31
32 /**
33 * The operator which is used to declare inheritances
34 */
35 const INHERITANCE_OPERATOR = '__inheritances';
36
37 /**
38 * The reference configuration is used to get untouched values which
39 * can be merged into the touched configuration.
40 *
41 * @var array
42 */
43 protected $referenceConfiguration = [];
44
45 /**
46 * This stack is needed to find cyclically inheritances which are on
47 * the same nesting level but which do not follow each other directly.
48 *
49 * @var array
50 */
51 protected $inheritanceStack = [];
52
53 /**
54 * Needed to park a configuration path for cyclically inheritances
55 * detection while inheritances for this path is ongoing.
56 *
57 * @var string
58 */
59 protected $inheritancePathToCkeck = '';
60
61 /**
62 * Returns an instance of this service. Additionally the configuration
63 * which should be resolved can be passed.
64 *
65 * @param array $configuration
66 * @return InheritancesResolverService
67 * @internal
68 */
69 public static function create(array $configuration = []): InheritancesResolverService
70 {
71 /** @var InheritancesResolverService $inheritancesResolverService */
72 $inheritancesResolverService = GeneralUtility::makeInstance(ObjectManager::class)
73 ->get(self::class);
74 $inheritancesResolverService->setReferenceConfiguration($configuration);
75 return $inheritancesResolverService;
76 }
77
78 /**
79 * Reset the state of this service.
80 * Mainly introduced for unit tests.
81 *
82 * @return InheritancesResolverService
83 * @internal
84 */
85 public function reset()
86 {
87 $this->referenceConfiguration = [];
88 $this->inheritanceStack = [];
89 $this->inheritancePathToCkeck = '';
90 return $this;
91 }
92
93 /**
94 * Set the reference configuration which is used to get untouched
95 * values which can be merged into the touched configuration.
96 *
97 * @param array
98 * @return InheritancesResolverService
99 */
100 public function setReferenceConfiguration(array $referenceConfiguration)
101 {
102 $this->referenceConfiguration = $referenceConfiguration;
103 return $this;
104 }
105
106 /**
107 * Resolve all inheritances within a configuration.
108 * After that the configuration array is cleaned from the
109 * inheritance operator.
110 *
111 * @return array
112 * @internal
113 */
114 public function getResolvedConfiguration(): array
115 {
116 $configuration = $this->resolve($this->referenceConfiguration);
117 $configuration = $this->removeInheritanceOperatorRecursive($configuration);
118 return $configuration;
119 }
120
121 /**
122 * Resolve all inheritances within a configuration.
123 *
124 * @toDo: More description
125 * @param array $configuration
126 * @param array $pathStack
127 * @param bool $setInheritancePathToCkeck
128 * @return array
129 */
130 protected function resolve(
131 array $configuration,
132 array $pathStack = [],
133 bool $setInheritancePathToCkeck = true
134 ): array {
135 foreach ($configuration as $key => $values) {
136 $pathStack[] = $key;
137 $path = implode('.', $pathStack);
138
139 $this->throwExceptionIfCycleInheritances($path, $path);
140 if ($setInheritancePathToCkeck) {
141 $this->inheritancePathToCkeck = $path;
142 }
143
144 if (is_array($configuration[$key])) {
145 if (isset($configuration[$key][self::INHERITANCE_OPERATOR])) {
146 $inheritances = static::getValueByPathHelper(
147 $this->referenceConfiguration,
148 $path . '.' . self::INHERITANCE_OPERATOR
149 );
150
151 if (is_array($inheritances)) {
152 $inheritedConfigurations = $this->resolveInheritancesRecursive($inheritances);
153
154 $configuration[$key] = $this->mergeRecursiveWithOverrule(
155 $inheritedConfigurations,
156 $configuration[$key]
157 );
158 }
159
160 unset($configuration[$key][self::INHERITANCE_OPERATOR]);
161 }
162
163 if (!empty($configuration[$key])) {
164 $configuration[$key] = $this->resolve(
165 $configuration[$key],
166 $pathStack
167 );
168 }
169 }
170 array_pop($pathStack);
171 }
172
173 return $configuration;
174 }
175
176 /**
177 * Additional helper for the resolve method.
178 *
179 * @toDo: More description
180 * @param array $inheritances
181 * @return array
182 * @throws CycleInheritancesException
183 */
184 protected function resolveInheritancesRecursive(array $inheritances): array
185 {
186 ksort($inheritances);
187 $inheritedConfigurations = [];
188 foreach ($inheritances as $inheritancePath) {
189 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
190 $inheritedConfiguration = static::getValueByPathHelper(
191 $this->referenceConfiguration,
192 $inheritancePath
193 );
194
195 if (
196 isset($inheritedConfiguration[self::INHERITANCE_OPERATOR])
197 && count($inheritedConfiguration) === 1
198 ) {
199 if ($this->inheritancePathToCkeck === $inheritancePath) {
200 throw new CycleInheritancesException(
201 $this->inheritancePathToCkeck . ' has cycle inheritances',
202 1474900796
203 );
204 }
205
206 $inheritedConfiguration = $this->resolveInheritancesRecursive(
207 $inheritedConfiguration[self::INHERITANCE_OPERATOR]
208 );
209 } else {
210 $pathStack = explode('.', $inheritancePath);
211 $key = array_pop($pathStack);
212 $newConfiguration = [
213 $key => $inheritedConfiguration
214 ];
215 $inheritedConfiguration = $this->resolve(
216 $newConfiguration,
217 $pathStack,
218 false
219 );
220 $inheritedConfiguration = $inheritedConfiguration[$key];
221 }
222
223 $inheritedConfigurations = $this->mergeRecursiveWithOverrule(
224 $inheritedConfigurations,
225 $inheritedConfiguration
226 );
227 }
228
229 return $inheritedConfigurations;
230 }
231
232 /**
233 * Throw an exception if a cycle is detected.
234 *
235 * @toDo: More description
236 * @param string $path
237 * @param string $pathToCheck
238 * @return void
239 * @throws CycleInheritancesException
240 */
241 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
242 {
243 $configuration = static::getValueByPathHelper(
244 $this->referenceConfiguration,
245 $path
246 );
247
248 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
249 $inheritances = static::getValueByPathHelper(
250 $this->referenceConfiguration,
251 $path . '.' . self::INHERITANCE_OPERATOR
252 );
253 if (is_array($inheritances)) {
254 foreach ($inheritances as $inheritancePath) {
255 $configuration = static::getValueByPathHelper(
256 $this->referenceConfiguration,
257 $inheritancePath
258 );
259 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
260 $_inheritances = static::getValueByPathHelper(
261 $this->referenceConfiguration,
262 $inheritancePath . '.' . self::INHERITANCE_OPERATOR
263 );
264 foreach ($_inheritances as $_inheritancePath) {
265 if (strpos($pathToCheck, $_inheritancePath) === 0) {
266 throw new CycleInheritancesException(
267 $pathToCheck . ' has cycle inheritances',
268 1474900797
269 );
270 }
271 }
272 }
273
274 if (
275 is_array($this->inheritanceStack[$pathToCheck])
276 && in_array($inheritancePath, $this->inheritanceStack[$pathToCheck])
277 ) {
278 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
279 throw new CycleInheritancesException(
280 $pathToCheck . ' has cycle inheritances',
281 1474900799
282 );
283 }
284 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
285 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
286 }
287 $this->inheritanceStack[$pathToCheck] = null;
288 }
289 }
290 }
291
292 /**
293 * Recursively remove self::INHERITANCE_OPERATOR keys
294 *
295 * @param array $array
296 * @return array the modified array
297 */
298 protected function removeInheritanceOperatorRecursive(array $array): array
299 {
300 $result = $array;
301 foreach ($result as $key => $value) {
302 if ($key === self::INHERITANCE_OPERATOR) {
303 unset($result[$key]);
304 continue;
305 }
306
307 if (is_array($value)) {
308 $result[$key] = $this->removeInheritanceOperatorRecursive($value);
309 }
310 }
311 return $result;
312 }
313
314 /**
315 * Merges two arrays recursively and "binary safe" (integer keys are overridden as well),
316 * overruling similar values in the first array ($firstArray) with the
317 * values of the second array ($secondArray)
318 * In case of identical keys, ie. keeping the values of the second.
319 * This is basicly the Extbase arrayMergeRecursiveOverrule method.
320 * This method act different to the core mergeRecursiveWithOverrule method.
321 * This method has the possibility to overrule a array value within the
322 * $firstArray with a string value within the $secondArray.
323 * The core method does not support such a overrule.
324 * The reason for this code duplication is that the extbase method will be
325 * deprecated in the future.
326 *
327 * @param array $firstArray First array
328 * @param array $secondArray Second array, overruling the first array
329 * @param bool $dontAddNewKeys If set, keys that are NOT found in $firstArray (first array)
330 * will not be set. Thus only existing value can/will be
331 * overruled from second array.
332 * @param bool $emptyValuesOverride If set (which is the default), values from $secondArray
333 * will overrule if they are empty (according to PHP's empty() function)
334 * @return array Resulting array where $secondArray values has overruled $firstArray values
335 * @internal
336 */
337 protected function mergeRecursiveWithOverrule(
338 array $firstArray,
339 array $secondArray,
340 bool $dontAddNewKeys = false,
341 bool $emptyValuesOverride = true
342 ): array {
343 foreach ($secondArray as $key => $value) {
344 if (
345 array_key_exists($key, $firstArray)
346 && is_array($firstArray[$key])
347 ) {
348 if (is_array($secondArray[$key])) {
349 $firstArray[$key] = $this->mergeRecursiveWithOverrule(
350 $firstArray[$key],
351 $secondArray[$key],
352 $dontAddNewKeys,
353 $emptyValuesOverride
354 );
355 } else {
356 $firstArray[$key] = $secondArray[$key];
357 }
358 } else {
359 if ($dontAddNewKeys) {
360 if (array_key_exists($key, $firstArray)) {
361 if ($emptyValuesOverride || !empty($value)) {
362 $firstArray[$key] = $value;
363 }
364 }
365 } else {
366 if ($emptyValuesOverride || !empty($value)) {
367 $firstArray[$key] = $value;
368 }
369 }
370 }
371 }
372 reset($firstArray);
373 return $firstArray;
374 }
375
376 /**
377 * Helper to return a specified path.
378 *
379 * @param array &$array The array to traverse as a reference
380 * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz'
381 * @return mixed The value found, NULL if the path didn't exist
382 */
383 protected static function getValueByPathHelper(array $array, $path)
384 {
385 try {
386 return ArrayUtility::getValueByPath($array, $path, '.');
387 } catch (\RuntimeException $e) {
388 return null;
389 }
390 }
391 }