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