3 namespace Laminas\ZendFrameworkBridge;
5 use function array_intersect_key;
6 use function array_key_exists;
7 use function array_pop;
10 use function is_callable;
12 use function is_string;
14 class ConfigPostProcessor
17 const SERVICE_MANAGER_KEYS_OF_INTEREST = [
24 /** @var array String keys => string values */
25 private $exactReplacements = [
26 'zend-expressive' => 'mezzio',
27 'zf-apigility' => 'api-tools',
30 /** @var Replacements */
31 private $replacements;
33 /** @var callable[] */
36 public function __construct()
38 $this->replacements = new Replacements();
40 /* Define the rulesets for replacements.
42 * Each ruleset has the following signature:
45 * @param string[] $keys Full nested key hierarchy leading to the value
46 * @return null|callable
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.
52 * The callback should have the following signature:
55 * @param string[] $keys
56 * @return mixed The transformed value
61 return is_string($value) && isset($this->exactReplacements[$value])
62 ? [$this, 'replaceExactValue']
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']
76 // service- and pluginmanager handling
78 return is_array($value) && array_intersect_key(self::SERVICE_MANAGER_KEYS_OF_INTEREST, $value) !== []
79 ? [$this, 'replaceDependencyConfiguration']
84 function ($value, array $keys) {
85 return $keys !== [] && is_array($value)
93 * @param string[] $keys Hierarchy of keys, for determining location in
94 * nested configuration.
97 public function __invoke(array $config, array $keys = [])
101 foreach ($config as $key => $value) {
102 // Determine new key from replacements
103 $newKey = is_string($key) ? $this->replace($key, $keys) : $key;
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;
110 // Perform value replacements, if any
111 $newValue = $this->replace($value, $keys, $newKey);
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]
122 // New value is null; nothing to do.
123 if (null === $newValue) {
127 // Key already exists as an array value, but $value is not an array
128 if (! is_array($newValue)) {
129 $rewritten[$newKey][] = $newValue;
133 // Key already exists as an array value, and $value is also an array
134 $rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue);
141 * Perform substitutions as needed on an individual value.
143 * The $key is provided to allow fine-grained selection of rewrite rules.
145 * @param mixed $value
146 * @param string[] $keys Key hierarchy
147 * @param null|int|string $key
150 private function replace($value, array $keys, $key = null)
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.
156 // Identify rewrite strategy and perform replacements
157 $rewriteRule = $this->replacementRuleMatch($value, $keys);
158 return $rewriteRule($value, $keys);
162 * Merge two arrays together.
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.
169 * Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge
170 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
174 public static function merge(array $a, array $b)
176 foreach ($b as $key => $value) {
177 if (! isset($a[$key]) && ! array_key_exists($key, $a)) {
182 if (null === $value && array_key_exists($key, $a)) {
183 // Leave as-is if value from $b is null
192 if (is_array($value) && is_array($a[$key])) {
193 $a[$key] = static::merge($a[$key], $value);
204 * @param mixed $value
205 * @param null|int|string $key
206 * @return callable Callable to invoke with value
208 private function replacementRuleMatch($value, $key = null)
210 foreach ($this->rulesets as $ruleset) {
211 $result = $ruleset($value, $key);
212 if (is_callable($result)) {
216 return [$this, 'fallbackReplacement'];
220 * Replace a value using the translation table, if the value is a string.
222 * @param mixed $value
225 private function fallbackReplacement($value)
227 return is_string($value)
228 ? $this->replacements->replace($value)
233 * Replace a value matched exactly.
235 * @param mixed $value
238 private function replaceExactValue($value)
240 return $this->exactReplacements[$value];
243 private function replaceDependencyConfiguration(array $config)
245 $aliases = isset($config['aliases']) && is_array($config['aliases'])
246 ? $this->replaceDependencyAliases($config['aliases'])
250 $config['aliases'] = $aliases;
253 $config = $this->replaceDependencyInvokables($config);
254 $config = $this->replaceDependencyFactories($config);
255 $config = $this->replaceDependencyServices($config);
257 $keys = self::SERVICE_MANAGER_KEYS_OF_INTEREST;
258 foreach ($config as $key => $data) {
259 if (isset($keys[$key])) {
263 $config[$key] = is_array($data) ? $this->__invoke($data, [$key]) : $data;
270 * Rewrite dependency aliases array
272 * In this case, we want to keep the alias as-is, but rewrite the target.
274 * We need also provide an additional alias if the alias key is a legacy class.
278 private function replaceDependencyAliases(array $aliases)
280 foreach ($aliases as $alias => $target) {
281 if (! is_string($alias) || ! is_string($target)) {
285 $newTarget = $this->replacements->replace($target);
286 $newAlias = $this->replacements->replace($alias);
288 $notIn = [$newTarget];
290 while (isset($aliases[$name])) {
291 $notIn[] = $aliases[$name];
292 $name = $aliases[$name];
295 if ($newAlias === $alias && ! in_array($alias, $notIn, true)) {
296 $aliases[$alias] = $newTarget;
300 if (isset($aliases[$newAlias])) {
304 if (! in_array($newAlias, $notIn, true)) {
305 $aliases[$alias] = $newAlias;
306 $aliases[$newAlias] = $newTarget;
314 * Rewrite dependency invokables array
316 * In this case, we want to keep the alias as-is, but rewrite the target.
318 * We need also provide an additional alias if invokable is defined with
319 * an alias which is a legacy class.
323 private function replaceDependencyInvokables(array $config)
325 if (empty($config['invokables']) || ! is_array($config['invokables'])) {
329 foreach ($config['invokables'] as $alias => $target) {
330 if (! is_string($alias)) {
334 $newTarget = $this->replacements->replace($target);
335 $newAlias = $this->replacements->replace($alias);
337 if ($alias === $target || isset($config['aliases'][$newAlias])) {
338 $config['invokables'][$alias] = $newTarget;
342 $config['invokables'][$newAlias] = $newTarget;
344 if ($newAlias === $alias) {
348 $config['aliases'][$alias] = $newAlias;
350 unset($config['invokables'][$alias]);
357 * @param mixed $value
358 * @return mixed Returns $value verbatim.
360 private function noopReplacement($value)
365 private function replaceDependencyFactories(array $config)
367 if (empty($config['factories']) || ! is_array($config['factories'])) {
371 foreach ($config['factories'] as $service => $factory) {
372 if (! is_string($service)) {
376 $replacedService = $this->replacements->replace($service);
377 $factory = is_string($factory) ? $this->replacements->replace($factory) : $factory;
378 $config['factories'][$replacedService] = $factory;
380 if ($replacedService === $service) {
384 unset($config['factories'][$service]);
385 if (isset($config['aliases'][$service])) {
389 $config['aliases'][$service] = $replacedService;
395 private function replaceDependencyServices(array $config)
397 if (empty($config['services']) || ! is_array($config['services'])) {
401 foreach ($config['services'] as $service => $serviceInstance) {
402 if (! is_string($service)) {
406 $replacedService = $this->replacements->replace($service);
407 $serviceInstance = is_array($serviceInstance) ? $this->__invoke($serviceInstance) : $serviceInstance;
409 $config['services'][$replacedService] = $serviceInstance;
411 if ($service === $replacedService) {
415 unset($config['services'][$service]);
417 if (isset($config['aliases'][$service])) {
421 $config['aliases'][$service] = $replacedService;