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
9 namespace Laminas\ZendFrameworkBridge;
11 use function array_intersect_key;
12 use function array_key_exists;
13 use function array_pop;
14 use function array_push;
16 use function in_array;
17 use function is_array;
18 use function is_callable;
20 use function is_string;
22 class ConfigPostProcessor
25 const SERVICE_MANAGER_KEYS_OF_INTEREST = [
32 /** @var array String keys => string values */
33 private $exactReplacements = [
34 'zend-expressive' => 'mezzio',
35 'zf-apigility' => 'api-tools',
38 /** @var Replacements */
39 private $replacements;
41 /** @var callable[] */
44 public function __construct()
46 $this->replacements = new Replacements();
48 /* Define the rulesets for replacements.
50 * Each ruleset has the following signature:
53 * @param string[] $keys Full nested key hierarchy leading to the value
54 * @return null|callable
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.
60 * The callback should have the following signature:
63 * @param string[] $keys
64 * @return mixed The transformed value
69 return is_string($value) && isset($this->exactReplacements[$value])
70 ? [$this, 'replaceExactValue']
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']
84 // service- and pluginmanager handling
86 return is_array($value) && array_intersect_key(self::SERVICE_MANAGER_KEYS_OF_INTEREST, $value) !== []
87 ? [$this, 'replaceDependencyConfiguration']
92 function ($value, array $keys) {
93 return 0 !== count($keys) && is_array($value)
101 * @param string[] $keys Hierarchy of keys, for determining location in
102 * nested configuration.
105 public function __invoke(array $config, array $keys = [])
109 foreach ($config as $key => $value) {
110 // Determine new key from replacements
111 $newKey = is_string($key) ? $this->replace($key, $keys) : $key;
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;
118 // Perform value replacements, if any
119 $newValue = $this->replace($value, $keys, $newKey);
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]
130 // New value is null; nothing to do.
131 if (null === $newValue) {
135 // Key already exists as an array value, but $value is not an array
136 if (! is_array($newValue)) {
137 $rewritten[$newKey][] = $newValue;
141 // Key already exists as an array value, and $value is also an array
142 $rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue);
149 * Perform substitutions as needed on an individual value.
151 * The $key is provided to allow fine-grained selection of rewrite rules.
153 * @param mixed $value
154 * @param string[] $keys Key hierarchy
155 * @param null|int|string $key
158 private function replace($value, array $keys, $key = null)
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);
164 // Identify rewrite strategy and perform replacements
165 $rewriteRule = $this->replacementRuleMatch($value, $keys);
166 return $rewriteRule($value, $keys);
170 * Merge two arrays together.
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.
177 * Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge
178 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
182 public static function merge(array $a, array $b)
184 foreach ($b as $key => $value) {
185 if (! isset($a[$key]) && ! array_key_exists($key, $a)) {
190 if (null === $value && array_key_exists($key, $a)) {
191 // Leave as-is if value from $b is null
200 if (is_array($value) && is_array($a[$key])) {
201 $a[$key] = static::merge($a[$key], $value);
212 * @param mixed $value
213 * @param null|int|string $key
214 * @return callable Callable to invoke with value
216 private function replacementRuleMatch($value, $key = null)
218 foreach ($this->rulesets as $ruleset) {
219 $result = $ruleset($value, $key);
220 if (is_callable($result)) {
224 return [$this, 'fallbackReplacement'];
228 * Replace a value using the translation table, if the value is a string.
230 * @param mixed $value
233 private function fallbackReplacement($value)
235 return is_string($value)
236 ? $this->replacements->replace($value)
241 * Replace a value matched exactly.
243 * @param mixed $value
246 private function replaceExactValue($value)
248 return $this->exactReplacements[$value];
251 private function replaceDependencyConfiguration(array $config)
253 $aliases = isset($config['aliases']) && is_array($config['aliases'])
254 ? $this->replaceDependencyAliases($config['aliases'])
258 $config['aliases'] = $aliases;
261 $config = $this->replaceDependencyInvokables($config);
262 $config = $this->replaceDependencyFactories($config);
263 $config = $this->replaceDependencyServices($config);
265 $keys = self::SERVICE_MANAGER_KEYS_OF_INTEREST;
266 foreach ($config as $key => $data) {
267 if (isset($keys[$key])) {
271 $config[$key] = is_array($data) ? $this->__invoke($data, [$key]) : $data;
278 * Rewrite dependency aliases array
280 * In this case, we want to keep the alias as-is, but rewrite the target.
282 * We need also provide an additional alias if the alias key is a legacy class.
286 private function replaceDependencyAliases(array $aliases)
288 foreach ($aliases as $alias => $target) {
289 if (! is_string($alias) || ! is_string($target)) {
293 $newTarget = $this->replacements->replace($target);
294 $newAlias = $this->replacements->replace($alias);
296 $notIn = [$newTarget];
298 while (isset($aliases[$name])) {
299 $notIn[] = $aliases[$name];
300 $name = $aliases[$name];
303 if ($newAlias === $alias && ! in_array($alias, $notIn, true)) {
304 $aliases[$alias] = $newTarget;
308 if (isset($aliases[$newAlias])) {
312 if (! in_array($newAlias, $notIn, true)) {
313 $aliases[$alias] = $newAlias;
314 $aliases[$newAlias] = $newTarget;
322 * Rewrite dependency invokables array
324 * In this case, we want to keep the alias as-is, but rewrite the target.
326 * We need also provide an additional alias if invokable is defined with
327 * an alias which is a legacy class.
331 private function replaceDependencyInvokables(array $config)
333 if (empty($config['invokables']) || ! is_array($config['invokables'])) {
337 foreach ($config['invokables'] as $alias => $target) {
338 if (! is_string($alias)) {
342 $newTarget = $this->replacements->replace($target);
343 $newAlias = $this->replacements->replace($alias);
345 if ($alias === $target || isset($config['aliases'][$newAlias])) {
346 $config['invokables'][$alias] = $newTarget;
350 $config['invokables'][$newAlias] = $newTarget;
352 if ($newAlias === $alias) {
356 $config['aliases'][$alias] = $newAlias;
358 unset($config['invokables'][$alias]);
365 * @param mixed $value
366 * @return mixed Returns $value verbatim.
368 private function noopReplacement($value)
373 private function replaceDependencyFactories(array $config)
375 if (empty($config['factories']) || ! is_array($config['factories'])) {
379 foreach ($config['factories'] as $service => $factory) {
380 if (! is_string($service)) {
384 $replacedService = $this->replacements->replace($service);
385 $factory = is_string($factory) ? $this->replacements->replace($factory) : $factory;
386 $config['factories'][$replacedService] = $factory;
388 if ($replacedService === $service) {
392 unset($config['factories'][$service]);
393 if (isset($config['aliases'][$service])) {
397 $config['aliases'][$service] = $replacedService;
403 private function replaceDependencyServices(array $config)
405 if (empty($config['services']) || ! is_array($config['services'])) {
409 foreach ($config['services'] as $service => $serviceInstance) {
410 if (! is_string($service)) {
414 $replacedService = $this->replacements->replace($service);
415 $serviceInstance = is_array($serviceInstance) ? $this->__invoke($serviceInstance) : $serviceInstance;
417 $config['services'][$replacedService] = $serviceInstance;
419 if ($service === $replacedService) {
423 unset($config['services'][$service]);
425 if (isset($config['aliases'][$service])) {
429 $config['aliases'][$service] = $replacedService;