Revert "[TASK] Avoid slow array functions in loops"
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Routing / Enhancer / VariableProcessor.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Routing\Enhancer;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 /**
20 * Helper for processing various variables within a Route Enhancer
21 */
22 class VariableProcessor
23 {
24 protected const LEVEL_DELIMITER = '__';
25 protected const ARGUMENT_SEPARATOR = '/';
26 protected const VARIABLE_PATTERN = '#\{(?P<name>[^}]+)\}#';
27
28 /**
29 * @var array
30 */
31 protected $hashes = [];
32
33 /**
34 * @var array
35 */
36 protected $nestedValues = [];
37
38 /**
39 * @param string $value
40 * @return string
41 */
42 protected function addHash(string $value): string
43 {
44 if (strlen($value) < 32 && !preg_match('#[^\w]#', $value)) {
45 return $value;
46 }
47 $hash = md5($value);
48 // Symfony Route Compiler requires first literal to be non-integer
49 if ($hash{0} === (string)(int)$hash{0}) {
50 $hash{0} = str_replace(
51 range('0', '9'),
52 range('o', 'x'),
53 $hash{0}
54 );
55 }
56 $this->hashes[$hash] = $value;
57 return $hash;
58 }
59
60 /**
61 * @param string $hash
62 * @return string
63 * @throws \OutOfRangeException
64 */
65 protected function resolveHash(string $hash): string
66 {
67 if (strlen($hash) < 32) {
68 return $hash;
69 }
70 if (!isset($this->hashes[$hash])) {
71 throw new \OutOfRangeException(
72 'Hash not resolvable',
73 1537633463
74 );
75 }
76 return $this->hashes[$hash];
77 }
78
79 /**
80 * @param string $value
81 * @return string
82 */
83 protected function addNestedValue(string $value): string
84 {
85 if (strpos($value, static::ARGUMENT_SEPARATOR) === false) {
86 return $value;
87 }
88 $nestedValue = str_replace(
89 static::ARGUMENT_SEPARATOR,
90 static::LEVEL_DELIMITER,
91 $value
92 );
93 $this->nestedValues[$nestedValue] = $value;
94 return $nestedValue;
95 }
96
97 /**
98 * @param string $value
99 * @return string
100 */
101 protected function resolveNestedValue(string $value): string
102 {
103 if (strpos($value, static::LEVEL_DELIMITER) === false) {
104 return $value;
105 }
106 return $this->nestedValues[$value] ?? $value;
107 }
108
109 /**
110 * @param string $routePath
111 * @param string|null $namespace
112 * @param array $arguments
113 * @return string
114 */
115 public function deflateRoutePath(string $routePath, string $namespace = null, array $arguments = []): string
116 {
117 if (!preg_match_all(static::VARIABLE_PATTERN, $routePath, $matches)) {
118 return $routePath;
119 }
120
121 $search = array_values($matches[0]);
122 $replace = array_map(
123 function (string $name) {
124 return '{' . $name . '}';
125 },
126 $this->deflateValues($matches['name'], $namespace, $arguments)
127 );
128
129 return str_replace($search, $replace, $routePath);
130 }
131
132 /**
133 * @param string $routePath
134 * @param string|null $namespace
135 * @param array $arguments
136 * @return string
137 */
138 public function inflateRoutePath(string $routePath, string $namespace = null, array $arguments = []): string
139 {
140 if (!preg_match_all(static::VARIABLE_PATTERN, $routePath, $matches)) {
141 return $routePath;
142 }
143
144 $search = array_values($matches[0]);
145 $replace = array_map(
146 function (string $name) {
147 return '{' . $name . '}';
148 },
149 $this->inflateValues($matches['name'], $namespace, $arguments)
150 );
151
152 return str_replace($search, $replace, $routePath);
153 }
154
155 /**
156 * Deflates (flattens) route/request parameters for a given namespace.
157 *
158 * @param array $parameters
159 * @param string $namespace
160 * @param array $arguments
161 * @return array
162 */
163 public function deflateNamespaceParameters(array $parameters, string $namespace, array $arguments = []): array
164 {
165 if (empty($namespace) || empty($parameters[$namespace])) {
166 return $parameters;
167 }
168 // prefix items of namespace parameters and apply argument mapping
169 $namespaceParameters = $this->deflateKeys($parameters[$namespace], $namespace, $arguments, false);
170 // deflate those array items
171 $namespaceParameters = $this->deflateArray($namespaceParameters);
172 unset($parameters[$namespace]);
173 // merge with remaining array items
174 return array_merge($parameters, $namespaceParameters);
175 }
176
177 /**
178 * Inflates (unflattens) route/request parameters.
179 *
180 * @param array $parameters
181 * @param string $namespace
182 * @param array $arguments
183 * @return array
184 */
185 public function inflateNamespaceParameters(array $parameters, string $namespace, array $arguments = []): array
186 {
187 if (empty($namespace) || empty($parameters)) {
188 return $parameters;
189 }
190
191 $parameters = $this->inflateArray($parameters, $namespace, $arguments);
192 // apply argument mapping on items of inflated namespace parameters
193 if (!empty($parameters[$namespace]) && !empty($arguments)) {
194 $parameters[$namespace] = $this->inflateKeys($parameters[$namespace], null, $arguments, false);
195 }
196 return $parameters;
197 }
198
199 /**
200 * Deflates (flattens) route/request parameters for a given namespace.
201 *
202 * @param array $parameters
203 * @param array $arguments
204 * @return array
205 */
206 public function deflateParameters(array $parameters, array $arguments = []): array
207 {
208 $parameters = $this->deflateKeys($parameters, null, $arguments, false);
209 return $this->deflateArray($parameters);
210 }
211
212 /**
213 * Inflates (unflattens) route/request parameters.
214 *
215 * @param array $parameters
216 * @param array $arguments
217 * @return array
218 */
219 public function inflateParameters(array $parameters, array $arguments = []): array
220 {
221 $parameters = $this->inflateArray($parameters, null, $arguments);
222 return $this->inflateKeys($parameters, null, $arguments, false);
223 }
224
225 /**
226 * Deflates keys names on the first level, now recursion into sub-arrays.
227 * Can be used to adjust key names of route requirements, mappers, etc.
228 *
229 * @param array $items
230 * @param string|null $namespace
231 * @param array $arguments
232 * @param bool $hash = true
233 * @return array
234 */
235 public function deflateKeys(array $items, string $namespace = null, array $arguments = [], bool $hash = true): array
236 {
237 if (empty($items) || empty($arguments) && empty($namespace)) {
238 return $items;
239 }
240 $keys = $this->deflateValues(array_keys($items), $namespace, $arguments, $hash);
241 return array_combine(
242 $keys,
243 array_values($items)
244 );
245 }
246
247 /**
248 * Inflates keys names on the first level, now recursion into sub-arrays.
249 * Can be used to adjust key names of route requirements, mappers, etc.
250 *
251 * @param array $items
252 * @param string|null $namespace
253 * @param array $arguments
254 * @param bool $hash = true
255 * @return array
256 */
257 public function inflateKeys(array $items, string $namespace = null, array $arguments = [], bool $hash = true): array
258 {
259 if (empty($items) || empty($arguments) && empty($namespace)) {
260 return $items;
261 }
262 $keys = $this->inflateValues(array_keys($items), $namespace, $arguments, $hash);
263 return array_combine(
264 $keys,
265 array_values($items)
266 );
267 }
268
269 /**
270 * Deflates plain values.
271 *
272 * @param array $values
273 * @param string|null $namespace
274 * @param array $arguments
275 * @param bool $hash
276 * @return array
277 */
278 protected function deflateValues(array $values, string $namespace = null, array $arguments = [], bool $hash = true): array
279 {
280 if (empty($values) || empty($arguments) && empty($namespace)) {
281 return $values;
282 }
283 $namespacePrefix = $namespace ? $namespace . static::LEVEL_DELIMITER : '';
284 return array_map(
285 function (string $value) use ($arguments, $namespacePrefix, $hash) {
286 $value = $arguments[$value] ?? $value;
287 $value = $this->addNestedValue($value);
288 $value = $namespacePrefix . $value;
289 if (!$hash) {
290 return $value;
291 }
292 return $this->addHash($value);
293 },
294 $values
295 );
296 }
297
298 /**
299 * Inflates plain values.
300 *
301 * @param array $values
302 * @param string|null $namespace
303 * @param array $arguments
304 * @param bool $hash
305 * @return array
306 */
307 protected function inflateValues(array $values, string $namespace = null, array $arguments = [], bool $hash = true): array
308 {
309 if (empty($values) || empty($arguments) && empty($namespace)) {
310 return $values;
311 }
312 $namespacePrefix = $namespace ? $namespace . static::LEVEL_DELIMITER : '';
313 return array_map(
314 function (string $value) use ($arguments, $namespacePrefix, $hash) {
315 if ($hash) {
316 $value = $this->resolveHash($value);
317 }
318 if (!empty($namespacePrefix) && strpos($value, $namespacePrefix) === 0) {
319 $value = substr($value, strlen($namespacePrefix));
320 }
321 $value = $this->resolveNestedValue($value);
322 $index = array_search($value, $arguments);
323 return $index !== false ? $index : $value;
324 },
325 $values
326 );
327 }
328
329 /**
330 * Deflates (flattens) array having nested structures.
331 *
332 * @param array $array
333 * @param string $prefix
334 * @return array
335 */
336 protected function deflateArray(array $array, string $prefix = ''): array
337 {
338 $delimiter = static::LEVEL_DELIMITER;
339 if ($prefix !== '' && substr($prefix, -strlen($delimiter)) !== $delimiter) {
340 $prefix .= static::LEVEL_DELIMITER;
341 }
342
343 $result = [];
344 foreach ($array as $key => $value) {
345 if (is_array($value)) {
346 $result = array_merge(
347 $result,
348 $this->deflateArray(
349 $value,
350 $prefix . $key . static::LEVEL_DELIMITER
351 )
352 );
353 } else {
354 $deflatedKey = $this->addHash($prefix . $key);
355 $result[$deflatedKey] = $value;
356 }
357 }
358 return $result;
359 }
360
361 /**
362 * Inflates (unflattens) an array into nested structures.
363 *
364 * @param array $array
365 * @param string $namespace
366 * @param array $arguments
367 * @return array
368 */
369 protected function inflateArray(array $array, ?string $namespace, array $arguments): array
370 {
371 $result = [];
372 foreach ($array as $key => $value) {
373 $inflatedKey = $this->resolveHash($key);
374 // inflate nested values `namespace__any__neste` -> `namespace__any/nested`
375 $inflatedKey = $this->inflateNestedValue($inflatedKey, $namespace, $arguments);
376 $steps = explode(static::LEVEL_DELIMITER, $inflatedKey);
377 $pointer = &$result;
378 foreach ($steps as $step) {
379 $pointer = &$pointer[$step];
380 }
381 $pointer = $value;
382 unset($pointer);
383 }
384 return $result;
385 }
386
387 /**
388 * @param string $value
389 * @param string $namespace
390 * @param array $arguments
391 * @return string
392 */
393 protected function inflateNestedValue(string $value, ?string $namespace, array $arguments): string
394 {
395 $namespacePrefix = $namespace ? $namespace . static::LEVEL_DELIMITER : '';
396 if (!empty($namespace) && strpos($value, $namespacePrefix) !== 0) {
397 return $value;
398 }
399 $possibleNestedValueKey = substr($value, strlen($namespacePrefix));
400 $possibleNestedValue = $this->nestedValues[$possibleNestedValueKey] ?? null;
401 if (!$possibleNestedValue || !in_array($possibleNestedValue, $arguments, true)) {
402 return $value;
403 }
404 return $namespacePrefix . $possibleNestedValue;
405 }
406 }