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\GeneralUtility
;
20 use TYPO3\CMS\Extbase\
Object\ObjectManager
;
21 use TYPO3\CMS\Form\Mvc\Configuration\Exception\CycleInheritancesException
;
24 * Resolve declared inheritances within an configuration array
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
32 * ---------------------
45 * ---------------------
57 * ---------------------
58 * Scope: frontend / backend
61 class InheritancesResolverService
65 * The operator which is used to declare inheritances
67 const INHERITANCE_OPERATOR
= '__inheritances';
70 * The reference configuration is used to get untouched values which
71 * can be merged into the touched configuration.
75 protected $referenceConfiguration = [];
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.
83 protected $inheritanceStack = [];
86 * Needed to buffer a configuration path for cyclically inheritances
87 * detection while inheritances for this path is ongoing.
91 protected $inheritancePathToCkeck = '';
94 * Returns an instance of this service. Additionally the configuration
95 * which should be resolved can be passed.
97 * @param array $configuration
98 * @return InheritancesResolverService
101 public static function create(array $configuration = []): InheritancesResolverService
103 /** @var InheritancesResolverService $inheritancesResolverService */
104 $inheritancesResolverService = GeneralUtility
::makeInstance(ObjectManager
::class)
106 $inheritancesResolverService->setReferenceConfiguration($configuration);
107 return $inheritancesResolverService;
111 * Reset the state of this service.
112 * Mainly introduced for unit tests.
114 * @return InheritancesResolverService
117 public function reset()
119 $this->referenceConfiguration
= [];
120 $this->inheritanceStack
= [];
121 $this->inheritancePathToCkeck
= '';
126 * Set the reference configuration which is used to get untouched
127 * values which can be merged into the touched configuration.
129 * @param array $referenceConfiguration
130 * @return InheritancesResolverService
132 public function setReferenceConfiguration(array $referenceConfiguration)
134 $this->referenceConfiguration
= $referenceConfiguration;
139 * Resolve all inheritances within a configuration.
140 * After that the configuration array is cleaned from the
141 * inheritance operator.
146 public function getResolvedConfiguration(): array
148 $configuration = $this->resolve($this->referenceConfiguration
);
149 $configuration = $this->removeInheritanceOperatorRecursive($configuration);
150 return $configuration;
154 * Resolve all inheritances within a configuration.
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
160 * Return the completed configuration.
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
167 protected function resolve(
168 array $configuration,
169 array $pathStack = [],
170 bool $setInheritancePathToCkeck = true
172 foreach ($configuration as $key => $values) {
173 //add current key to pathStack
175 $path = implode('.', $pathStack);
177 //check endless loop for current path
178 $this->throwExceptionIfCycleInheritances($path, $path);
180 //overwrite service property 'inheritancePathToCheck' with current path
181 if ($setInheritancePathToCkeck) {
182 $this->inheritancePathToCkeck
= $path;
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
);
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]);
196 //remove the inheritance operator from configuration
197 unset($configuration[$key][self
::INHERITANCE_OPERATOR
]);
200 if (!empty($configuration[$key])) {
201 // resolve subnode of YAML config
202 $configuration[$key] = $this->resolve($configuration[$key], $pathStack);
205 array_pop($pathStack);
208 return $configuration;
212 * Additional helper for the resolve method.
214 * Takes all inheritances (an array of YAML paths), and check them for endless loops
216 * @param array $inheritances
218 * @throws CycleInheritancesException
220 protected function resolveInheritancesRecursive(array $inheritances): array
222 ksort($inheritances);
223 $inheritedConfigurations = [];
224 foreach ($inheritances as $inheritancePath) {
225 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
226 $inheritedConfiguration = $this->getValueByPath($this->referenceConfiguration
, $inheritancePath);
229 isset($inheritedConfiguration[self
::INHERITANCE_OPERATOR
])
230 && count($inheritedConfiguration) === 1
232 if ($this->inheritancePathToCkeck
=== $inheritancePath) {
233 throw new CycleInheritancesException(
234 $this->inheritancePathToCkeck
. ' has cycle inheritances',
239 $inheritedConfiguration = $this->resolveInheritancesRecursive(
240 $inheritedConfiguration[self
::INHERITANCE_OPERATOR
]
243 $pathStack = explode('.', $inheritancePath);
244 $key = array_pop($pathStack);
245 $newConfiguration = [
246 $key => $inheritedConfiguration
248 $inheritedConfiguration = $this->resolve(
253 $inheritedConfiguration = $inheritedConfiguration[$key];
256 if ($inheritedConfiguration === null) {
257 throw new CycleInheritancesException(
258 $inheritancePath . ' does not exist within the configuration',
263 $inheritedConfigurations = array_replace_recursive(
264 $inheritedConfigurations,
265 $inheritedConfiguration
269 return $inheritedConfigurations;
273 * Throw an exception if a cycle is detected.
275 * @param string $path
276 * @param string $pathToCheck
277 * @throws CycleInheritancesException
279 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
281 $configuration = $this->getValueByPath($this->referenceConfiguration
, $path);
283 if (isset($configuration[self
::INHERITANCE_OPERATOR
])) {
284 $inheritances = $this->getValueByPath($this->referenceConfiguration
, $path . '.' . self
::INHERITANCE_OPERATOR
);
286 if (is_array($inheritances)) {
287 foreach ($inheritances as $inheritancePath) {
288 $configuration = $this->getValueByPath($this->referenceConfiguration
, $inheritancePath);
290 if (isset($configuration[self
::INHERITANCE_OPERATOR
])) {
291 $_inheritances = $this->getValueByPath($this->referenceConfiguration
, $inheritancePath . '.' . self
::INHERITANCE_OPERATOR
);
293 foreach ($_inheritances as $_inheritancePath) {
294 if (strpos($pathToCheck, $_inheritancePath) === 0) {
295 throw new CycleInheritancesException(
296 $pathToCheck . ' has cycle inheritances',
304 is_array($this->inheritanceStack
[$pathToCheck])
305 && in_array($inheritancePath, $this->inheritanceStack
[$pathToCheck])
307 $this->inheritanceStack
[$pathToCheck][] = $inheritancePath;
308 throw new CycleInheritancesException(
309 $pathToCheck . ' has cycle inheritances',
313 $this->inheritanceStack
[$pathToCheck][] = $inheritancePath;
314 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
316 $this->inheritanceStack
[$pathToCheck] = null;
322 * Recursively remove self::INHERITANCE_OPERATOR keys
324 * @param array $array
325 * @return array the modified array
327 protected function removeInheritanceOperatorRecursive(array $array): array
330 foreach ($result as $key => $value) {
331 if ($key === self
::INHERITANCE_OPERATOR
) {
332 unset($result[$key]);
336 if (is_array($value)) {
337 $result[$key] = $this->removeInheritanceOperatorRecursive($value);
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;
347 * @param array $config
348 * @param string $path
349 * @param string $delimiter
350 * @return string|array|null
352 protected function getValueByPath(array $config, string $path, string $delimiter = '.')
355 return ArrayUtility
::getValueByPath($config, $path, $delimiter);
356 } catch (\RuntimeException
$exception) {