true, 'factories' => true, 'invokables' => true, 'services' => true, ]; /** @var array String keys => string values */ private $exactReplacements = [ 'zend-expressive' => 'mezzio', 'zf-apigility' => 'api-tools', ]; /** * @psalm-suppress PropertyNotSetInConstructor Initialized during call to the only public method __invoke() * @var Replacements */ private $replacements; /** @var callable[] */ private $rulesets; public function __construct() { /* Define the rulesets for replacements. * * Each ruleset has the following signature: * * @param mixed $value * @param string[] $keys Full nested key hierarchy leading to the value * @return null|callable * * If no match is made, a null is returned, allowing it to fallback to * the next ruleset in the list. If a match is made, a callback is returned, * and that will be used to perform the replacement on the value. * * The callback should have the following signature: * * @param mixed $value * @param string[] $keys * @return mixed The transformed value */ $this->rulesets = [ // Exact values function ($value) { return is_string($value) && isset($this->exactReplacements[$value]) ? [$this, 'replaceExactValue'] : null; }, // Router (MVC applications) // We do not want to rewrite these. function ($value, array $keys) { $key = array_pop($keys); // Only worried about a top-level "router" key. return $key === 'router' && $keys === [] && is_array($value) ? [$this, 'noopReplacement'] : null; }, // service- and pluginmanager handling function ($value) { return is_array($value) && array_intersect_key(self::SERVICE_MANAGER_KEYS_OF_INTEREST, $value) !== [] ? [$this, 'replaceDependencyConfiguration'] : null; }, // Array values function ($value, array $keys) { return $keys !== [] && is_array($value) ? [$this, 'processConfig'] : null; }, ]; } /** * @param string[] $keys Hierarchy of keys, for determining location in * nested configuration. * @return array */ public function __invoke(array $config, array $keys = []) { $this->replacements = $this->initializeReplacements($config); return $this->processConfig($config, $keys); } /** * Perform substitutions as needed on an individual value. * * The $key is provided to allow fine-grained selection of rewrite rules. * * @param mixed $value * @param string[] $keys Key hierarchy * @param null|int|string $key * @return mixed */ private function replace($value, array $keys, $key = null) { // Add new key to the list of keys. // We do not need to remove it later, as we are working on a copy of the array. $keys[] = $key; // Identify rewrite strategy and perform replacements $rewriteRule = $this->replacementRuleMatch($value, $keys); return $rewriteRule($value, $keys); } /** * Merge two arrays together. * * If an integer key exists in both arrays, the value from the second array * will be appended to the first array. If both values are arrays, they are * merged together, else the value of the second array overwrites the one * of the first array. * * Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) * * @return array */ public static function merge(array $a, array $b) { foreach ($b as $key => $value) { if (! isset($a[$key]) && ! array_key_exists($key, $a)) { $a[$key] = $value; continue; } if (null === $value && array_key_exists($key, $a)) { // Leave as-is if value from $b is null continue; } if (is_int($key)) { $a[] = $value; continue; } if (is_array($value) && is_array($a[$key])) { $a[$key] = static::merge($a[$key], $value); continue; } $a[$key] = $value; } return $a; } /** * @param mixed $value * @param null|int|string $key * @return callable Callable to invoke with value */ private function replacementRuleMatch($value, $key = null) { foreach ($this->rulesets as $ruleset) { $result = $ruleset($value, $key); if (is_callable($result)) { return $result; } } return [$this, 'fallbackReplacement']; } /** * Replace a value using the translation table, if the value is a string. * * @param mixed $value * @return mixed */ private function fallbackReplacement($value) { return is_string($value) ? $this->replacements->replace($value) : $value; } /** * Replace a value matched exactly. * * @param mixed $value * @return mixed */ private function replaceExactValue($value) { return $this->exactReplacements[$value]; } private function replaceDependencyConfiguration(array $config) { $aliases = isset($config['aliases']) && is_array($config['aliases']) ? $this->replaceDependencyAliases($config['aliases']) : []; if ($aliases) { $config['aliases'] = $aliases; } $config = $this->replaceDependencyInvokables($config); $config = $this->replaceDependencyFactories($config); $config = $this->replaceDependencyServices($config); $keys = self::SERVICE_MANAGER_KEYS_OF_INTEREST; foreach ($config as $key => $data) { if (isset($keys[$key])) { continue; } $config[$key] = is_array($data) ? $this->processConfig($data, [$key]) : $data; } return $config; } /** * Rewrite dependency aliases array * * In this case, we want to keep the alias as-is, but rewrite the target. * * We need also provide an additional alias if the alias key is a legacy class. * * @return array */ private function replaceDependencyAliases(array $aliases) { foreach ($aliases as $alias => $target) { if (! is_string($alias) || ! is_string($target)) { continue; } $newTarget = $this->replacements->replace($target); $newAlias = $this->replacements->replace($alias); $notIn = [$newTarget]; $name = $newTarget; while (isset($aliases[$name])) { $notIn[] = $aliases[$name]; $name = $aliases[$name]; } if ($newAlias === $alias && ! in_array($alias, $notIn, true)) { $aliases[$alias] = $newTarget; continue; } if (isset($aliases[$newAlias])) { continue; } if (! in_array($newAlias, $notIn, true)) { $aliases[$alias] = $newAlias; $aliases[$newAlias] = $newTarget; } } return $aliases; } /** * Rewrite dependency invokables array * * In this case, we want to keep the alias as-is, but rewrite the target. * * We need also provide an additional alias if invokable is defined with * an alias which is a legacy class. * * @return array */ private function replaceDependencyInvokables(array $config) { if (empty($config['invokables']) || ! is_array($config['invokables'])) { return $config; } foreach ($config['invokables'] as $alias => $target) { if (! is_string($alias)) { continue; } $newTarget = $this->replacements->replace($target); $newAlias = $this->replacements->replace($alias); if ($alias === $target || isset($config['aliases'][$newAlias])) { $config['invokables'][$alias] = $newTarget; continue; } $config['invokables'][$newAlias] = $newTarget; if ($newAlias === $alias) { continue; } $config['aliases'][$alias] = $newAlias; unset($config['invokables'][$alias]); } return $config; } /** * @param mixed $value * @return mixed Returns $value verbatim. */ private function noopReplacement($value) { return $value; } private function replaceDependencyFactories(array $config) { if (empty($config['factories']) || ! is_array($config['factories'])) { return $config; } foreach ($config['factories'] as $service => $factory) { if (! is_string($service)) { continue; } $replacedService = $this->replacements->replace($service); $factory = is_string($factory) ? $this->replacements->replace($factory) : $factory; $config['factories'][$replacedService] = $factory; if ($replacedService === $service) { continue; } unset($config['factories'][$service]); if (isset($config['aliases'][$service])) { continue; } $config['aliases'][$service] = $replacedService; } return $config; } private function replaceDependencyServices(array $config) { if (empty($config['services']) || ! is_array($config['services'])) { return $config; } foreach ($config['services'] as $service => $serviceInstance) { if (! is_string($service)) { continue; } $replacedService = $this->replacements->replace($service); $serviceInstance = is_array($serviceInstance) ? $this->processConfig($serviceInstance) : $serviceInstance; $config['services'][$replacedService] = $serviceInstance; if ($service === $replacedService) { continue; } unset($config['services'][$service]); if (isset($config['aliases'][$service])) { continue; } $config['aliases'][$service] = $replacedService; } return $config; } private function initializeReplacements(array $config): Replacements { $replacements = $config['laminas-zendframework-bridge']['replacements'] ?? []; if (! is_array($replacements)) { throw new RuntimeException(sprintf( 'Invalid laminas-zendframework-bridge.replacements configuration;' . ' value MUST be an array; received %s', is_object($replacements) ? get_class($replacements) : gettype($replacements) )); } foreach ($replacements as $lookup => $replacement) { if ( ! is_string($lookup) || ! is_string($replacement) || preg_match('/^\s*$/', $lookup) || preg_match('/^\s*$/', $replacement) ) { throw new RuntimeException( 'Invalid lookup or replacement in laminas-zendframework-bridge.replacements configuration;' . ' all keys and values MUST be non-empty strings.' ); } } return new Replacements($replacements); } /** * @param string[] $keys Hierarchy of keys, for determining location in * nested configuration. */ private function processConfig(array $config, array $keys = []): array { $rewritten = []; foreach ($config as $key => $value) { // Do not rewrite configuration for the bridge if ($key === 'laminas-zendframework-bridge') { $rewritten[$key] = $value; continue; } // Determine new key from replacements $newKey = is_string($key) ? $this->replace($key, $keys) : $key; // Keep original values with original key, if the key has changed, but only at the top-level. if (empty($keys) && $newKey !== $key) { $rewritten[$key] = $value; } // Perform value replacements, if any $newValue = $this->replace($value, $keys, $newKey); // Key does not already exist and/or is not an array value if (!array_key_exists($newKey, $rewritten) || !is_array($rewritten[$newKey])) { // Do not overwrite existing values with null values $rewritten[$newKey] = array_key_exists($newKey, $rewritten) && null === $newValue ? $rewritten[$newKey] : $newValue; continue; } // New value is null; nothing to do. if (null === $newValue) { continue; } // Key already exists as an array value, but $value is not an array if (!is_array($newValue)) { $rewritten[$newKey][] = $newValue; continue; } // Key already exists as an array value, and $value is also an array $rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue); } return $rewritten; } }