[TASK] Update php-cs-fixer to 2.5.0
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Configuration / Loader / YamlFileLoader.php
1 <?php
2 namespace TYPO3\CMS\Core\Configuration\Loader;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Symfony\Component\Yaml\Yaml;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * A Yaml file loader that allows to load YAML files, based on the Symfony/Yaml component
22 *
23 * In addition to just load a yaml file, it adds some special functionality.
24 *
25 * - A special "imports" key in the yaml file allows to include other yaml files recursively
26 * where the actual yaml file gets loaded after the import statements, which are interpreted at the very beginning
27 *
28 * - Merging configuration options of import files when having simple "lists" will add items to the list instead
29 * of overwriting them.
30 *
31 * - Special placeholder values set via %optionA.suboptionB% replace the value with the named path of the configuration
32 * The placeholders will act as a full replacement of this value.
33 */
34 class YamlFileLoader
35 {
36
37 /**
38 * Loads and parses a YAML file, and returns an array with the found data
39 *
40 * @param string $fileName either relative to PATH_site or prefixed with EXT:...
41 * @return array the configuration as array
42 * @throws \RuntimeException when the file is empty or is of invalid format
43 */
44 public function load(string $fileName): array
45 {
46 $content = $this->getFileContents($fileName);
47 $content = Yaml::parse($content);
48
49 if (!is_array($content)) {
50 throw new \RuntimeException('YAML file "' . $fileName . '" could not be parsed into valid syntax, probably empty?', 1497332874);
51 }
52
53 $content = $this->processImports($content);
54
55 // Check for "%" placeholders
56 $content = $this->processPlaceholders($content, $content);
57
58 return $content;
59 }
60
61 /**
62 * Put into a separate method to ease the pains with unit tests
63 *
64 * @param string $fileName either relative to PATH_site or prefixed with EXT:...
65 *
66 * @return string the contents of the file
67 * @throws \RuntimeException when the file was not accessible
68 */
69 protected function getFileContents(string $fileName): string
70 {
71 $streamlinedFileName = GeneralUtility::getFileAbsFileName($fileName);
72 if (!$streamlinedFileName) {
73 throw new \RuntimeException('YAML File "' . $fileName . '" could not be loaded', 1485784246);
74 }
75 return file_get_contents($streamlinedFileName);
76 }
77
78 /**
79 * Checks for the special "imports" key on the main level of a file,
80 * which calls "load" recursively.
81 * @param array $content
82 *
83 * @return array
84 */
85 protected function processImports(array $content): array
86 {
87 if (isset($content['imports']) && is_array($content['imports'])) {
88 foreach ($content['imports'] as $import) {
89 $importedContent = $this->load($import['resource']);
90 // override the imported content with the one from the current file
91 $content = $this->merge($importedContent, $content);
92 }
93 unset($content['imports']);
94 }
95 return $content;
96 }
97
98 /**
99 * Main function that gets called recursively to check for %...% placeholders
100 * inside the array
101 *
102 * @param array $content the current sub-level content array
103 * @param array $referenceArray the global configuration array
104 *
105 * @return array the modified sub-level content array
106 */
107 protected function processPlaceholders(array $content, array $referenceArray): array
108 {
109 foreach ($content as $k => $v) {
110 if ($this->isPlaceholder($v)) {
111 $content[$k] = $this->getValueFromReferenceArray($v, $referenceArray);
112 } elseif (is_array($v)) {
113 $content[$k] = $this->processPlaceholders($v, $referenceArray);
114 }
115 }
116 return $content;
117 }
118
119 /**
120 * Returns the value for a placeholder as fetched from the referenceArray
121 *
122 * @param string $placeholder the string to search for
123 * @param array $referenceArray the main configuration array where to look up the data
124 *
125 * @return array|mixed|string
126 */
127 protected function getValueFromReferenceArray(string $placeholder, array $referenceArray)
128 {
129 $pointer = trim($placeholder, '%');
130 $parts = explode('.', $pointer);
131 $referenceData = $referenceArray;
132 foreach ($parts as $part) {
133 if (isset($referenceData[$part])) {
134 $referenceData = $referenceData[$part];
135 } else {
136 // return unsubstituted placeholder
137 return $placeholder;
138 }
139 }
140 if ($this->isPlaceholder($referenceData)) {
141 $referenceData = $this->getValueFromReferenceArray($referenceData, $referenceArray);
142 }
143 return $referenceData;
144 }
145
146 /**
147 * Checks if a value is a string and begins and ends with %...%
148 *
149 * @param mixed $value the probe to check for
150 * @return bool
151 */
152 protected function isPlaceholder($value): bool
153 {
154 return is_string($value) && substr($value, 0, 1) === '%' && substr($value, -1) === '%';
155 }
156
157 /**
158 * Same as array_replace_recursive except that when in simple arrays (= yaml lists), the entries are
159 * appended (array_merge)
160 *
161 * @param array $val1
162 * @param array $val2
163 *
164 * @return array
165 */
166 protected function merge(array $val1, array $val2): array
167 {
168 // Simple lists get merged / added up
169 if (count(array_filter(array_keys($val1), 'is_int')) === count($val1)) {
170 return array_merge($val1, $val2);
171 }
172 foreach ($val1 as $k => $v) {
173 // The key also exists in second array, if it is a simple value
174 // then $val2 will override the value, where an array is calling merge() recursively.
175 if (isset($val2[$k])) {
176 if (is_array($v) && isset($val2[$k])) {
177 if (is_array($val2[$k])) {
178 $val1[$k] = $this->merge($v, $val2[$k]);
179 } else {
180 $val1[$k] = $val2[$k];
181 }
182 } else {
183 $val1[$k] = $val2[$k];
184 }
185 unset($val2[$k]);
186 }
187 }
188 // If there are properties in the second array left, they are added up
189 if (!empty($val2)) {
190 foreach ($val2 as $k => $v) {
191 $val1[$k] = $v;
192 }
193 }
194
195 return $val1;
196 }
197 }