[OptionsResolver] Added a light-weight, low-level API for basic option resolving

This commit is contained in:
Bernhard Schussek 2014-08-20 12:35:25 +02:00
parent e86fe91d70
commit 90660255a2
7 changed files with 1066 additions and 435 deletions

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver\Exception;
/**
* Thrown when an argument is invalid.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
/**
@ -56,6 +59,244 @@ class Options implements \ArrayAccess, \Iterator, \Countable
*/
private $reading = false;
/**
* Merges options with an array of default values and throws an exception if
* any of the options does not exist.
*
* @param array $options A list of option names and
* values
* @param array|Options|OptionsConfig $defaults The accepted options and
* their default values
*
* @return array The merged and validated options
*
* @throws InvalidOptionsException If any of the options is not present in
* the defaults array
* @throws InvalidArgumentException If the defaults are invalid
*
* @since 2.6
*/
public static function resolve(array $options, $defaults)
{
if (is_array($defaults)) {
static::validateNames($options, $defaults, true);
return array_replace($defaults, $options);
}
if ($defaults instanceof self) {
static::validateNames($options, $defaults->options, true);
// Make sure this method can be called multiple times
$combinedOptions = clone $defaults;
// Override options set by the user
foreach ($options as $option => $value) {
$combinedOptions->set($option, $value);
}
// Resolve options
return $combinedOptions->all();
}
if ($defaults instanceof OptionsConfig) {
static::validateNames($options, $defaults->knownOptions, true);
static::validateRequired($options, $defaults->requiredOptions, true);
// Make sure this method can be called multiple times
$combinedOptions = clone $defaults->defaultOptions;
// Override options set by the user
foreach ($options as $option => $value) {
$combinedOptions->set($option, $value);
}
// Resolve options
$resolvedOptions = $combinedOptions->all();
static::validateTypes($resolvedOptions, $defaults->allowedTypes);
static::validateValues($resolvedOptions, $defaults->allowedValues);
return $resolvedOptions;
}
throw new InvalidArgumentException('The second argument is expected to be given as array, Options instance or OptionsConfig instance.');
}
/**
* Validates that the given option names exist and throws an exception
* otherwise.
*
* @param array $options A list of option names and values
* @param string|array $acceptedOptions The accepted option(s), either passed
* as single string or in the values of
* the given array
* @param bool $namesAsKeys If set to true, the option names
* should be passed in the keys of the
* accepted options array
*
* @throws InvalidOptionsException If any of the options is not present in
* the accepted options
*
* @since 2.6
*/
public static function validateNames(array $options, $acceptedOptions, $namesAsKeys = false)
{
$acceptedOptions = (array) $acceptedOptions;
if (!$namesAsKeys) {
$acceptedOptions = array_flip($acceptedOptions);
}
$diff = array_diff_key($options, $acceptedOptions);
if (count($diff) > 0) {
ksort($acceptedOptions);
ksort($diff);
throw new InvalidOptionsException(sprintf(
(count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Known options are: "%s"',
implode('", "', array_keys($diff)),
implode('", "', array_keys($acceptedOptions))
));
}
}
/**
* Validates that the required options are given and throws an exception
* otherwise.
*
* The option names may be any strings that don't consist exclusively of
* digits. For example, "case1" is a valid option name, "1" is not.
*
* @param array $options A list of option names and values
* @param string|array $requiredOptions The required option(s), either
* passed as single string or in the
* values of the given array
* @param bool $namesAsKeys If set to true, the option names
* should be passed in the keys of the
* required options array
*
* @throws MissingOptionsException If a required option is missing
*
* @since 2.6
*/
public static function validateRequired(array $options, $requiredOptions, $namesAsKeys = false)
{
$requiredOptions = (array) $requiredOptions;
if (!$namesAsKeys) {
$requiredOptions = array_flip($requiredOptions);
}
$diff = array_diff_key($requiredOptions, $options);
if (count($diff) > 0) {
ksort($diff);
throw new MissingOptionsException(sprintf(
count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.',
implode('", "', array_keys($diff))
));
}
}
/**
* Validates that the given options match the accepted types and
* throws an exception otherwise.
*
* Accepted type names are any types for which a native "is_*()" function
* exists. For example, "int" is an acceptable type name and will be checked
* with the "is_int()" function.
*
* Types may also be passed as closures which return true or false.
*
* @param array $options A list of option names and values
* @param array $acceptedTypes A mapping of option names to accepted option
* types. The types may be given as
* string/closure or as array of strings/closures
*
* @throws InvalidOptionsException If any of the types does not match the
* accepted types of the option
*
* @since 2.6
*/
public static function validateTypes(array $options, array $acceptedTypes)
{
foreach ($acceptedTypes as $option => $optionTypes) {
if (!array_key_exists($option, $options)) {
continue;
}
$value = $options[$option];
$optionTypes = (array) $optionTypes;
foreach ($optionTypes as $type) {
$isFunction = 'is_'.$type;
if (function_exists($isFunction) && $isFunction($value)) {
continue 2;
} elseif ($value instanceof $type) {
continue 2;
}
}
$printableValue = is_object($value)
? get_class($value)
: (is_array($value)
? 'Array'
: (string) $value);
throw new InvalidOptionsException(sprintf(
'The option "%s" with value "%s" is expected to be of type "%s"',
$option,
$printableValue,
implode('", "', $optionTypes)
));
}
}
/**
* Validates that the given option values match the accepted values and
* throws an exception otherwise.
*
* @param array $options A list of option names and values
* @param array $acceptedValues A mapping of option names to accepted option
* values. The option values must be given as
* arrays
*
* @throws InvalidOptionsException If any of the values does not match the
* accepted values of the option
*
* @since 2.6
*/
public static function validateValues(array $options, array $acceptedValues)
{
foreach ($acceptedValues as $option => $optionValues) {
if (array_key_exists($option, $options)) {
if (is_array($optionValues) && !in_array($options[$option], $optionValues, true)) {
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $optionValues)));
}
if (is_callable($optionValues) && !call_user_func($optionValues, $options[$option])) {
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", which it is not valid', $option, $options[$option]));
}
}
}
}
/**
* Constructs a new object with a set of default options.
*
* @param array $options A list of option names and values
*/
public function __construct(array $options = array())
{
foreach ($options as $option => $value) {
$this->set($option, $value);
}
}
/**
* Sets the value of a given option.
*
@ -179,8 +420,10 @@ class Options implements \ArrayAccess, \Iterator, \Countable
// If an option is a closure that should be evaluated lazily, store it
// in the "lazy" property.
if ($value instanceof \Closure) {
$reflClosure = new \ReflectionFunction($value);
if (is_callable($value)) {
$reflClosure = is_array($value)
? new \ReflectionMethod($value[0], $value[1])
: new \ReflectionFunction($value);
$params = $reflClosure->getParameters();
if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && __CLASS__ === $class->name) {
@ -229,11 +472,11 @@ class Options implements \ArrayAccess, \Iterator, \Countable
}
if (isset($this->lazy[$option])) {
$this->resolve($option);
$this->resolveOption($option);
}
if (isset($this->normalizers[$option])) {
$this->normalize($option);
$this->normalizeOption($option);
}
return $this->options[$option];
@ -306,13 +549,13 @@ class Options implements \ArrayAccess, \Iterator, \Countable
// Double check, in case the option has already been resolved
// by cascade in the previous cycles
if (isset($this->lazy[$option])) {
$this->resolve($option);
$this->resolveOption($option);
}
}
foreach ($this->normalizers as $option => $normalizer) {
if (isset($this->normalizers[$option])) {
$this->normalize($option);
$this->normalizeOption($option);
}
}
@ -444,7 +687,7 @@ class Options implements \ArrayAccess, \Iterator, \Countable
* @throws OptionDefinitionException If the option has a cyclic dependency
* on another option.
*/
private function resolve($option)
private function resolveOption($option)
{
// The code duplication with normalize() exists for performance
// reasons, in order to save a method call.
@ -464,7 +707,7 @@ class Options implements \ArrayAccess, \Iterator, \Countable
$this->lock[$option] = true;
foreach ($this->lazy[$option] as $closure) {
$this->options[$option] = $closure($this, $this->options[$option]);
$this->options[$option] = call_user_func($closure, $this, $this->options[$option]);
}
unset($this->lock[$option]);
@ -482,7 +725,7 @@ class Options implements \ArrayAccess, \Iterator, \Countable
* @throws OptionDefinitionException If the option has a cyclic dependency
* on another option.
*/
private function normalize($option)
private function normalizeOption($option)
{
// The code duplication with resolve() exists for performance
// reasons, in order to save a method call.

View File

@ -0,0 +1,354 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
/**
* Stores option configuration.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*
* @since 2.6
*/
class OptionsConfig
{
/**
* The default option values.
*
* @var Options
*
* @internal Public for performance reasons. Should not be accessed by user
* code.
*/
public $defaultOptions;
/**
* The options known by the resolver.
*
* @var array
*
* @internal Public for performance reasons. Should not be accessed by user
* code.
*/
public $knownOptions = array();
/**
* The options without defaults that are required to be passed to resolve().
*
* @var array
*
* @internal Public for performance reasons. Should not be accessed by user
* code.
*/
public $requiredOptions = array();
/**
* A list of accepted values for each option.
*
* @var array
*
* @internal Public for performance reasons. Should not be accessed by user
* code.
*/
public $allowedValues = array();
/**
* A list of accepted types for each option.
*
* @var array
*
* @internal Public for performance reasons. Should not be accessed by user
* code.
*/
public $allowedTypes = array();
/**
* Creates a new instance.
*/
public function __construct()
{
$this->defaultOptions = new Options();
}
/**
* Clones the resolver.
*/
public function __clone()
{
$this->defaultOptions = clone $this->defaultOptions;
}
/**
* Sets default option values.
*
* The options can either be values of any types or closures that
* evaluate the option value lazily. These closures must have one
* of the following signatures:
*
* <code>
* function (Options $options)
* function (Options $options, $value)
* </code>
*
* The second parameter passed to the closure is the previously
* set default value, in case you are overwriting an existing
* default value.
*
* The closures should return the lazily created option value.
*
* @param array $defaultValues A list of option names as keys and default
* values or closures as values
*
* @return OptionsConfig This configuration instance
*/
public function setDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions->overload($option, $value);
$this->knownOptions[$option] = true;
unset($this->requiredOptions[$option]);
}
return $this;
}
/**
* Replaces default option values.
*
* Old defaults are erased, which means that closures passed here cannot
* access the previous default value. This may be useful to improve
* performance if the previous default value is calculated by an expensive
* closure.
*
* @param array $defaultValues A list of option names as keys and default
* values or closures as values
*
* @return OptionsConfig This configuration instance
*/
public function replaceDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions->set($option, $value);
$this->knownOptions[$option] = true;
unset($this->requiredOptions[$option]);
}
return $this;
}
/**
* Sets optional options.
*
* This method declares valid option names without setting default values for them.
* If these options are not passed to {@link resolve()} and no default has been set
* for them, they will be missing in the final options array. This can be helpful
* if you want to determine whether an option has been set or not because otherwise
* {@link resolve()} would trigger an exception for unknown options.
*
* @param array $optionNames A list of option names
*
* @return OptionsConfig This configuration instance
*
* @throws Exception\OptionDefinitionException When trying to pass default values
*/
public function setOptional(array $optionNames)
{
foreach ($optionNames as $key => $option) {
if (!is_int($key)) {
throw new OptionDefinitionException('You should not pass default values to setOptional()');
}
$this->knownOptions[$option] = true;
}
return $this;
}
/**
* Sets required options.
*
* If these options are not passed to {@link resolve()} and no default has been set for
* them, an exception will be thrown.
*
* @param array $optionNames A list of option names
*
* @return OptionsConfig This configuration instance
*
* @throws Exception\OptionDefinitionException When trying to pass default values
*/
public function setRequired(array $optionNames)
{
foreach ($optionNames as $key => $option) {
if (!is_int($key)) {
throw new OptionDefinitionException('You should not pass default values to setRequired()');
}
$this->knownOptions[$option] = true;
// set as required if no default has been set already
if (!isset($this->defaultOptions[$option])) {
$this->requiredOptions[$option] = true;
}
}
return $this;
}
/**
* Sets allowed values for a list of options.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values.
*
* @return OptionsConfig This configuration instance
*
* @throws Exception\InvalidOptionsException If an option has not been defined
* (see {@link isKnown()}) for which
* an allowed value is set
*/
public function setAllowedValues(array $allowedValues)
{
Options::validateNames($allowedValues, $this->knownOptions, true);
$this->allowedValues = array_replace($this->allowedValues, $allowedValues);
return $this;
}
/**
* Adds allowed values for a list of options.
*
* The values are merged with the allowed values defined previously.
*
* @param array $allowedValues A list of option names as keys and arrays
* with values acceptable for that option as
* values
*
* @return OptionsConfig This configuration instance
*
* @throws Exception\InvalidOptionsException If an option has not been defined
* (see {@link isKnown()}) for which
* an allowed value is set
*/
public function addAllowedValues(array $allowedValues)
{
Options::validateNames($allowedValues, $this->knownOptions, true);
$this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues);
return $this;
}
/**
* Sets allowed types for a list of options.
*
* @param array $allowedTypes A list of option names as keys and type
* names passed as string or array as values
*
* @return OptionsConfig This configuration instance
*
* @throws Exception\InvalidOptionsException If an option has not been defined for
* which an allowed type is set
*/
public function setAllowedTypes(array $allowedTypes)
{
Options::validateNames($allowedTypes, $this->knownOptions, true);
$this->allowedTypes = array_replace($this->allowedTypes, $allowedTypes);
return $this;
}
/**
* Adds allowed types for a list of options.
*
* The types are merged with the allowed types defined previously.
*
* @param array $allowedTypes A list of option names as keys and type
* names passed as string or array as values
*
* @return OptionsConfig This configuration instance
*
* @throws Exception\InvalidOptionsException If an option has not been defined for
* which an allowed type is set
*/
public function addAllowedTypes(array $allowedTypes)
{
Options::validateNames($allowedTypes, $this->knownOptions, true);
$this->allowedTypes = array_merge_recursive($this->allowedTypes, $allowedTypes);
return $this;
}
/**
* Sets normalizers that are applied on resolved options.
*
* The normalizers should be closures with the following signature:
*
* <code>
* function (Options $options, $value)
* </code>
*
* The second parameter passed to the closure is the value of
* the option.
*
* The closure should return the normalized value.
*
* @param array $normalizers An array of closures
*
* @return OptionsConfig This configuration instance
*/
public function setNormalizers(array $normalizers)
{
Options::validateNames($normalizers, $this->knownOptions, true);
foreach ($normalizers as $option => $normalizer) {
$this->defaultOptions->setNormalizer($option, $normalizer);
}
return $this;
}
/**
* Returns whether an option is known.
*
* An option is known if it has been passed to either {@link setDefaults()},
* {@link setRequired()} or {@link setOptional()} before.
*
* @param string $option The name of the option
*
* @return bool Whether the option is known
*/
public function isKnown($option)
{
return isset($this->knownOptions[$option]);
}
/**
* Returns whether an option is required.
*
* An option is required if it has been passed to {@link setRequired()},
* but not to {@link setDefaults()}. That is, the option has been declared
* as required and no default value has been set.
*
* @param string $option The name of the option
*
* @return bool Whether the option is required
*/
public function isRequired($option)
{
return isset($this->requiredOptions[$option]);
}
}

