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