default services conflit ?

This commit is contained in:
armansansd
2022-04-27 11:30:43 +02:00
parent 28190a5749
commit 8bb1064a3b
8132 changed files with 900138 additions and 426 deletions

View File

@@ -0,0 +1,160 @@
<?php
namespace Consolidation\Config;
use Dflydev\DotAccessData\Data;
use Consolidation\Config\Util\ConfigInterpolatorInterface;
use Consolidation\Config\Util\ConfigInterpolatorTrait;
class Config implements ConfigInterface, ConfigInterpolatorInterface
{
use ConfigInterpolatorTrait;
/**
* @var Data
*/
protected $config;
/**
* TODO: make this private in 2.0 to prevent being saved as an array
* Making private now breaks backward compatibility
*
* @var Data
*/
protected $defaults;
/**
* Create a new configuration object, and initialize it with
* the provided nested array containing configuration data.
*
* @param array $data - Config data to store
*/
public function __construct(array $data = null)
{
$this->config = new Data($data);
$this->setDefaults(new Data());
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return ($this->config->has($key));
}
/**
* {@inheritdoc}
*/
public function get($key, $defaultFallback = null)
{
if ($this->has($key)) {
return $this->config->get($key);
}
return $this->getDefault($key, $defaultFallback);
}
/**
* {@inheritdoc}
*/
public function set($key, $value)
{
$this->config->set($key, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function import($data)
{
return $this->replace($data);
}
/**
* {@inheritdoc}
*/
public function replace($data)
{
$this->config = new Data($data);
return $this;
}
/**
* {@inheritdoc}
*/
public function combine($data)
{
if (!empty($data)) {
$this->config->import($data, true);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function export()
{
return $this->config->export();
}
/**
* {@inheritdoc}
*/
public function hasDefault($key)
{
return $this->getDefaults()->has($key);
}
/**
* {@inheritdoc}
*/
public function getDefault($key, $defaultFallback = null)
{
return $this->hasDefault($key) ? $this->getDefaults()->get($key) : $defaultFallback;
}
/**
* {@inheritdoc}
*/
public function setDefault($key, $value)
{
$this->getDefaults()->set($key, $value);
return $this;
}
/**
* Return the class $defaults property and ensure it's a Data object
* TODO: remove Data object validation in 2.0
*
* @return Data
*/
protected function getDefaults()
{
// Ensure $this->defaults is a Data object (not an array)
if (!$this->defaults instanceof Data) {
$this->setDefaults($this->defaults);
}
return $this->defaults;
}
/**
* Sets the $defaults class parameter
* TODO: remove support for array in 2.0 as this would currently break backward compatibility
*
* @param Data|array $defaults
*
* @throws \Exception
*/
protected function setDefaults($defaults)
{
if (is_array($defaults)) {
$this->defaults = new Data($defaults);
} elseif ($defaults instanceof Data) {
$this->defaults = $defaults;
} else {
throw new \Exception("Unknown type provided for \$defaults");
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Consolidation\Config;
interface ConfigAwareInterface
{
/**
* Set the config reference
*
* @param ConfigInterface $config
*
* @return $this
*/
public function setConfig(ConfigInterface $config);
/**
* Get the config reference
*
* @return ConfigInterface
*/
public function getConfig();
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Consolidation\Config;
trait ConfigAwareTrait
{
/**
* @var ConfigInterface
*/
protected $config;
/**
* Set the config management object.
*
* @param ConfigInterface $config
*
* @return $this
*/
public function setConfig(ConfigInterface $config)
{
$this->config = $config;
return $this;
}
/**
* Get the config management object.
*
* @return ConfigInterface
*/
public function getConfig()
{
return $this->config;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Consolidation\Config;
interface ConfigInterface
{
/**
* Determine if a non-default config value exists.
*/
public function has($key);
/**
* Fetch a configuration value
*
* @param string $key Which config item to look up
* @param string|null $defaultFallback Fallback default value to use when
* configuration object has neither a value nor a default. Use is
* discouraged; use default context in ConfigOverlay, or provide defaults
* using a config processor.
*
* @return mixed
*/
public function get($key, $defaultFallback = null);
/**
* Set a config value
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function set($key, $value);
/**
* Import configuration from the provided nexted array, replacing whatever
* was here previously. No processing is done on the provided data.
*
* @deprecated Use 'replace'. Dflydev\DotAccessData\Data::import() merges, which is confusing, since this method replaces.
*
* @param array $data
* @return Config
*/
public function import($data);
/**
* Load configuration from the provided nexted array, replacing whatever
* was here previously. No processing is done on the provided data.
*
* TODO: This will become a required method in version 2.0. Adding now
* would break clients that implement ConfigInterface.
*
* @param array $data
* @return Config
*/
// public function replace($data);
/**
* Import configuration from the provided nexted array, merging with whatever
* was here previously. No processing is done on the provided data.
* Any data provided to the combine() method will overwrite existing data
* with the same key.
*
* TODO: This will become a required method in version 2.0. Adding now
* would break clients that implement ConfigInterface.
*
* @param array $data
* @return Config
*/
// public function combine($data);
/**
* Export all configuration as a nested array.
*/
public function export();
/**
* Return the default value for a given configuration item.
*
* @param string $key
*
* @return mixed
*/
public function hasDefault($key);
/**
* Return the default value for a given configuration item.
*
* @param string $key
* @param mixed $defaultFallback
*
* @return mixed
*/
public function getDefault($key, $defaultFallback = null);
/**
* Set the default value for a configuration setting. This allows us to
* set defaults either before or after more specific configuration values
* are loaded. Keeping defaults separate from current settings also
* allows us to determine when a setting has been overridden.
*
* @param string $key
* @param string $value
*/
public function setDefault($key, $value);
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Consolidation\Config;
interface GlobalOptionDefaultValuesInterface
{
/**
* Return an associative array of option-key => default-value
*/
public function getGlobalOptionDefaultValues();
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\ConfigInterface;
use Consolidation\Config\Util\ConfigFallback;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputOption;
class ConfigForCommand implements EventSubscriberInterface
{
protected $config;
protected $application;
public function __construct(ConfigInterface $config)
{
$this->config = $config;
}
public function setApplication(Application $application)
{
$this->application = $application;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [ConsoleEvents::COMMAND => 'injectConfiguration'];
}
/**
* Before a Console command runs, inject configuration settings
* for this command into the default value of the options of
* this command.
*
* @param \Symfony\Component\Console\Event\ConsoleCommandEvent $event
*/
public function injectConfiguration(ConsoleCommandEvent $event)
{
$command = $event->getCommand();
$this->injectConfigurationForGlobalOptions($event->getInput());
$this->injectConfigurationForCommand($command, $event->getInput());
$targetOfHelpCommand = $this->getHelpCommandTarget($command, $event->getInput());
if ($targetOfHelpCommand) {
$this->injectConfigurationForCommand($targetOfHelpCommand, $event->getInput());
}
}
protected function injectConfigurationForGlobalOptions($input)
{
if (!$this->application) {
return;
}
$configGroup = new ConfigFallback($this->config, 'options');
$definition = $this->application->getDefinition();
$options = $definition->getOptions();
return $this->injectConfigGroupIntoOptions($configGroup, $options, $input);
}
protected function injectConfigurationForCommand($command, $input)
{
$commandName = $command->getName();
$commandName = str_replace(':', '.', $commandName);
$configGroup = new ConfigFallback($this->config, $commandName, 'command.', '.options.');
$definition = $command->getDefinition();
$options = $definition->getOptions();
return $this->injectConfigGroupIntoOptions($configGroup, $options, $input);
}
protected function injectConfigGroupIntoOptions($configGroup, $options, $input)
{
foreach ($options as $option => $inputOption) {
$key = str_replace('.', '-', $option);
$value = $configGroup->get($key);
if ($value !== null) {
if (is_bool($value) && ($value == true)) {
$input->setOption($key, $value);
} elseif ($inputOption->acceptValue()) {
$inputOption->setDefault($value);
}
}
}
}
protected function getHelpCommandTarget($command, $input)
{
if (($command->getName() != 'help') || (!isset($this->application))) {
return false;
}
$this->fixInputForSymfony2($command, $input);
// Symfony Console helpfully swaps 'command_name' and 'command'
// depending on whether the user entered `help foo` or `--help foo`.
// One of these is always `help`, and the other is the command we
// are actually interested in.
$nameOfCommandToDescribe = $input->getArgument('command_name');
if ($nameOfCommandToDescribe == 'help') {
$nameOfCommandToDescribe = $input->getArgument('command');
}
return $this->application->find($nameOfCommandToDescribe);
}
protected function fixInputForSymfony2($command, $input)
{
// Symfony 3.x prepares $input for us; Symfony 2.x, on the other
// hand, passes it in prior to binding with the command definition,
// so we have to go to a little extra work. It may be inadvisable
// to do these steps for commands other than 'help'.
if (!$input->hasArgument('command_name')) {
$command->ignoreValidationErrors();
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Consolidation\Config\Inject;
use Consolidation\Config\Util\ConfigMerge;
/**
* Given an object that contains configuration methods, inject any
* configuration found in the configuration file.
*
* The proper use for this method is to call setter methods of the
* provided object. Using configuration to call methods that do work
* is an abuse of this mechanism.
*/
class ConfigForSetters
{
protected $config;
public function __construct($config, $group, $prefix = '', $postfix = '')
{
if (!empty($group) && empty($postfix)) {
$postfix = '.';
}
$this->config = new ConfigMerge($config, $group, $prefix, $postfix);
}
public function apply($object, $configurationKey)
{
$settings = $this->config->get($configurationKey);
foreach ($settings as $setterMethod => $args) {
$fn = [$object, $setterMethod];
if (is_callable($fn)) {
$result = call_user_func_array($fn, (array)$args);
// We require that $fn must only be used with setter methods.
// Setter methods are required to always return $this so that
// they may be chained. We will therefore throw an exception
// for any setter that returns something else.
if ($result != $object) {
$methodDescription = get_class($object) . "::$setterMethod";
$propertyDescription = $this->config->describe($configurationKey);
throw new \Exception("$methodDescription did not return '\$this' when processing $propertyDescription.");
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Consolidation\Config\Loader;
/**
* Load configuration files.
*/
abstract class ConfigLoader implements ConfigLoaderInterface
{
protected $config = [];
protected $source = '';
public function getSourceName()
{
return $this->source;
}
protected function setSourceName($source)
{
$this->source = $source;
return $this;
}
public function export()
{
return $this->config;
}
public function keys()
{
return array_keys($this->config);
}
abstract public function load($path);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\Config\Loader;
/**
* Load configuration files, and fill in any property values that
* need to be expanded.
*/
interface ConfigLoaderInterface
{
/**
* Convert loaded configuration into a simple php nested array.
*
* @return array
*/
public function export();
/**
* Return the top-level keys in the exported data.
*
* @return array
*/
public function keys();
/**
* Return a symbolic name for this configuration loader instance.
*/
public function getSourceName();
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Consolidation\Config\Loader;
use Grasmash\Expander\Expander;
use Consolidation\Config\Util\ArrayUtil;
/**
* The config processor combines multiple configuration
* files together, and processes them as necessary.
*/
class ConfigProcessor
{
protected $processedConfig = [];
protected $unprocessedConfig = [];
protected $nameOfItemsToMerge = [];
protected $expander;
public function __construct($expander = null)
{
$this->expander = $expander ?: new Expander();
}
/**
* By default, string config items always REPLACE, not MERGE when added
* from different sources. This method will allow applications to alter
* this behavior for specific items so that strings from multiple sources
* will be merged together into an array instead.
*/
public function useMergeStrategyForKeys($itemName)
{
if (is_array($itemName)) {
$this->nameOfItemsToMerge = array_merge($this->nameOfItemsToMerge, $itemName);
return $this;
}
$this->nameOfItemsToMerge[] = $itemName;
return $this;
}
/**
* Extend the configuration to be processed with the
* configuration provided by the specified loader.
*
* @param ConfigLoaderInterface $loader
*/
public function extend(ConfigLoaderInterface $loader)
{
return $this->addFromSource($loader->export(), $loader->getSourceName());
}
/**
* Extend the configuration to be processed with
* the provided nested array.
*
* @param array $data
*/
public function add($data)
{
$this->unprocessedConfig[] = $data;
return $this;
}
/**
* Extend the configuration to be processed with
* the provided nested array. Also record the name
* of the data source, if applicable.
*
* @param array $data
* @param string $source
*/
protected function addFromSource($data, $source = '')
{
if (empty($source)) {
return $this->add($data);
}
$this->unprocessedConfig[$source] = $data;
return $this;
}
/**
* Process all of the configuration that has been collected,
* and return a nested array.
*
* @return array
*/
public function export($referenceArray = [])
{
if (!empty($this->unprocessedConfig)) {
$this->processedConfig = $this->process(
$this->processedConfig,
$this->fetchUnprocessed(),
$referenceArray
);
}
return $this->processedConfig;
}
/**
* To aid in debugging: return the source of each configuration item.
* n.b. Must call this function *before* export and save the result
* if persistence is desired.
*/
public function sources()
{
$sources = [];
foreach ($this->unprocessedConfig as $sourceName => $config) {
if (!empty($sourceName)) {
$configSources = ArrayUtil::fillRecursive($config, $sourceName);
$sources = ArrayUtil::mergeRecursiveDistinct($sources, $configSources);
}
}
return $sources;
}
/**
* Get the configuration to be processed, and clear out the
* 'unprocessed' list.
*
* @return array
*/
protected function fetchUnprocessed()
{
$toBeProcessed = $this->unprocessedConfig;
$this->unprocessedConfig = [];
return $toBeProcessed;
}
/**
* Use a map-reduce to evaluate the items to be processed,
* and merge them into the processed array.
*
* @param array $processed
* @param array $toBeProcessed
* @return array
*/
protected function process(array $processed, array $toBeProcessed, $referenceArray = [])
{
$toBeReduced = array_map([$this, 'preprocess'], $toBeProcessed);
$reduced = array_reduce($toBeReduced, [$this, 'reduceOne'], $processed);
return $this->evaluate($reduced, $referenceArray);
}
/**
* Process a single configuration file from the 'to be processed'
* list. By default this is a no-op. Override this method to
* provide any desired configuration preprocessing, e.g. dot-notation
* expansion of the configuration keys, etc.
*
* @param array $config
* @return array
*/
protected function preprocess(array $config)
{
return $config;
}
/**
* Evaluate one item in the 'to be evaluated' list, and then
* merge it into the processed configuration (the 'carry').
*
* @param array $processed
* @param array $config
* @return array
*/
protected function reduceOne(array $processed, array $config)
{
return ArrayUtil::mergeRecursiveSelect($processed, $config, $this->nameOfItemsToMerge);
}
/**
* Evaluate one configuration item.
*
* @param array $processed
* @param array $config
* @return array
*/
protected function evaluate(array $config, $referenceArray = [])
{
return $this->expander->expandArrayProperties(
$config,
$referenceArray
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Consolidation\Config\Loader;
use Symfony\Component\Yaml\Yaml;
/**
* Load configuration files, and fill in any property values that
* need to be expanded.
*/
class YamlConfigLoader extends ConfigLoader
{
public function load($path)
{
$this->setSourceName($path);
// We silently skip any nonexistent config files, so that
// clients may simply `load` all of their candidates.
if (!file_exists($path)) {
$this->config = [];
return $this;
}
$this->config = (array) Yaml::parse(file_get_contents($path));
return $this;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Consolidation\Config\Util;
/**
* Useful array utilities.
*/
class ArrayUtil
{
/**
* Merges arrays recursively while preserving.
*
* @param array $array1
* @param array $array2
*
* @return array
*
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195
* @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
*/
public static function mergeRecursiveDistinct(
array &$array1,
array &$array2
) {
$merged = $array1;
foreach ($array2 as $key => &$value) {
$merged[$key] = self::mergeRecursiveValue($merged, $key, $value);
}
return $merged;
}
/**
* Process the value in an mergeRecursiveDistinct - make a recursive
* call if needed.
*/
protected static function mergeRecursiveValue(&$merged, $key, $value)
{
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
return self::mergeRecursiveDistinct($merged[$key], $value);
}
return $value;
}
/**
* Merges arrays recursively while preserving.
*
* @param array $array1
* @param array $array2
*
* @return array
*
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195
* @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
*/
public static function mergeRecursiveSelect(
array &$array1,
array &$array2,
array $selectionList,
$keyPrefix = ''
) {
$merged = $array1;
foreach ($array2 as $key => &$value) {
$merged[$key] = self::mergeRecursiveSelectValue($merged, $key, $value, $selectionList, $keyPrefix);
}
return $merged;
}
/**
* Process the value in an mergeRecursiveDistinct - make a recursive
* call if needed.
*/
protected static function mergeRecursiveSelectValue(&$merged, $key, $value, $selectionList, $keyPrefix)
{
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
if (self::selectMerge($keyPrefix, $key, $selectionList)) {
return array_merge_recursive($merged[$key], $value);
} else {
return self::mergeRecursiveSelect($merged[$key], $value, $selectionList, "${keyPrefix}${key}.");
}
}
return $value;
}
protected static function selectMerge($keyPrefix, $key, $selectionList)
{
return in_array("${keyPrefix}${key}", $selectionList);
}
/**
* Fills all of the leaf-node values of a nested array with the
* provided replacement value.
*/
public static function fillRecursive(array $data, $fill)
{
$result = [];
foreach ($data as $key => $value) {
$result[$key] = $fill;
if (self::isAssociative($value)) {
$result[$key] = self::fillRecursive($value, $fill);
}
}
return $result;
}
/**
* Return true if the provided parameter is an array, and at least
* one key is non-numeric.
*/
public static function isAssociative($testArray)
{
if (!is_array($testArray)) {
return false;
}
foreach (array_keys($testArray) as $key) {
if (!is_numeric($key)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Consolidation\Config\Util;
/**
* Fetch a configuration value from a configuration group. If the
* desired configuration value is not found in the most specific
* group named, keep stepping up to the next parent group until a
* value is located.
*
* Given the following constructor inputs:
* - $prefix = "command."
* - $group = "foo.bar.baz"
* - $postfix = ".options."
* Then the `get` method will then consider, in order:
* - command.foo.bar.baz.options
* - command.foo.bar.options
* - command.foo.options
* If any of these contain an option for "$key", then return its value.
*/
class ConfigFallback extends ConfigGroup
{
/**
* @inheritdoc
*/
public function get($key)
{
return $this->getWithFallback($key, $this->group, $this->prefix, $this->postfix);
}
/**
* Fetch an option value from a given key, or, if that specific key does
* not contain a value, then consult various fallback options until a
* value is found.
*
*/
protected function getWithFallback($key, $group, $prefix = '', $postfix = '.')
{
$configKey = "{$prefix}{$group}${postfix}{$key}";
if ($this->config->has($configKey)) {
return $this->config->get($configKey);
}
if ($this->config->hasDefault($configKey)) {
return $this->config->getDefault($configKey);
}
$moreGeneralGroupname = $this->moreGeneralGroupName($group);
if ($moreGeneralGroupname) {
return $this->getWithFallback($key, $moreGeneralGroupname, $prefix, $postfix);
}
return null;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Consolidation\Config\Util;
/**
* Fetch a configuration value from a configuration group. If the
* desired configuration value is not found in the most specific
* group named, keep stepping up to the next parent group until a
* value is located.
*
* Given the following constructor inputs:
* - $prefix = "command."
* - $group = "foo.bar.baz"
* - $postfix = ".options."
* Then the `get` method will then consider, in order:
* - command.foo.bar.baz.options
* - command.foo.bar.options
* - command.foo.options
* If any of these contain an option for "$key", then return its value.
*/
abstract class ConfigGroup
{
protected $config;
protected $group;
protected $prefix;
protected $postfix;
public function __construct($config, $group, $prefix = '', $postfix = '.')
{
$this->config = $config;
$this->group = $group;
$this->prefix = $prefix;
$this->postfix = $postfix;
}
/**
* Return a description of the configuration group (with prefix and postfix).
*/
public function describe($property)
{
return $this->prefix . $this->group . $this->postfix . $property;
}
/**
* Get the requested configuration key from the most specific configuration
* group that contains it.
*/
abstract public function get($key);
/**
* Given a group name, such as "foo.bar.baz", return the next configuration
* group in the fallback hierarchy, e.g. "foo.bar".
*/
protected function moreGeneralGroupName($group)
{
$result = preg_replace('#\.[^.]*$#', '', $group);
if ($result != $group) {
return $result;
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Provides configuration objects with an 'interpolate' method
* that may be used to inject config values into tokens embedded
* in strings.
*/
interface ConfigInterpolatorInterface extends ConfigInterface
{
/**
* interpolate replaces tokens in a string with the correspnding
* value from this config object. Tokens are surrounded by double
* curley braces, e.g. "{{key}}".
*
* Example:
* If the message is 'Hello, {{user.name}}', then the key user.name
* is fetched from the config object, and the token {{user.name}} is
* replaced with the result.
*
* @param string $message Message containing tokens to be replaced
* @param string|bool $default The value to substitute for tokens that
* are not found in the configuration. If `false`, then missing
* tokens are not replaced.
* @return string
*/
public function interpolate($message, $default = '');
/**
* mustInterpolate works exactly like interpolate, save for the fact
* that an exception is thrown is any of the tokens are not replaced.
*/
public function mustInterpolate($message);
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Provides configuration objects with an 'interpolate' method
* that may be used to inject config values into tokens embedded
* in strings..
*/
trait ConfigInterpolatorTrait
{
protected $interpolator;
protected function getInterpolator()
{
if (!isset($this->interpolator)) {
$this->interpolator = new Interpolator();
}
return $this->interpolator;
}
/**
* @inheritdoc
*/
public function interpolate($message, $default = '')
{
return $this->getInterpolator()->interpolate($this, $message, $default);
}
/**
* @inheritdoc
*/
public function mustInterpolate($message)
{
return $this->getInterpolator()->mustInterpolate($this, $message);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Consolidation\Config\Util;
/**
* Works like 'getWithFallback', but merges results from all applicable
* groups. Settings from most specific group take precedence.
*/
class ConfigMerge extends ConfigGroup
{
/**
* @inheritdoc
*/
public function get($key)
{
return $this->getWithMerge($key, $this->group, $this->prefix, $this->postfix);
}
/**
* Merge available configuration from each configuration group.
*/
public function getWithMerge($key, $group, $prefix = '', $postfix = '.')
{
$configKey = "{$prefix}{$group}${postfix}{$key}";
$result = $this->config->get($configKey, []);
if (!is_array($result)) {
throw new \UnexpectedValueException($configKey . ' must be a list of settings to apply.');
}
$moreGeneralGroupname = $this->moreGeneralGroupName($group);
if ($moreGeneralGroupname) {
$result += $this->getWithMerge($key, $moreGeneralGroupname, $prefix, $postfix);
}
return $result;
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
use Consolidation\Config\Util\ArrayUtil;
use Consolidation\Config\Util\ConfigInterpolatorInterface;
use Consolidation\Config\Util\ConfigInterpolatorTrait;
/**
* Overlay different configuration objects that implement ConfigInterface
* to make a priority-based, merged configuration object.
*
* Note that using a ConfigOverlay hides the defaults stored in each
* individual configuration context. When using overlays, always call
* getDefault / setDefault on the ConfigOverlay object itself.
*/
class ConfigOverlay implements ConfigInterface, ConfigInterpolatorInterface, ConfigRuntimeInterface
{
use ConfigInterpolatorTrait;
protected $contexts = [];
const DEFAULT_CONTEXT = 'default';
const PROCESS_CONTEXT = 'process';
public function __construct()
{
$this->contexts[self::DEFAULT_CONTEXT] = new Config();
$this->contexts[self::PROCESS_CONTEXT] = new Config();
}
/**
* Add a named configuration object to the configuration overlay.
* Configuration objects added LAST have HIGHEST priority, with the
* exception of the fact that the process context always has the
* highest priority.
*
* If a context has already been added, its priority will not change.
*/
public function addContext($name, ConfigInterface $config)
{
$process = $this->contexts[self::PROCESS_CONTEXT];
unset($this->contexts[self::PROCESS_CONTEXT]);
$this->contexts[$name] = $config;
$this->contexts[self::PROCESS_CONTEXT] = $process;
return $this;
}
/**
* Add a placeholder context that will be prioritized higher than
* existing contexts. This is done to ensure that contexts added
* later will maintain a higher priority if the placeholder context
* is later relaced with a different configuration set via addContext().
*
* @param string $name
* @return $this
*/
public function addPlaceholder($name)
{
return $this->addContext($name, new Config());
}
/**
* Increase the priority of the named context such that it is higher
* in priority than any existing context except for the 'process'
* context.
*
* @param string $name
* @return $this
*/
public function increasePriority($name)
{
$config = $this->getContext($name);
unset($this->contexts[$name]);
return $this->addContext($name, $config);
}
public function hasContext($name)
{
return isset($this->contexts[$name]);
}
public function getContext($name)
{
if ($this->hasContext($name)) {
return $this->contexts[$name];
}
return new Config();
}
public function runtimeConfig()
{
return $this->getContext(self::PROCESS_CONTEXT);
}
public function removeContext($name)
{
unset($this->contexts[$name]);
}
/**
* Determine if a non-default config value exists.
*/
public function findContext($key)
{
foreach (array_reverse($this->contexts) as $name => $config) {
if ($config->has($key)) {
return $config;
}
}
return false;
}
/**
* @inheritdoc
*/
public function has($key)
{
return $this->findContext($key) != false;
}
/**
* @inheritdoc
*/
public function get($key, $default = null)
{
if (is_array($default)) {
return $this->getUnion($key);
}
return $this->getSingle($key, $default);
}
public function getSingle($key, $default = null)
{
$context = $this->findContext($key);
if ($context) {
return $context->get($key, $default);
}
return $default;
}
public function getUnion($key)
{
$result = [];
foreach (array_reverse($this->contexts) as $name => $config) {
$item = (array) $config->get($key, []);
if ($item !== null) {
$result = array_merge($result, $item);
}
}
return $result;
}
/**
* @inheritdoc
*/
public function set($key, $value)
{
$this->contexts[self::PROCESS_CONTEXT]->set($key, $value);
return $this;
}
/**
* @inheritdoc
*/
public function import($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
public function replace($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
public function combine($data)
{
$this->unsupported(__FUNCTION__);
}
/**
* @inheritdoc
*/
protected function unsupported($fn)
{
throw new \Exception("The method '$fn' is not supported for the ConfigOverlay class.");
}
/**
* @inheritdoc
*/
public function export()
{
$export = [];
foreach ($this->contexts as $name => $config) {
$exportToMerge = $config->export();
$export = \array_replace_recursive($export, $exportToMerge);
}
return $export;
}
/**
* exportAll returns the export of all contexts, separated into
* separate buckets keyed by context name.
*
* @return array
*/
public function exportAll()
{
$export = [];
foreach ($this->contexts as $name => $config) {
$exportToInsert = $config->export();
$export[$name] = $exportToInsert;
}
return $export;
}
/**
* @inheritdoc
*/
public function hasDefault($key)
{
return $this->contexts[self::DEFAULT_CONTEXT]->has($key);
}
/**
* @inheritdoc
*/
public function getDefault($key, $default = null)
{
return $this->contexts[self::DEFAULT_CONTEXT]->get($key, $default);
}
/**
* @inheritdoc
*/
public function setDefault($key, $value)
{
$this->contexts[self::DEFAULT_CONTEXT]->set($key, $value);
return $this;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* ConfigRutimeInterface provides a method that returns those elements
* of the configuration that were set at runtime, e.g. via commandline
* options rather than being loaded from a file.
*/
interface ConfigRuntimeInterface
{
/**
* runtimeConfig returns those elements of the configuration not
* loaded from a file.
*
* @return ConfigInterface
*/
public function runtimeConfig();
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Provide a configuration object that fetches values from environment
* variables.
*/
class EnvConfig implements ConfigInterface
{
/** @var string */
protected $prefix;
/**
* EnvConfig constructor
*
* @param $prefix The string to appear before every environment
* variable key. For example, if the prefix is 'MYAPP_', then
* the key 'foo.bar' will be fetched from the environment variable
* MYAPP_FOO_BAR.
*/
public function __construct($prefix)
{
// Ensure that the prefix is always uppercase, and always
// ends with a '_', regardless of the form the caller provided.
$this->prefix = strtoupper(rtrim($prefix, '_')) . '_';
}
/**
* @inheritdoc
*/
public function has($key)
{
return $this->get($key) !== null;
}
/**
* @inheritdoc
*/
public function get($key, $defaultFallback = null)
{
$envKey = $this->prefix . strtoupper(strtr($key, '.-', '__'));
$envKey = str_replace($this->prefix . $this->prefix, $this->prefix, $envKey);
return getenv($envKey) ?: $defaultFallback;
}
/**
* @inheritdoc
*/
public function set($key, $value)
{
throw new \Exception('Cannot call "set" on environmental configuration.');
}
/**
* @inheritdoc
*/
public function import($data)
{
// no-op
}
/**
* @inheritdoc
*/
public function export()
{
return [];
}
/**
* @inheritdoc
*/
public function hasDefault($key)
{
return false;
}
/**
* @inheritdoc
*/
public function getDefault($key, $defaultFallback = null)
{
return $defaultFallback;
}
/**
* @inheritdoc
*/
public function setDefault($key, $value)
{
throw new \Exception('Cannot call "setDefault" on environmental configuration.');
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Consolidation\Config\Util;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
/**
* Provides configuration objects with an 'interpolate' method
* that may be used to inject config values into tokens embedded
* in strings..
*/
class Interpolator
{
/**
* interpolate replaces tokens in a string with the correspnding
* value from this config object. Tokens are surrounded by double
* curley braces, e.g. "{{key}}".
*
* Example:
* If the message is 'Hello, {{user.name}}', then the key user.name
* is fetched from the config object, and the token {{user.name}} is
* replaced with the result.
*
* @param string $message Message containing tokens to be replaced
* @param string|bool $default The value to substitute for tokens that
* are not found in the configuration. If `false`, then missing
* tokens are not replaced.
* @return string
*/
public function interpolate($data, $message, $default = '')
{
$replacements = $this->replacements($data, $message, $default);
return strtr($message, $replacements);
}
/**
* @inheritdoc
*/
public function mustInterpolate($data, $message)
{
$result = $this->interpolate($data, $message, false);
$tokens = $this->findTokens($result);
if (!empty($tokens)) {
throw new \Exception('The following required keys were not found in configuration: ' . implode(',', $tokens));
}
return $result;
}
/**
* findTokens finds all of the tokens in the provided message
*
* @param string $message String with tokens
* @return string[] map of token to key, e.g. {{key}} => key
*/
public function findTokens($message)
{
if (!preg_match_all('#{{([a-zA-Z0-9._-]+)}}#', $message, $matches, PREG_SET_ORDER)) {
return [];
}
$tokens = [];
foreach ($matches as $matchSet) {
list($sourceText, $key) = $matchSet;
$tokens[$sourceText] = $key;
}
return $tokens;
}
/**
* Replacements looks up all of the replacements in the configuration
* object, given the token keys from the provided message. Keys that
* do not exist in the configuration are replaced with the default value.
*/
public function replacements($data, $message, $default = '')
{
$tokens = $this->findTokens($message);
$replacements = [];
foreach ($tokens as $sourceText => $key) {
$replacementText = $this->get($data, $key, $default);
if ($replacementText !== false) {
$replacements[$sourceText] = $replacementText;
}
}
return $replacements;
}
protected function get($data, $key, $default)
{
if (is_array($data)) {
return array_key_exists($key, $data) ? $data[$key] : $default;
}
if ($data instanceof ConfigInterface) {
return $data->get($key, $default);
}
throw new \Exception('Bad data type provided to Interpolator');
}
}