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