View File

@ -11,342 +11,19 @@
namespace Symfony\Component\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
/**
* Helper for merging default and concrete option values.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Tobias Schultze <http://tobion.de>
*/
class OptionsResolver implements OptionsResolverInterface
class OptionsResolver extends OptionsConfig implements OptionsResolverInterface
{
/**
* The default option values.
* @var Options
*/
private $defaultOptions;
/**
* The options known by the resolver.
* @var array
*/
private $knownOptions = array();
/**
* The options without defaults that are required to be passed to resolve().
* @var array
*/
private $requiredOptions = array();
/**
* A list of accepted values for each option.
* @var array
*/
private $allowedValues = array();
/**
* A list of accepted types for each option.
* @var array
*/
private $allowedTypes = array();
/**
* Creates a new instance.
*/
public function __construct()
{
$this->defaultOptions = new Options();
}
/**
* Clones the resolver.
*/
public function __clone()
{
$this->defaultOptions = clone $this->defaultOptions;
}
/**
* {@inheritdoc}
*/
public function setDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions->overload($option, $value);
$this->knownOptions[$option] = true;
unset($this->requiredOptions[$option]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function replaceDefaults(array $defaultValues)
{
foreach ($defaultValues as $option => $value) {
$this->defaultOptions->set($option, $value);
$this->knownOptions[$option] = true;
unset($this->requiredOptions[$option]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setOptional(array $optionNames)
{
foreach ($optionNames as $key => $option) {
if (!is_int($key)) {
throw new OptionDefinitionException('You should not pass default values to setOptional()');
}
$this->knownOptions[$option] = true;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setRequired(array $optionNames)
{
foreach ($optionNames as $key => $option) {
if (!is_int($key)) {
throw new OptionDefinitionException('You should not pass default values to setRequired()');
}
$this->knownOptions[$option] = true;
// set as required if no default has been set already
if (!isset($this->defaultOptions[$option])) {
$this->requiredOptions[$option] = true;
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setAllowedValues(array $allowedValues)
{
$this->validateOptionsExistence($allowedValues);
$this->allowedValues = array_replace($this->allowedValues, $allowedValues);
return $this;
}
/**
* {@inheritdoc}
*/
public function addAllowedValues(array $allowedValues)
{
$this->validateOptionsExistence($allowedValues);
$this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues);
return $this;
}
/**
* {@inheritdoc}
*/
public function setAllowedTypes(array $allowedTypes)
{
$this->validateOptionsExistence($allowedTypes);
$this->allowedTypes = array_replace($this->allowedTypes, $allowedTypes);
return $this;
}
/**
* {@inheritdoc}
*/
public function addAllowedTypes(array $allowedTypes)
{
$this->validateOptionsExistence($allowedTypes);
$this->allowedTypes = array_merge_recursive($this->allowedTypes, $allowedTypes);
return $this;
}
/**
* {@inheritdoc}
*/
public function setNormalizers(array $normalizers)
{
$this->validateOptionsExistence($normalizers);
foreach ($normalizers as $option => $normalizer) {
$this->defaultOptions->setNormalizer($option, $normalizer);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function isKnown($option)
{
return isset($this->knownOptions[$option]);
}
/**
* {@inheritdoc}
*/
public function isRequired($option)
{
return isset($this->requiredOptions[$option]);
}
/**
* {@inheritdoc}
*/
public function resolve(array $options = array())
{
$this->validateOptionsExistence($options);
$this->validateOptionsCompleteness($options);
// Make sure this method can be called multiple times
$combinedOptions = clone $this->defaultOptions;
// Override options set by the user
foreach ($options as $option => $value) {
$combinedOptions->set($option, $value);
}
// Resolve options
$resolvedOptions = $combinedOptions->all();
$this->validateOptionTypes($resolvedOptions);
$this->validateOptionValues($resolvedOptions);
return $resolvedOptions;
}
/**
* Validates that the given option names exist and throws an exception
* otherwise.
*
* @param array $options An list of option names as keys.
*
* @throws InvalidOptionsException If any of the options has not been defined.
*/
private function validateOptionsExistence(array $options)
{
$diff = array_diff_key($options, $this->knownOptions);
if (count($diff) > 0) {
ksort($this->knownOptions);
ksort($diff);
throw new InvalidOptionsException(sprintf(
(count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Known options are: "%s"',
implode('", "', array_keys($diff)),
implode('", "', array_keys($this->knownOptions))
));
}
}
/**
* Validates that all required options are given and throws an exception
* otherwise.
*
* @param array $options An list of option names as keys.
*
* @throws MissingOptionsException If a required option is missing.
*/
private function validateOptionsCompleteness(array $options)
{
$diff = array_diff_key($this->requiredOptions, $options);
if (count($diff) > 0) {
ksort($diff);
throw new MissingOptionsException(sprintf(
count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.',
implode('", "', array_keys($diff))
));
}
}
/**
* Validates that the given option values match the allowed values and
* throws an exception otherwise.
*
* @param array $options A list of option values.
*
* @throws InvalidOptionsException If any of the values does not match the
* allowed values of the option.
*/
private function validateOptionValues(array $options)
{
foreach ($this->allowedValues as $option => $allowedValues) {
if (isset($options[$option])) {
if (is_array($allowedValues) && !in_array($options[$option], $allowedValues, true)) {
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
}
if (is_callable($allowedValues) && !call_user_func($allowedValues, $options[$option])) {
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", which it is not valid', $option, $options[$option]));
}
}
}
}
/**
* Validates that the given options match the allowed types and
* throws an exception otherwise.
*
* @param array $options A list of options.
*
* @throws InvalidOptionsException If any of the types does not match the
* allowed types of the option.
*/
private function validateOptionTypes(array $options)
{
foreach ($this->allowedTypes as $option => $allowedTypes) {
if (!array_key_exists($option, $options)) {
continue;
}
$value = $options[$option];
$allowedTypes = (array) $allowedTypes;
foreach ($allowedTypes as $type) {
$isFunction = 'is_'.$type;
if (function_exists($isFunction) && $isFunction($value)) {
continue 2;
} elseif ($value instanceof $type) {
continue 2;
}
}
$printableValue = is_object($value)
? get_class($value)
: (is_array($value)
? 'Array'
: (string) $value);
throw new InvalidOptionsException(sprintf(
'The option "%s" with value "%s" is expected to be of type "%s"',
$option,
$printableValue,
implode('", "', $allowedTypes)
));
}
return Options::resolve($options, $this);
}
}

View File

@ -1,101 +1,12 @@
OptionsResolver Component
=========================
OptionsResolver helps at configuring objects with option arrays.
It supports default values on different levels of your class hierarchy,
option constraints (required vs. optional, allowed values) and lazy options
whose default value depends on the value of another option.
The following example demonstrates a Person class with two required options
"firstName" and "lastName" and two optional options "age" and "gender", where
the default value of "gender" is derived from the passed first name, if
possible, and may only be one of "male" and "female".
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\Options;
class Person
{
protected $options;
public function __construct(array $options = array())
{
$resolver = new OptionsResolver();
$this->setDefaultOptions($resolver);
$this->options = $resolver->resolve($options);
}
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array(
'firstName',
'lastName',
));
$resolver->setDefaults(array(
'age' => null,
'gender' => function (Options $options) {
if (self::isKnownMaleName($options['firstName'])) {
return 'male';
}
return 'female';
},
));
$resolver->setAllowedValues(array(
'gender' => array('male', 'female'),
));
}
}
We can now easily instantiate a Person object:
// 'gender' is implicitly set to 'female'
$person = new Person(array(
'firstName' => 'Jane',
'lastName' => 'Doe',
));
We can also override the default values of the optional options:
$person = new Person(array(
'firstName' => 'Abdullah',
'lastName' => 'Mogashi',
'gender' => 'male',
'age' => 30,
));
Options can be added or changed in subclasses by overriding the `setDefaultOptions`
method:
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;
class Employee extends Person
{
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->setRequired(array(
'birthDate',
));
$resolver->setDefaults(array(
// $previousValue contains the default value configured in the
// parent class
'age' => function (Options $options, $previousValue) {
return self::calculateAge($options['birthDate']);
}
));
}
}
This component processes and validates option arrays.
Documentation
-------------
The documentation for the component can be found [online] [1].
Resources
---------
@ -105,3 +16,5 @@ You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/OptionsResolver/
$ composer.phar install
$ phpunit
[1]: http://symfony.com/doc/current/components/options_resolver.html

View File

@ -43,6 +43,23 @@ class OptionsResolverTest extends \PHPUnit_Framework_TestCase
), $this->resolver->resolve($options));
}
public function testResolveNumericOptions()
{
$this->resolver->setDefaults(array(
'1' => '1',
'2' => '2',
));
$options = array(
'2' => '20',
);
$this->assertEquals(array(
'1' => '1',
'2' => '20',
), $this->resolver->resolve($options));
}
public function testResolveLazy()
{
$this->resolver->setDefaults(array(
@ -94,7 +111,7 @@ class OptionsResolverTest extends \PHPUnit_Framework_TestCase
},
));
$options = array(
$options = array(,
);
$this->assertEquals(array(
@ -623,7 +640,7 @@ class OptionsResolverTest extends \PHPUnit_Framework_TestCase
));
$this->assertEquals(array(
'foo' => 'bar'
'foo' => 'bar',
), $this->resolver->resolve(array()));
}
@ -637,7 +654,7 @@ class OptionsResolverTest extends \PHPUnit_Framework_TestCase
));
$this->assertEquals(array(
'foo' => 'bar'
'foo' => 'bar',
), $this->resolver->resolve(array()));
}
@ -652,7 +669,7 @@ class OptionsResolverTest extends \PHPUnit_Framework_TestCase
$options = array(
'one' => '1',
'two' => '2'
'two' => '2',
);
$this->assertEquals($options, $this->resolver->resolve($options));

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\OptionsResolver\Tests;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsConfig;
class OptionsTest extends \PHPUnit_Framework_TestCase
{
@ -25,6 +26,389 @@ class OptionsTest extends \PHPUnit_Framework_TestCase
$this->options = new Options();
}
public function testResolve()
{
$defaults = array(
'one' => '1',
'two' => '2',
);
$options = array(
'two' => '20',
);
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), Options::resolve($options, $defaults));
}
public function testResolveNumericOptions()
{
$defaults = array(
'1' => '1',
'2' => '2',
);
$options = array(
'2' => '20',
);
$this->assertEquals(array(
'1' => '1',
'2' => '20',
), Options::resolve($options, $defaults));
}
public function testResolveLazy()
{
$defaults = new Options(array(
'one' => '1',
'two' => function (Options $options) {
return '20';
},
));
$options = array();
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), Options::resolve($options, $defaults));
}
public function testResolveConfig()
{
$config = new OptionsConfig();
$config->setDefaults(array(
'one' => '1',
'two' => '2',
));
$options = array(
'two' => '20',
);
$this->assertEquals(array(
'one' => '1',
'two' => '20',
), Options::resolve($options, $config));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfNonExistingOption()
{
$defaults = array(
'one' => '1',
);
$options = array(
'foo' => 'bar',
);
Options::resolve($options, $defaults);
}
public function testValidateNamesSucceedsIfValidOption()
{
$options = array(
'one' => '1',
);
Options::validateNames($options, 'one');
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateNamesFailsIfNonExistingOption()
{
$options = array(
'foo' => 'bar',
);
Options::validateNames($options, 'one');
}
public function testValidateNamesSucceedsIfValidOptions()
{
$options = array(
'one' => '1',
);
Options::validateNames($options, array(
'one',
'two',
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateNamesFailsIfNonExistingOptions()
{
$options = array(
'one' => '1',
'foo' => 'bar',
);
Options::validateNames($options, array(
'one',
'two',
));
}
public function testValidateNamesSucceedsIfValidOptionsNamesAsKeys()
{
$options = array(
'one' => '1',
);
Options::validateNames($options, array(
'one' => null,
'two' => null,
), true);
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateNamesFailsIfNonExistingOptionsNamesAsKeys()
{
$options = array(
'one' => '1',
'foo' => 'bar',
);
Options::validateNames($options, array(
'one' => null,
'two' => null,
), true);
}
public function testValidateRequiredSucceedsIfRequiredOptionPresent()
{
$options = array(
'one' => '10',
);
Options::validateRequired($options, 'one');
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
*/
public function testValidateRequiredFailsIfMissingRequiredOption()
{
$options = array(
'two' => '20',
);
Options::validateRequired($options, 'one');
}
public function testValidateRequiredSucceedsIfRequiredOptionsPresent()
{
$options = array(
'one' => '10',
'two' => '20',
);
Options::validateRequired($options, array(
'one',
'two',
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
*/
public function testValidateRequiredFailsIfMissingRequiredOptions()
{
$options = array(
'two' => '20',
);
Options::validateRequired($options, array(
'one',
'two',
));
}
public function testValidateRequiredSucceedsIfRequiredOptionsPresentNamesAsKeys()
{
$options = array(
'one' => '10',
'two' => '20',
);
Options::validateRequired($options, array(
'one' => null,
'two' => null,
), true);
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
*/
public function testValidateRequiredFailsIfMissingRequiredOptionsNamesAsKeys()
{
$options = array(
'two' => '20',
);
Options::validateRequired($options, array(
'one' => null,
'two' => null,
), true);
}
public function testValidateTypesSucceedsIfValidType()
{
$options = array(
'one' => 'one',
);
Options::validateTypes($options, array(
'one' => 'string',
));
}
public function testValidateTypesSucceedsIfValidTypePassArray()
{
$options = array(
'one' => 'one',
);
Options::validateTypes($options, array(
'one' => array('string', 'bool'),
));
}
public function testValidateTypesSucceedsIfValidTypePassObject()
{
$object = new \stdClass();
$options = array(
'one' => $object,
);
Options::validateTypes($options, array(
'one' => 'object',
));
}
public function testValidateTypesSucceedsIfValidTypePassClass()
{
$object = new \stdClass();
$options = array(
'one' => $object,
);
Options::validateTypes($options, array(
'one' => '\stdClass',
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateTypesFailsIfInvalidType()
{
$options = array(
'one' => 1.23,
);
Options::validateTypes($options, array(
'one' => array('string', 'bool'),
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateTypesFailsIfInvalidTypeMultipleOptions()
{
$options = array(
'one' => 'foo',
'two' => 1.23,
);
Options::validateTypes($options, array(
'one' => 'string',
'two' => 'bool',
));
}
public function testValidateValuesSucceedsIfValidValue()
{
$options = array(
'one' => 'one',
);
Options::validateValues($options, array(
'one' => array('1', 'one'),
));
}
public function testValidateValuesSucceedsIfValidValueMultipleOptions()
{
$options = array(
'one' => '1',
'two' => 'two',
);
Options::validateValues($options, array(
'one' => array('1', 'one'),
'two' => array('2', 'two'),
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateValuesFailsIfInvalidValue()
{
$options = array(
'one' => '2',
);
Options::validateValues($options, array(
'one' => array('1', 'one'),
));
}
public function testValidateValuesSucceedsIfValidValueCallback()
{
$options = array(
'test' => true,
);
Options::validateValues($options, array(
'test' => function ($value) {
return true;
},
));
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testValidateValuesFailsIfInvalidValueCallback()
{
$options = array(
'test' => true,
);
Options::validateValues($options, array(
'test' => function ($value) {
return false;
},
));
}
public function testArrayAccess()
{
$this->assertFalse(isset($this->options['foo']));
@ -91,15 +475,37 @@ class OptionsTest extends \PHPUnit_Framework_TestCase
public function testSetLazyOption()
{
$test = $this;
$this->options->set('foo', function (Options $options) use ($test) {
$this->options->set('foo', function (Options $options) {
return 'dynamic';
});
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public static function getLazyOptionStatic(Options $options)
{
return 'dynamic';
}
public function testSetLazyOptionToClassMethod()
{
$this->options->set('foo', array(__CLASS__, 'getLazyOptionStatic'));
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public static function getLazyOption(Options $options)
{
return 'dynamic';
}
public function testSetLazyOptionToInstanceMethod()
{
$this->options->set('foo', array($this, 'getLazyOption'));
$this->assertEquals('dynamic', $this->options->get('foo'));
}
public function testSetDiscardsPreviousValue()
{
$test = $this;
@ -372,7 +778,7 @@ class OptionsTest extends \PHPUnit_Framework_TestCase
'two' => '2',
'three' => function (Options $options) {
return '2' === $options['two'] ? '3' : 'foo';
}
},
));
$this->assertEquals(array(