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 a configuration array
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
33 * ---------------------
46 * ---------------------
58 * ---------------------
59 * Scope: frontend / backend
62 class InheritancesResolverService
66 * The operator which is used to declare inheritances
68 const INHERITANCE_OPERATOR
= '__inheritances';
71 * The reference configuration is used to get untouched values which
72 * can be merged into the touched configuration.
76 protected $referenceConfiguration = [];
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.
84 protected $inheritanceStack = [];
87 * Needed to buffer a configuration path for cyclically inheritances
88 * detection while inheritances for this path is ongoing.
92 protected $inheritancePathToCheck = '';
95 * Returns an instance of this service. Additionally the configuration
96 * which should be resolved can be passed.
98 * @param array $configuration
99 * @return InheritancesResolverService
102 public static function create(array $configuration = []): InheritancesResolverService
104 /** @var InheritancesResolverService $inheritancesResolverService */
105 $inheritancesResolverService = GeneralUtility
::makeInstance(ObjectManager
::class)
107 $inheritancesResolverService->setReferenceConfiguration($configuration);
108 return $inheritancesResolverService;
112 * Reset the state of this service.
113 * Mainly introduced for unit tests.
115 * @return InheritancesResolverService
118 public function reset()
120 $this->referenceConfiguration
= [];
121 $this->inheritanceStack
= [];
122 $this->inheritancePathToCheck
= '';
127 * Set the reference configuration which is used to get untouched
128 * values which can be merged into the touched configuration.
130 * @param array $referenceConfiguration
131 * @return InheritancesResolverService
133 public function setReferenceConfiguration(array $referenceConfiguration)
135 $this->referenceConfiguration
= $referenceConfiguration;
140 * Resolve all inheritances within a configuration.
141 * After that the configuration array is cleaned from the
142 * inheritance operator.
147 public function getResolvedConfiguration(): array
149 $configuration = $this->resolve($this->referenceConfiguration
);
150 $configuration = $this->removeInheritanceOperatorRecursive($configuration);
151 return $configuration;
155 * Resolve all inheritances within a configuration.
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
161 * Return the completed configuration.
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
168 protected function resolve(
169 array $configuration,
170 array $pathStack = [],
171 bool
$setInheritancePathToCheck = true
173 foreach ($configuration as $key => $values) {
174 //add current key to pathStack
176 $path = implode('.', $pathStack);
178 //check endless loop for current path
179 $this->throwExceptionIfCycleInheritances($path, $path);
181 //overwrite service property 'inheritancePathToCheck' with current path
182 if ($setInheritancePathToCheck) {
183 $this->inheritancePathToCheck
= $path;
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
);
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]);
197 //remove the inheritance operator from configuration
198 unset($configuration[$key][self
::INHERITANCE_OPERATOR
]);
201 if (!empty($configuration[$key])) {
202 // resolve subnode of YAML config
203 $configuration[$key] = $this->resolve($configuration[$key], $pathStack);
206 array_pop($pathStack);
209 return $configuration;
213 * Additional helper for the resolve method.
215 * Takes all inheritances (an array of YAML paths), and check them for endless loops
217 * @param array $inheritances
219 * @throws CycleInheritancesException
221 protected function resolveInheritancesRecursive(array $inheritances): array
223 ksort($inheritances);
224 $inheritedConfigurations = [];
225 foreach ($inheritances as $inheritancePath) {
226 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
227 $inheritedConfiguration = $this->getValueByPath($this->referenceConfiguration
, $inheritancePath);
230 isset($inheritedConfiguration[self
::INHERITANCE_OPERATOR
])
231 && count($inheritedConfiguration) === 1
233 if ($this->inheritancePathToCheck
=== $inheritancePath) {
234 throw new CycleInheritancesException(
235 $this->inheritancePathToCheck
. ' has cycle inheritances',
240 $inheritedConfiguration = $this->resolveInheritancesRecursive(
241 $inheritedConfiguration[self
::INHERITANCE_OPERATOR
]
244 $pathStack = explode('.', $inheritancePath);
245 $key = array_pop($pathStack);
246 $newConfiguration = [
247 $key => $inheritedConfiguration
249 $inheritedConfiguration = $this->resolve(
254 $inheritedConfiguration = $inheritedConfiguration[$key];
257 if ($inheritedConfiguration === null
) {
258 throw new CycleInheritancesException(
259 $inheritancePath . ' does not exist within the configuration',
264 $inheritedConfigurations = array_replace_recursive(
265 $inheritedConfigurations,
266 $inheritedConfiguration
270 return $inheritedConfigurations;
274 * Throw an exception if a cycle is detected.
276 * @param string $path
277 * @param string $pathToCheck
278 * @throws CycleInheritancesException
280 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
282 $configuration = $this->getValueByPath($this->referenceConfiguration
, $path);
284 if (isset($configuration[self
::INHERITANCE_OPERATOR
])) {
285 $inheritances = $this->getValueByPath($this->referenceConfiguration
, $path . '.' . self
::INHERITANCE_OPERATOR
);
287 if (is_array($inheritances)) {
288 foreach ($inheritances as $inheritancePath) {
289 $configuration = $this->getValueByPath($this->referenceConfiguration
, $inheritancePath);
291 if (isset($configuration[self
::INHERITANCE_OPERATOR
])) {
292 $_inheritances = $this->getValueByPath($this->referenceConfiguration
, $inheritancePath . '.' . self
::INHERITANCE_OPERATOR
);
294 foreach ($_inheritances as $_inheritancePath) {
295 if (strpos($pathToCheck, $_inheritancePath) === 0) {
296 throw new CycleInheritancesException(
297 $pathToCheck . ' has cycle inheritances',
305 isset($this->inheritanceStack
[$pathToCheck])
306 && is_array($this->inheritanceStack
[$pathToCheck])
307 && in_array($inheritancePath, $this->inheritanceStack
[$pathToCheck])
309 $this->inheritanceStack
[$pathToCheck][] = $inheritancePath;
310 throw new CycleInheritancesException(
311 $pathToCheck . ' has cycle inheritances',
315 $this->inheritanceStack
[$pathToCheck][] = $inheritancePath;
316 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
318 $this->inheritanceStack
[$pathToCheck] = null
;
324 * Recursively remove self::INHERITANCE_OPERATOR keys
326 * @param array $array
327 * @return array the modified array
329 protected function removeInheritanceOperatorRecursive(array $array): array
332 foreach ($result as $key => $value) {
333 if ($key === self
::INHERITANCE_OPERATOR
) {
334 unset($result[$key]);
338 if (is_array($value)) {
339 $result[$key] = $this->removeInheritanceOperatorRecursive($value);
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;
349 * @param array $config
350 * @param string $path
351 * @param string $delimiter
352 * @return string|array|null
354 protected function getValueByPath(array $config, string $path, string $delimiter = '.')
357 return ArrayUtility
::getValueByPath($config, $path, $delimiter);
358 } catch (MissingArrayPathException
$exception) {