bac7b9747990b93f7ae93f4d17daf1ba3f5ff583
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 /**
4 * @see https://github.com/laminas/laminas-zendframework-bridge for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-zendframework-bridge/blob/master/COPYRIGHT.md
6 * @license https://github.com/laminas/laminas-zendframework-bridge/blob/master/LICENSE.md New BSD License
7 */
8
9 namespace Laminas\ZendFrameworkBridge;
10
11 use function array_intersect_key;
12 use function array_key_exists;
13 use function array_pop;
14 use function array_push;
15 use function count;
16 use function in_array;
17 use function is_array;
18 use function is_callable;
19 use function is_int;
20 use function is_string;
21
22 class ConfigPostProcessor
23 {
24 /** @internal */
25 const SERVICE_MANAGER_KEYS_OF_INTEREST = [
26 'aliases' => true,
27 'factories' => true,
28 'invokables' => true,
29 'services' => true,
30 ];
31
32 /** @var array String keys => string values */
33 private $exactReplacements = [
34 'zend-expressive' => 'mezzio',
35 'zf-apigility' => 'api-tools',
36 ];
37
38 /** @var Replacements */
39 private $replacements;
40
41 /** @var callable[] */
42 private $rulesets;
43
44 public function __construct()
45 {
46 $this->replacements = new Replacements();
47
48 /* Define the rulesets for replacements.
49 *
50 * Each ruleset has the following signature:
51 *
52 * @param mixed $value
53 * @param string[] $keys Full nested key hierarchy leading to the value
54 * @return null|callable
55 *
56 * If no match is made, a null is returned, allowing it to fallback to
57 * the next ruleset in the list. If a match is made, a callback is returned,
58 * and that will be used to perform the replacement on the value.
59 *
60 * The callback should have the following signature:
61 *
62 * @param mixed $value
63 * @param string[] $keys
64 * @return mixed The transformed value
65 */
66 $this->rulesets = [
67 // Exact values
68 function ($value) {
69 return is_string($value) && isset($this->exactReplacements[$value])
70 ? [$this, 'replaceExactValue']
71 : null;
72 },
73
74 // Router (MVC applications)
75 // We do not want to rewrite these.
76 function ($value, array $keys) {
77 $key = array_pop($keys);
78 // Only worried about a top-level "router" key.
79 return $key === 'router' && count($keys) === 0 && is_array($value)
80 ? [$this, 'noopReplacement']
81 : null;
82 },
83
84 // service- and pluginmanager handling
85 function ($value) {
86 return is_array($value) && array_intersect_key(self::SERVICE_MANAGER_KEYS_OF_INTEREST, $value) !== []
87 ? [$this, 'replaceDependencyConfiguration']
88 : null;
89 },
90
91 // Array values
92 function ($value, array $keys) {
93 return 0 !== count($keys) && is_array($value)
94 ? [$this, '__invoke']
95 : null;
96 },
97 ];
98 }
99
100 /**
101 * @param string[] $keys Hierarchy of keys, for determining location in
102 * nested configuration.
103 * @return array
104 */
105 public function __invoke(array $config, array $keys = [])
106 {
107 $rewritten = [];
108
109 foreach ($config as $key => $value) {
110 // Determine new key from replacements
111 $newKey = is_string($key) ? $this->replace($key, $keys) : $key;
112
113 // Keep original values with original key, if the key has changed, but only at the top-level.
114 if (empty($keys) && $newKey !== $key) {
115 $rewritten[$key] = $value;
116 }
117
118 // Perform value replacements, if any
119 $newValue = $this->replace($value, $keys, $newKey);
120
121 // Key does not already exist and/or is not an array value
122 if (! array_key_exists($newKey, $rewritten) || ! is_array($rewritten[$newKey])) {
123 // Do not overwrite existing values with null values
124 $rewritten[$newKey] = array_key_exists($newKey, $rewritten) && null === $newValue
125 ? $rewritten[$newKey]
126 : $newValue;
127 continue;
128 }
129
130 // New value is null; nothing to do.
131 if (null === $newValue) {
132 continue;
133 }
134
135 // Key already exists as an array value, but $value is not an array
136 if (! is_array($newValue)) {
137 $rewritten[$newKey][] = $newValue;
138 continue;
139 }
140
141 // Key already exists as an array value, and $value is also an array
142 $rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue);
143 }
144
145 return $rewritten;
146 }
147
148 /**
149 * Perform substitutions as needed on an individual value.
150 *
151 * The $key is provided to allow fine-grained selection of rewrite rules.
152 *
153 * @param mixed $value
154 * @param string[] $keys Key hierarchy
155 * @param null|int|string $key
156 * @return mixed
157 */
158 private function replace($value, array $keys, $key = null)
159 {
160 // Add new key to the list of keys.
161 // We do not need to remove it later, as we are working on a copy of the array.
162 array_push($keys, $key);
163
164 // Identify rewrite strategy and perform replacements
165 $rewriteRule = $this->replacementRuleMatch($value, $keys);
166 return $rewriteRule($value, $keys);
167 }
168
169 /**
170 * Merge two arrays together.
171 *
172 * If an integer key exists in both arrays, the value from the second array
173 * will be appended to the first array. If both values are arrays, they are
174 * merged together, else the value of the second array overwrites the one
175 * of the first array.
176 *
177 * Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge
178 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
179 *
180 * @return array
181 */
182 public static function merge(array $a, array $b)
183 {
184 foreach ($b as $key => $value) {
185 if (! isset($a[$key]) && ! array_key_exists($key, $a)) {
186 $a[$key] = $value;
187 continue;
188 }
189
190 if (null === $value && array_key_exists($key, $a)) {
191 // Leave as-is if value from $b is null
192 continue;
193 }
194
195 if (is_int($key)) {
196 $a[] = $value;
197 continue;
198 }
199
200 if (is_array($value) && is_array($a[$key])) {
201 $a[$key] = static::merge($a[$key], $value);
202 continue;
203 }
204
205 $a[$key] = $value;
206 }
207
208 return $a;
209 }
210
211 /**
212 * @param mixed $value
213 * @param null|int|string $key
214 * @return callable Callable to invoke with value
215 */
216 private function replacementRuleMatch($value, $key = null)
217 {
218 foreach ($this->rulesets as $ruleset) {
219 $result = $ruleset($value, $key);
220 if (is_callable($result)) {
221 return $result;
222 }
223 }
224 return [$this, 'fallbackReplacement'];
225 }
226
227 /**
228 * Replace a value using the translation table, if the value is a string.
229 *
230 * @param mixed $value
231 * @return mixed
232 */
233 private function fallbackReplacement($value)
234 {
235 return is_string($value)
236 ? $this->replacements->replace($value)
237 : $value;
238 }
239
240 /**
241 * Replace a value matched exactly.
242 *
243 * @param mixed $value
244 * @return mixed
245 */
246 private function replaceExactValue($value)
247 {
248 return $this->exactReplacements[$value];
249 }
250
251 private function replaceDependencyConfiguration(array $config)
252 {
253 $aliases = isset($config['aliases']) && is_array($config['aliases'])
254 ? $this->replaceDependencyAliases($config['aliases'])
255 : [];
256
257 if ($aliases) {
258 $config['aliases'] = $aliases;
259 }
260
261 $config = $this->replaceDependencyInvokables($config);
262 $config = $this->replaceDependencyFactories($config);
263 $config = $this->replaceDependencyServices($config);
264
265 $keys = self::SERVICE_MANAGER_KEYS_OF_INTEREST;
266 foreach ($config as $key => $data) {
267 if (isset($keys[$key])) {
268 continue;
269 }
270
271 $config[$key] = is_array($data) ? $this->__invoke($data, [$key]) : $data;
272 }
273
274 return $config;
275 }
276
277 /**
278 * Rewrite dependency aliases array
279 *
280 * In this case, we want to keep the alias as-is, but rewrite the target.
281 *
282 * We need also provide an additional alias if the alias key is a legacy class.
283 *
284 * @return array
285 */
286 private function replaceDependencyAliases(array $aliases)
287 {
288 foreach ($aliases as $alias => $target) {
289 if (! is_string($alias) || ! is_string($target)) {
290 continue;
291 }
292
293 $newTarget = $this->replacements->replace($target);
294 $newAlias = $this->replacements->replace($alias);
295
296 $notIn = [$newTarget];
297 $name = $newTarget;
298 while (isset($aliases[$name])) {
299 $notIn[] = $aliases[$name];
300 $name = $aliases[$name];
301 }
302
303 if ($newAlias === $alias && ! in_array($alias, $notIn, true)) {
304 $aliases[$alias] = $newTarget;
305 continue;
306 }
307
308 if (isset($aliases[$newAlias])) {
309 continue;
310 }
311
312 if (! in_array($newAlias, $notIn, true)) {
313 $aliases[$alias] = $newAlias;
314 $aliases[$newAlias] = $newTarget;
315 }
316 }
317
318 return $aliases;
319 }
320
321 /**
322 * Rewrite dependency invokables array
323 *
324 * In this case, we want to keep the alias as-is, but rewrite the target.
325 *
326 * We need also provide an additional alias if invokable is defined with
327 * an alias which is a legacy class.
328 *
329 * @return array
330 */
331 private function replaceDependencyInvokables(array $config)
332 {
333 if (empty($config['invokables']) || ! is_array($config['invokables'])) {
334 return $config;
335 }
336
337 foreach ($config['invokables'] as $alias => $target) {
338 if (! is_string($alias)) {
339 continue;
340 }
341
342 $newTarget = $this->replacements->replace($target);
343 $newAlias = $this->replacements->replace($alias);
344
345 if ($alias === $target || isset($config['aliases'][$newAlias])) {
346 $config['invokables'][$alias] = $newTarget;
347 continue;
348 }
349
350 $config['invokables'][$newAlias] = $newTarget;
351
352 if ($newAlias === $alias) {
353 continue;
354 }
355
356 $config['aliases'][$alias] = $newAlias;
357
358 unset($config['invokables'][$alias]);
359 }
360
361 return $config;
362 }
363
364 /**
365 * @param mixed $value
366 * @return mixed Returns $value verbatim.
367 */
368 private function noopReplacement($value)
369 {
370 return $value;
371 }
372
373 private function replaceDependencyFactories(array $config)
374 {
375 if (empty($config['factories']) || ! is_array($config['factories'])) {
376 return $config;
377 }
378
379 foreach ($config['factories'] as $service => $factory) {
380 if (! is_string($service)) {
381 continue;
382 }
383
384 $replacedService = $this->replacements->replace($service);
385 $factory = is_string($factory) ? $this->replacements->replace($factory) : $factory;
386 $config['factories'][$replacedService] = $factory;
387
388 if ($replacedService === $service) {
389 continue;
390 }
391
392 unset($config['factories'][$service]);
393 if (isset($config['aliases'][$service])) {
394 continue;
395 }
396
397 $config['aliases'][$service] = $replacedService;
398 }
399
400 return $config;
401 }
402
403 private function replaceDependencyServices(array $config)
404 {
405 if (empty($config['services']) || ! is_array($config['services'])) {
406 return $config;
407 }
408
409 foreach ($config['services'] as $service => $serviceInstance) {
410 if (! is_string($service)) {
411 continue;
412 }
413
414 $replacedService = $this->replacements->replace($service);
415 $serviceInstance = is_array($serviceInstance) ? $this->__invoke($serviceInstance) : $serviceInstance;
416
417 $config['services'][$replacedService] = $serviceInstance;
418
419 if ($service === $replacedService) {
420 continue;
421 }
422
423 unset($config['services'][$service]);
424
425 if (isset($config['aliases'][$service])) {
426 continue;
427 }
428
429 $config['aliases'][$service] = $replacedService;
430 }
431
432 return $config;
433 }
434 }