[TASK] Resolve Todos in InheritancesResolverService
[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 * Basic concept:
27 * - Take a large YAML config and replace the key '__inheritance' by the referenced YAML partial (of the same config file)
28 * - Maybe also override some keys of the referenced partial
29 * - Avoid endless loop by reference cycles
30 *
31 * e.g.
32 * ---------------------
33 *
34 * Form:
35 * part1:
36 * key1: value1
37 * key2: value2
38 * key3: value3
39 * part2:
40 * __inheritance:
41 * 10: Form.part1
42 * key2: another_value
43 *
44 * will result in:
45 * ---------------------
46 *
47 * Form:
48 * part1:
49 * key1: value1
50 * key2: value2
51 * key3: value3
52 * part2:
53 * key1: value1
54 * key2: another_value
55 * key3: value3
56 *
57 * ---------------------
58 * Scope: frontend / backend
59 * @internal
60 */
61 class InheritancesResolverService
62 {
63
64 /**
65 * The operator which is used to declare inheritances
66 */
67 const INHERITANCE_OPERATOR = '__inheritances';
68
69 /**
70 * The reference configuration is used to get untouched values which
71 * can be merged into the touched configuration.
72 *
73 * @var array
74 */
75 protected $referenceConfiguration = [];
76
77 /**
78 * This stack is needed to find cyclically inheritances which are on
79 * the same nesting level but which do not follow each other directly.
80 *
81 * @var array
82 */
83 protected $inheritanceStack = [];
84
85 /**
86 * Needed to buffer a configuration path for cyclically inheritances
87 * detection while inheritances for this path is ongoing.
88 *
89 * @var string
90 */
91 protected $inheritancePathToCkeck = '';
92
93 /**
94 * Returns an instance of this service. Additionally the configuration
95 * which should be resolved can be passed.
96 *
97 * @param array $configuration
98 * @return InheritancesResolverService
99 * @internal
100 */
101 public static function create(array $configuration = []): InheritancesResolverService
102 {
103 /** @var InheritancesResolverService $inheritancesResolverService */
104 $inheritancesResolverService = GeneralUtility::makeInstance(ObjectManager::class)
105 ->get(self::class);
106 $inheritancesResolverService->setReferenceConfiguration($configuration);
107 return $inheritancesResolverService;
108 }
109
110 /**
111 * Reset the state of this service.
112 * Mainly introduced for unit tests.
113 *
114 * @return InheritancesResolverService
115 * @internal
116 */
117 public function reset()
118 {
119 $this->referenceConfiguration = [];
120 $this->inheritanceStack = [];
121 $this->inheritancePathToCkeck = '';
122 return $this;
123 }
124
125 /**
126 * Set the reference configuration which is used to get untouched
127 * values which can be merged into the touched configuration.
128 *
129 * @param array $referenceConfiguration
130 * @return InheritancesResolverService
131 */
132 public function setReferenceConfiguration(array $referenceConfiguration)
133 {
134 $this->referenceConfiguration = $referenceConfiguration;
135 return $this;
136 }
137
138 /**
139 * Resolve all inheritances within a configuration.
140 * After that the configuration array is cleaned from the
141 * inheritance operator.
142 *
143 * @return array
144 * @internal
145 */
146 public function getResolvedConfiguration(): array
147 {
148 $configuration = $this->resolve($this->referenceConfiguration);
149 $configuration = $this->removeInheritanceOperatorRecursive($configuration);
150 return $configuration;
151 }
152
153 /**
154 * Resolve all inheritances within a configuration.
155 *
156 * Takes a YAML config mapped to associative array $configuration
157 * - replace all findings of key '__inheritance' recursively
158 * - perform a deep search in config by iteration, thus check for endless loop by reference cycle
159 *
160 * Return the completed configuration.
161 *
162 * @param array $configuration - a mapped YAML configuration (full or partial)
163 * @param array $pathStack - an identifier for YAML key as array (Form.part1.key => {Form, part1, key})
164 * @param bool $setInheritancePathToCkeck
165 * @return array
166 */
167 protected function resolve(
168 array $configuration,
169 array $pathStack = [],
170 bool $setInheritancePathToCkeck = true
171 ): array {
172 foreach ($configuration as $key => $values) {
173 //add current key to pathStack
174 $pathStack[] = $key;
175 $path = implode('.', $pathStack);
176
177 //check endless loop for current path
178 $this->throwExceptionIfCycleInheritances($path, $path);
179
180 //overwrite service property 'inheritancePathToCheck' with current path
181 if ($setInheritancePathToCkeck) {
182 $this->inheritancePathToCkeck = $path;
183 }
184
185 //if value of subnode is an array, perform a deep search iteration step
186 if (is_array($configuration[$key])) {
187 if (isset($configuration[$key][self::INHERITANCE_OPERATOR])) {
188 $inheritances = $this->getValueByPath($this->referenceConfiguration, $path . '.' . self::INHERITANCE_OPERATOR);
189
190 //and replace the __inheritance operator by the respective partial
191 if (is_array($inheritances)) {
192 $inheritedConfigurations = $this->resolveInheritancesRecursive($inheritances);
193 $configuration[$key] = array_replace_recursive($inheritedConfigurations, $configuration[$key]);
194 }
195
196 //remove the inheritance operator from configuration
197 unset($configuration[$key][self::INHERITANCE_OPERATOR]);
198 }
199
200 if (!empty($configuration[$key])) {
201 // resolve subnode of YAML config
202 $configuration[$key] = $this->resolve($configuration[$key], $pathStack);
203 }
204 }
205 array_pop($pathStack);
206 }
207
208 return $configuration;
209 }
210
211 /**
212 * Additional helper for the resolve method.
213 *
214 * Takes all inheritances (an array of YAML paths), and check them for endless loops
215 *
216 * @param array $inheritances
217 * @return array
218 * @throws CycleInheritancesException
219 */
220 protected function resolveInheritancesRecursive(array $inheritances): array
221 {
222 ksort($inheritances);
223 $inheritedConfigurations = [];
224 foreach ($inheritances as $inheritancePath) {
225 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
226 $inheritedConfiguration = $this->getValueByPath($this->referenceConfiguration, $inheritancePath);
227
228 if (
229 isset($inheritedConfiguration[self::INHERITANCE_OPERATOR])
230 && count($inheritedConfiguration) === 1
231 ) {
232 if ($this->inheritancePathToCkeck === $inheritancePath) {
233 throw new CycleInheritancesException(
234 $this->inheritancePathToCkeck . ' has cycle inheritances',
235 1474900796
236 );
237 }
238
239 $inheritedConfiguration = $this->resolveInheritancesRecursive(
240 $inheritedConfiguration[self::INHERITANCE_OPERATOR]
241 );
242 } else {
243 $pathStack = explode('.', $inheritancePath);
244 $key = array_pop($pathStack);
245 $newConfiguration = [
246 $key => $inheritedConfiguration
247 ];
248 $inheritedConfiguration = $this->resolve(
249 $newConfiguration,
250 $pathStack,
251 false
252 );
253 $inheritedConfiguration = $inheritedConfiguration[$key];
254 }
255
256 if ($inheritedConfiguration === null) {
257 throw new CycleInheritancesException(
258 $inheritancePath . ' does not exist within the configuration',
259 1489260796
260 );
261 }
262
263 $inheritedConfigurations = array_replace_recursive(
264 $inheritedConfigurations,
265 $inheritedConfiguration
266 );
267 }
268
269 return $inheritedConfigurations;
270 }
271
272 /**
273 * Throw an exception if a cycle is detected.
274 *
275 * @param string $path
276 * @param string $pathToCheck
277 * @throws CycleInheritancesException
278 */
279 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
280 {
281 $configuration = $this->getValueByPath($this->referenceConfiguration, $path);
282
283 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
284 $inheritances = $this->getValueByPath($this->referenceConfiguration, $path . '.' . self::INHERITANCE_OPERATOR);
285
286 if (is_array($inheritances)) {
287 foreach ($inheritances as $inheritancePath) {
288 $configuration = $this->getValueByPath($this->referenceConfiguration, $inheritancePath);
289
290 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
291 $_inheritances = $this->getValueByPath($this->referenceConfiguration, $inheritancePath . '.' . self::INHERITANCE_OPERATOR);
292
293 foreach ($_inheritances as $_inheritancePath) {
294 if (strpos($pathToCheck, $_inheritancePath) === 0) {
295 throw new CycleInheritancesException(
296 $pathToCheck . ' has cycle inheritances',
297 1474900797
298 );
299 }
300 }
301 }
302
303 if (
304 is_array($this->inheritanceStack[$pathToCheck])
305 && in_array($inheritancePath, $this->inheritanceStack[$pathToCheck])
306 ) {
307 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
308 throw new CycleInheritancesException(
309 $pathToCheck . ' has cycle inheritances',
310 1474900799
311 );
312 }
313 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
314 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
315 }
316 $this->inheritanceStack[$pathToCheck] = null;
317 }
318 }
319 }
320
321 /**
322 * Recursively remove self::INHERITANCE_OPERATOR keys
323 *
324 * @param array $array
325 * @return array the modified array
326 */
327 protected function removeInheritanceOperatorRecursive(array $array): array
328 {
329 $result = $array;
330 foreach ($result as $key => $value) {
331 if ($key === self::INHERITANCE_OPERATOR) {
332 unset($result[$key]);
333 continue;
334 }
335
336 if (is_array($value)) {
337 $result[$key] = $this->removeInheritanceOperatorRecursive($value);
338 }
339 }
340 return $result;
341 }
342
343 /**
344 * Check the given array representation of a YAML config for the given path and return it's value / sub-array.
345 * If path is not found, return null;
346 *
347 * @param array $config
348 * @param string $path
349 * @param string $delimiter
350 * @return string|array|null
351 */
352 protected function getValueByPath(array $config, string $path, string $delimiter = '.')
353 {
354 try {
355 return ArrayUtility::getValueByPath($config, $path, $delimiter);
356 } catch (\RuntimeException $exception) {
357 return null;
358 }
359 }
360 }