2 declare(strict_types
= 1);
3 namespace TYPO3\CMS\Form\Mvc\Configuration
;
6 * This file is part of the TYPO3 CMS project.
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.
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
15 * The TYPO3 project - inspiring people to share!
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
;
25 * Resolve declared inheritances within an configuration array
27 * Scope: frontend / backend
30 class InheritancesResolverService
34 * The operator which is used to declare inheritances
36 const INHERITANCE_OPERATOR
= '__inheritances';
39 * The reference configuration is used to get untouched values which
40 * can be merged into the touched configuration.
44 protected $referenceConfiguration = [];
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.
52 protected $inheritanceStack = [];
55 * Needed to park a configuration path for cyclically inheritances
56 * detection while inheritances for this path is ongoing.
60 protected $inheritancePathToCkeck = '';
63 * Returns an instance of this service. Additionally the configuration
64 * which should be resolved can be passed.
66 * @param array $configuration
67 * @return InheritancesResolverService
70 public static function create(array $configuration = []): InheritancesResolverService
72 /** @var InheritancesResolverService $inheritancesResolverService */
73 $inheritancesResolverService = GeneralUtility
::makeInstance(ObjectManager
::class)
75 $inheritancesResolverService->setReferenceConfiguration($configuration);
76 return $inheritancesResolverService;
80 * Reset the state of this service.
81 * Mainly introduced for unit tests.
83 * @return InheritancesResolverService
86 public function reset()
88 $this->referenceConfiguration
= [];
89 $this->inheritanceStack
= [];
90 $this->inheritancePathToCkeck
= '';
95 * Set the reference configuration which is used to get untouched
96 * values which can be merged into the touched configuration.
98 * @param array $referenceConfiguration
99 * @return InheritancesResolverService
101 public function setReferenceConfiguration(array $referenceConfiguration)
103 $this->referenceConfiguration
= $referenceConfiguration;
108 * Resolve all inheritances within a configuration.
109 * After that the configuration array is cleaned from the
110 * inheritance operator.
115 public function getResolvedConfiguration(): array
117 $configuration = $this->resolve($this->referenceConfiguration
);
118 $configuration = $this->removeInheritanceOperatorRecursive($configuration);
119 return $configuration;
123 * Resolve all inheritances within a configuration.
125 * @todo: More description
126 * @param array $configuration
127 * @param array $pathStack
128 * @param bool $setInheritancePathToCkeck
131 protected function resolve(
132 array $configuration,
133 array $pathStack = [],
134 bool $setInheritancePathToCkeck = true
136 foreach ($configuration as $key => $values) {
138 $path = implode('.', $pathStack);
140 $this->throwExceptionIfCycleInheritances($path, $path);
141 if ($setInheritancePathToCkeck) {
142 $this->inheritancePathToCkeck
= $path;
145 if (is_array($configuration[$key])) {
146 if (isset($configuration[$key][self
::INHERITANCE_OPERATOR
])) {
148 $inheritances = ArrayUtility
::getValueByPath(
149 $this->referenceConfiguration
,
150 $path . '.' . self
::INHERITANCE_OPERATOR
,
153 } catch (MissingArrayPathException
$exception) {
154 $inheritances = null;
157 if (is_array($inheritances)) {
158 $inheritedConfigurations = $this->resolveInheritancesRecursive($inheritances);
160 $configuration[$key] = array_replace_recursive(
161 $inheritedConfigurations,
166 unset($configuration[$key][self
::INHERITANCE_OPERATOR
]);
169 if (!empty($configuration[$key])) {
170 $configuration[$key] = $this->resolve(
171 $configuration[$key],
176 array_pop($pathStack);
179 return $configuration;
183 * Additional helper for the resolve method.
185 * @todo: More description
186 * @param array $inheritances
188 * @throws CycleInheritancesException
190 protected function resolveInheritancesRecursive(array $inheritances): array
192 ksort($inheritances);
193 $inheritedConfigurations = [];
194 foreach ($inheritances as $inheritancePath) {
195 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
197 $inheritedConfiguration = ArrayUtility
::getValueByPath(
198 $this->referenceConfiguration
,
202 } catch (MissingArrayPathException
$exception) {
203 $inheritedConfiguration = null;
207 isset($inheritedConfiguration[self
::INHERITANCE_OPERATOR
])
208 && count($inheritedConfiguration) === 1
210 if ($this->inheritancePathToCkeck
=== $inheritancePath) {
211 throw new CycleInheritancesException(
212 $this->inheritancePathToCkeck
. ' has cycle inheritances',
217 $inheritedConfiguration = $this->resolveInheritancesRecursive(
218 $inheritedConfiguration[self
::INHERITANCE_OPERATOR
]
221 $pathStack = explode('.', $inheritancePath);
222 $key = array_pop($pathStack);
223 $newConfiguration = [
224 $key => $inheritedConfiguration
226 $inheritedConfiguration = $this->resolve(
231 $inheritedConfiguration = $inheritedConfiguration[$key];
234 if ($inheritedConfiguration === null) {
235 throw new CycleInheritancesException(
236 $inheritancePath . ' does not exist within the configuration',
241 $inheritedConfigurations = array_replace_recursive(
242 $inheritedConfigurations,
243 $inheritedConfiguration
247 return $inheritedConfigurations;
251 * Throw an exception if a cycle is detected.
253 * @todo: More description
254 * @param string $path
255 * @param string $pathToCheck
256 * @throws CycleInheritancesException
258 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
261 $configuration = ArrayUtility
::getValueByPath(
262 $this->referenceConfiguration
,
266 } catch (MissingArrayPathException
$exception) {
267 $configuration = null;
270 if (isset($configuration[self
::INHERITANCE_OPERATOR
])) {
272 $inheritances = ArrayUtility
::getValueByPath(
273 $this->referenceConfiguration
,
274 $path . '.' . self
::INHERITANCE_OPERATOR
,
277 } catch (MissingArrayPathException
$exception) {
278 $inheritances = null;
281 if (is_array($inheritances)) {
282 foreach ($inheritances as $inheritancePath) {
284 $configuration = ArrayUtility
::getValueByPath(
285 $this->referenceConfiguration
,
289 } catch (MissingArrayPathException
$exception) {
290 $configuration = null;
293 if (isset($configuration[self
::INHERITANCE_OPERATOR
])) {
295 $_inheritances = ArrayUtility
::getValueByPath(
296 $this->referenceConfiguration
,
297 $inheritancePath . '.' . self
::INHERITANCE_OPERATOR
,
300 } catch (MissingArrayPathException
$exception) {
301 $_inheritances = null;
304 foreach ($_inheritances as $_inheritancePath) {
305 if (strpos($pathToCheck, $_inheritancePath) === 0) {
306 throw new CycleInheritancesException(
307 $pathToCheck . ' has cycle inheritances',
315 is_array($this->inheritanceStack
[$pathToCheck])
316 && in_array($inheritancePath, $this->inheritanceStack
[$pathToCheck])
318 $this->inheritanceStack
[$pathToCheck][] = $inheritancePath;
319 throw new CycleInheritancesException(
320 $pathToCheck . ' has cycle inheritances',
324 $this->inheritanceStack
[$pathToCheck][] = $inheritancePath;
325 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
327 $this->inheritanceStack
[$pathToCheck] = null;
333 * Recursively remove self::INHERITANCE_OPERATOR keys
335 * @param array $array
336 * @return array the modified array
338 protected function removeInheritanceOperatorRecursive(array $array): array
341 foreach ($result as $key => $value) {
342 if ($key === self
::INHERITANCE_OPERATOR
) {
343 unset($result[$key]);
347 if (is_array($value)) {
348 $result[$key] = $this->removeInheritanceOperatorRecursive($value);