[CLEANUP] Remove/Replace non-standard phpdoc annotations
[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 $referenceConfiguration
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 try {
147 $inheritances = ArrayUtility::getValueByPath(
148 $this->referenceConfiguration,
149 $path . '.' . self::INHERITANCE_OPERATOR,
150 '.'
151 );
152 } catch (\RuntimeException $exception) {
153 $inheritances = null;
154 }
155
156 if (is_array($inheritances)) {
157 $inheritedConfigurations = $this->resolveInheritancesRecursive($inheritances);
158
159 $configuration[$key] = array_replace_recursive(
160 $inheritedConfigurations,
161 $configuration[$key]
162 );
163 }
164
165 unset($configuration[$key][self::INHERITANCE_OPERATOR]);
166 }
167
168 if (!empty($configuration[$key])) {
169 $configuration[$key] = $this->resolve(
170 $configuration[$key],
171 $pathStack
172 );
173 }
174 }
175 array_pop($pathStack);
176 }
177
178 return $configuration;
179 }
180
181 /**
182 * Additional helper for the resolve method.
183 *
184 * @todo: More description
185 * @param array $inheritances
186 * @return array
187 * @throws CycleInheritancesException
188 */
189 protected function resolveInheritancesRecursive(array $inheritances): array
190 {
191 ksort($inheritances);
192 $inheritedConfigurations = [];
193 foreach ($inheritances as $inheritancePath) {
194 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
195 try {
196 $inheritedConfiguration = ArrayUtility::getValueByPath(
197 $this->referenceConfiguration,
198 $inheritancePath,
199 '.'
200 );
201 } catch (\RuntimeException $exception) {
202 $inheritedConfiguration = null;
203 }
204
205 if (
206 isset($inheritedConfiguration[self::INHERITANCE_OPERATOR])
207 && count($inheritedConfiguration) === 1
208 ) {
209 if ($this->inheritancePathToCkeck === $inheritancePath) {
210 throw new CycleInheritancesException(
211 $this->inheritancePathToCkeck . ' has cycle inheritances',
212 1474900796
213 );
214 }
215
216 $inheritedConfiguration = $this->resolveInheritancesRecursive(
217 $inheritedConfiguration[self::INHERITANCE_OPERATOR]
218 );
219 } else {
220 $pathStack = explode('.', $inheritancePath);
221 $key = array_pop($pathStack);
222 $newConfiguration = [
223 $key => $inheritedConfiguration
224 ];
225 $inheritedConfiguration = $this->resolve(
226 $newConfiguration,
227 $pathStack,
228 false
229 );
230 $inheritedConfiguration = $inheritedConfiguration[$key];
231 }
232
233 if ($inheritedConfiguration === null) {
234 throw new CycleInheritancesException(
235 $inheritancePath . ' does not exist within the configuration',
236 1489260796
237 );
238 }
239
240 $inheritedConfigurations = array_replace_recursive(
241 $inheritedConfigurations,
242 $inheritedConfiguration
243 );
244 }
245
246 return $inheritedConfigurations;
247 }
248
249 /**
250 * Throw an exception if a cycle is detected.
251 *
252 * @todo: More description
253 * @param string $path
254 * @param string $pathToCheck
255 * @throws CycleInheritancesException
256 */
257 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
258 {
259 try {
260 $configuration = ArrayUtility::getValueByPath(
261 $this->referenceConfiguration,
262 $path,
263 '.'
264 );
265 } catch (\RuntimeException $exception) {
266 $configuration = null;
267 }
268
269 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
270 try {
271 $inheritances = ArrayUtility::getValueByPath(
272 $this->referenceConfiguration,
273 $path . '.' . self::INHERITANCE_OPERATOR,
274 '.'
275 );
276 } catch (\RuntimeException $exception) {
277 $inheritances = null;
278 }
279
280 if (is_array($inheritances)) {
281 foreach ($inheritances as $inheritancePath) {
282 try {
283 $configuration = ArrayUtility::getValueByPath(
284 $this->referenceConfiguration,
285 $inheritancePath,
286 '.'
287 );
288 } catch (\RuntimeException $exception) {
289 $configuration = null;
290 }
291
292 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
293 try {
294 $_inheritances = ArrayUtility::getValueByPath(
295 $this->referenceConfiguration,
296 $inheritancePath . '.' . self::INHERITANCE_OPERATOR,
297 '.'
298 );
299 } catch (\RuntimeException $exception) {
300 $_inheritances = null;
301 }
302
303 foreach ($_inheritances as $_inheritancePath) {
304 if (strpos($pathToCheck, $_inheritancePath) === 0) {
305 throw new CycleInheritancesException(
306 $pathToCheck . ' has cycle inheritances',
307 1474900797
308 );
309 }
310 }
311 }
312
313 if (
314 is_array($this->inheritanceStack[$pathToCheck])
315 && in_array($inheritancePath, $this->inheritanceStack[$pathToCheck])
316 ) {
317 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
318 throw new CycleInheritancesException(
319 $pathToCheck . ' has cycle inheritances',
320 1474900799
321 );
322 }
323 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
324 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
325 }
326 $this->inheritanceStack[$pathToCheck] = null;
327 }
328 }
329 }
330
331 /**
332 * Recursively remove self::INHERITANCE_OPERATOR keys
333 *
334 * @param array $array
335 * @return array the modified array
336 */
337 protected function removeInheritanceOperatorRecursive(array $array): array
338 {
339 $result = $array;
340 foreach ($result as $key => $value) {
341 if ($key === self::INHERITANCE_OPERATOR) {
342 unset($result[$key]);
343 continue;
344 }
345
346 if (is_array($value)) {
347 $result[$key] = $this->removeInheritanceOperatorRecursive($value);
348 }
349 }
350 return $result;
351 }
352 }