diff --git a/UPGRADE-2.6.md b/UPGRADE-2.6.md index 817d35f5e7b9a..288d3ecad8c11 100644 --- a/UPGRADE-2.6.md +++ b/UPGRADE-2.6.md @@ -128,3 +128,184 @@ HttpFoundation - You would need to migrate the table manually if you want to keep session information of your users. - You could use `PdoSessionHandler::createTable` to initialize a correctly defined table depending on the used database vendor. + +OptionsResolver +--------------- + + * The "array" type hint was removed from the `OptionsResolverInterface` methods + `setRequired()`, `setAllowedValues()`, `addAllowedValues()`, + `setAllowedTypes()` and `addAllowedTypes()`. You must remove the type hint + from your implementations. + + * The interface `OptionsResolverInterface` was deprecated, since + `OptionsResolver` instances are not supposed to be shared between classes. + You should type hint against `OptionsResolver` instead. + + Before: + + ```php + protected function configureOptions(OptionsResolverInterface $resolver) + { + // ... + } + ``` + + After: + + ```php + protected function configureOptions(OptionsResolver $resolver) + { + // ... + } + ``` + + * `OptionsResolver::isRequired()` now returns `true` if a required option has + a default value set. The new method `isMissing()` exhibits the old + functionality of `isRequired()`. + + Before: + + ```php + $resolver->setRequired(array('port')); + + $resolver->isRequired('port'); + // => true + + $resolver->setDefaults(array('port' => 25)); + + $resolver->isRequired('port'); + // => false + ``` + + After: + + ```php + $resolver->setRequired(array('port')); + + $resolver->isRequired('port'); + // => true + $resolver->isMissing('port'); + // => true + + $resolver->setDefaults(array('port' => 25)); + + $resolver->isRequired('port'); + // => true + $resolver->isMissing('port'); + // => false + ``` + + * `OptionsResolver::replaceDefaults()` was deprecated. Use `clear()` and + `setDefaults()` instead. + + Before: + + ```php + $resolver->replaceDefaults(array( + 'port' => 25, + )); + ``` + + After: + + ```php + $resolver->clear(); + $resolver->setDefaults(array( + 'port' => 25, + )); + ``` + + * `OptionsResolver::setOptional()` was deprecated. Use `setDefined()` instead. + + Before: + + ```php + $resolver->setOptional(array('port')); + ``` + + After: + + ```php + $resolver->setDefined('port'); + ``` + + * `OptionsResolver::isKnown()` was deprecated. Use `isDefined()` instead. + + Before: + + ```php + if ($resolver->isKnown('port')) { + // ... + } + ``` + + After: + + ```php + if ($resolver->isDefined('port')) { + // ... + } + ``` + + * The methods `setAllowedValues()`, `addAllowedValues()`, `setAllowedTypes()` + and `addAllowedTypes()` were changed to modify one option at a time instead + of batch processing options. The old API exists for backwards compatibility, + but will be removed in Symfony 3.0. + + Before: + + ```php + $resolver->setAllowedValues(array( + 'method' => array('POST', 'GET'), + )); + ``` + + After: + + ```php + $resolver->setAllowedValues('method', array('POST', 'GET')); + ``` + + * The class `Options` was merged into `OptionsResolver`. If you instantiated + this class manually, you should instantiate `OptionsResolver` now. + `Options` is now a marker interface implemented by `OptionsResolver`. + + Before: + + ```php + $options = new Options(); + ``` + + After: + + ```php + $resolver = new OptionsResolver(); + ``` + + * Normalizers for defined but unset options are not executed anymore. If you + want to have them executed, you should define a default value. + + Before: + + ```php + $resolver->setOptional(array('port')); + $resolver->setNormalizers(array( + 'port' => function ($options, $value) { + // return normalized value + } + )); + + $options = $resolver->resolve($options); + ``` + + After: + + ```php + $resolver->setDefault('port', null); + $resolver->setNormalizer('port', function ($options, $value) { + // return normalized value + }); + + $options = $resolver->resolve($options); + ``` + diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md new file mode 100644 index 0000000000000..27cfd01cb0fef --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -0,0 +1,44 @@ +CHANGELOG +========= + +2.6.0 +----- + + * deprecated OptionsResolverInterface + * [BC BREAK] removed "array" type hint from OptionsResolverInterface methods + setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and + addAllowedTypes() + * added OptionsResolver::setDefault() + * added OptionsResolver::hasDefault() + * added OptionsResolver::setNormalizer() + * added OptionsResolver::isRequired() + * added OptionsResolver::getRequiredOptions() + * added OptionsResolver::isMissing() + * added OptionsResolver::getMissingOptions() + * added OptionsResolver::setDefined() + * added OptionsResolver::isDefined() + * added OptionsResolver::getDefinedOptions() + * added OptionsResolver::remove() + * added OptionsResolver::clear() + * deprecated OptionsResolver::replaceDefaults() + * deprecated OptionsResolver::setOptional() in favor of setDefined() + * deprecated OptionsResolver::isKnown() in favor of isDefined() + * [BC BREAK] OptionsResolver::isRequired() returns true now if a required + option has a default value set + * [BC BREAK] merged Options into OptionsResolver and turned Options into an + interface + * deprecated Options::overload() (now in OptionsResolver) + * deprecated Options::set() (now in OptionsResolver) + * deprecated Options::get() (now in OptionsResolver) + * deprecated Options::has() (now in OptionsResolver) + * deprecated Options::replace() (now in OptionsResolver) + * [BC BREAK] Options::get() (now in OptionsResolver) can only be used within + lazy option/normalizer closures now + * [BC BREAK] removed Traversable interface from Options since using within + lazy option/normalizer closures resulted in exceptions + * [BC BREAK] removed Options::all() since using within lazy option/normalizer + closures resulted in exceptions + * [BC BREAK] OptionDefinitionException now extends LogicException instead of + RuntimeException + * [BC BREAK] normalizers are not executed anymore for unset options + * normalizers are executed after validating the options now diff --git a/src/Symfony/Component/OptionsResolver/Exception/AccessException.php b/src/Symfony/Component/OptionsResolver/Exception/AccessException.php new file mode 100644 index 0000000000000..6a28b0ae64021 --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Exception/AccessException.php @@ -0,0 +1,22 @@ + + * + * 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 trying to read an option outside of or write it inside of + * {@link Options::resolve()}. + * + * @author Bernhard Schussek + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/OptionsResolver/Exception/InvalidArgumentException.php b/src/Symfony/Component/OptionsResolver/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..6d421d68b35cb --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.php b/src/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.php index 2e7ea1bc0c419..6fd4f125f4478 100644 --- a/src/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.php +++ b/src/Symfony/Component/OptionsResolver/Exception/InvalidOptionsException.php @@ -12,10 +12,12 @@ namespace Symfony\Component\OptionsResolver\Exception; /** - * Exception thrown when an invalid option is passed. + * Thrown when the value of an option does not match its validation rules. + * + * You should make sure a valid value is passed to the option. * * @author Bernhard Schussek */ -class InvalidOptionsException extends \InvalidArgumentException implements ExceptionInterface +class InvalidOptionsException extends InvalidArgumentException { } diff --git a/src/Symfony/Component/OptionsResolver/Exception/MissingOptionsException.php b/src/Symfony/Component/OptionsResolver/Exception/MissingOptionsException.php index 8544dfb2ee25b..faa487f16f003 100644 --- a/src/Symfony/Component/OptionsResolver/Exception/MissingOptionsException.php +++ b/src/Symfony/Component/OptionsResolver/Exception/MissingOptionsException.php @@ -14,8 +14,10 @@ /** * Exception thrown when a required option is missing. * + * Add the option to the passed options array. + * * @author Bernhard Schussek */ -class MissingOptionsException extends \InvalidArgumentException implements ExceptionInterface +class MissingOptionsException extends InvalidArgumentException { } diff --git a/src/Symfony/Component/OptionsResolver/Exception/OptionDefinitionException.php b/src/Symfony/Component/OptionsResolver/Exception/OptionDefinitionException.php index 11617fe1a165d..e8e339d446efa 100644 --- a/src/Symfony/Component/OptionsResolver/Exception/OptionDefinitionException.php +++ b/src/Symfony/Component/OptionsResolver/Exception/OptionDefinitionException.php @@ -12,10 +12,10 @@ namespace Symfony\Component\OptionsResolver\Exception; /** - * Thrown when an option definition is invalid. + * Thrown when two lazy options have a cyclic dependency. * * @author Bernhard Schussek */ -class OptionDefinitionException extends \RuntimeException implements ExceptionInterface +class OptionDefinitionException extends \LogicException implements ExceptionInterface { } diff --git a/src/Symfony/Component/OptionsResolver/Exception/UndefinedOptionsException.php b/src/Symfony/Component/OptionsResolver/Exception/UndefinedOptionsException.php new file mode 100644 index 0000000000000..6ca3fce470a60 --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Exception/UndefinedOptionsException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when an undefined option is passed. + * + * You should remove the options in question from your code or define them + * beforehand. + * + * @author Bernhard Schussek + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/src/Symfony/Component/OptionsResolver/Options.php b/src/Symfony/Component/OptionsResolver/Options.php index 43c81f0c33403..d444ec4230d51 100644 --- a/src/Symfony/Component/OptionsResolver/Options.php +++ b/src/Symfony/Component/OptionsResolver/Options.php @@ -11,503 +11,12 @@ namespace Symfony\Component\OptionsResolver; -use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; - /** - * Container for resolving inter-dependent options. + * Contains resolved option values. * * @author Bernhard Schussek + * @author Tobias Schultze */ -class Options implements \ArrayAccess, \Iterator, \Countable +interface Options extends \ArrayAccess, \Countable { - /** - * A list of option values. - * @var array - */ - private $options = array(); - - /** - * A list of normalizer closures. - * @var array - */ - private $normalizers = array(); - - /** - * A list of closures for evaluating lazy options. - * @var array - */ - private $lazy = array(); - - /** - * A list containing the currently locked options. - * @var array - */ - private $lock = array(); - - /** - * Whether at least one option has already been read. - * - * Once read, the options cannot be changed anymore. This is - * necessary in order to avoid inconsistencies during the resolving - * process. If any option is changed after being read, all evaluated - * lazy options that depend on this option would become invalid. - * - * @var bool - */ - private $reading = false; - - /** - * Sets the value of a given option. - * - * You can set lazy options by passing a closure with the following - * signature: - * - * - * function (Options $options) - * - * - * This closure will be evaluated once the option is read using - * {@link get()}. The closure has access to the resolved values of - * other options through the passed {@link Options} instance. - * - * @param string $option The name of the option. - * @param mixed $value The value of the option. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - */ - public function set($option, $value) - { - // Setting is not possible once an option is read, because then lazy - // options could manipulate the state of the object, leading to - // inconsistent results. - if ($this->reading) { - throw new OptionDefinitionException('Options cannot be set anymore once options have been read.'); - } - - // Setting is equivalent to overloading while discarding the previous - // option value - unset($this->options[$option]); - unset($this->lazy[$option]); - - $this->overload($option, $value); - } - - /** - * Sets the normalizer for a given option. - * - * Normalizers should be closures with the following signature: - * - * - * function (Options $options, $value) - * - * - * This closure will be evaluated once the option is read using - * {@link get()}. The closure has access to the resolved values of - * other options through the passed {@link Options} instance. - * - * @param string $option The name of the option. - * @param \Closure $normalizer The normalizer. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - */ - public function setNormalizer($option, \Closure $normalizer) - { - if ($this->reading) { - throw new OptionDefinitionException('Normalizers cannot be added anymore once options have been read.'); - } - - $this->normalizers[$option] = $normalizer; - } - - /** - * Replaces the contents of the container with the given options. - * - * This method is a shortcut for {@link clear()} with subsequent - * calls to {@link set()}. - * - * @param array $options The options to set. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - */ - public function replace(array $options) - { - if ($this->reading) { - throw new OptionDefinitionException('Options cannot be replaced anymore once options have been read.'); - } - - $this->options = array(); - $this->lazy = array(); - $this->normalizers = array(); - - foreach ($options as $option => $value) { - $this->overload($option, $value); - } - } - - /** - * Overloads the value of a given option. - * - * Contrary to {@link set()}, this method keeps the previous default - * value of the option so that you can access it if you pass a closure. - * Passed closures should have the following signature: - * - * - * function (Options $options, $value) - * - * - * The second parameter passed to the closure is the current default - * value of the option. - * - * @param string $option The option name. - * @param mixed $value The option value. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - */ - public function overload($option, $value) - { - if ($this->reading) { - throw new OptionDefinitionException('Options cannot be overloaded anymore once options have been read.'); - } - - // 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); - $params = $reflClosure->getParameters(); - - if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && __CLASS__ === $class->name) { - // Initialize the option if no previous value exists - if (!isset($this->options[$option])) { - $this->options[$option] = null; - } - - // Ignore previous lazy options if the closure has no second parameter - if (!isset($this->lazy[$option]) || !isset($params[1])) { - $this->lazy[$option] = array(); - } - - // Store closure for later evaluation - $this->lazy[$option][] = $value; - - return; - } - } - - // Remove lazy options by default - unset($this->lazy[$option]); - - $this->options[$option] = $value; - } - - /** - * Returns the value of the given option. - * - * If the option was a lazy option, it is evaluated now. - * - * @param string $option The option name. - * - * @return mixed The option value. - * - * @throws \OutOfBoundsException If the option does not exist. - * @throws OptionDefinitionException If a cyclic dependency is detected - * between two lazy options. - */ - public function get($option) - { - $this->reading = true; - - if (!array_key_exists($option, $this->options)) { - throw new \OutOfBoundsException(sprintf('The option "%s" does not exist.', $option)); - } - - if (isset($this->lazy[$option])) { - $this->resolve($option); - } - - if (isset($this->normalizers[$option])) { - $this->normalize($option); - } - - return $this->options[$option]; - } - - /** - * Returns whether the given option exists. - * - * @param string $option The option name. - * - * @return bool Whether the option exists. - */ - public function has($option) - { - return array_key_exists($option, $this->options); - } - - /** - * Removes the option with the given name. - * - * @param string $option The option name. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - */ - public function remove($option) - { - if ($this->reading) { - throw new OptionDefinitionException('Options cannot be removed anymore once options have been read.'); - } - - unset($this->options[$option]); - unset($this->lazy[$option]); - unset($this->normalizers[$option]); - } - - /** - * Removes all options. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - */ - public function clear() - { - if ($this->reading) { - throw new OptionDefinitionException('Options cannot be cleared anymore once options have been read.'); - } - - $this->options = array(); - $this->lazy = array(); - $this->normalizers = array(); - } - - /** - * Returns the values of all options. - * - * Lazy options are evaluated at this point. - * - * @return array The option values. - */ - public function all() - { - $this->reading = true; - - // Performance-wise this is slightly better than - // while (null !== $option = key($this->lazy)) - foreach ($this->lazy as $option => $closures) { - // Double check, in case the option has already been resolved - // by cascade in the previous cycles - if (isset($this->lazy[$option])) { - $this->resolve($option); - } - } - - foreach ($this->normalizers as $option => $normalizer) { - if (isset($this->normalizers[$option])) { - $this->normalize($option); - } - } - - return $this->options; - } - - /** - * Equivalent to {@link has()}. - * - * @param string $option The option name. - * - * @return bool Whether the option exists. - * - * @see \ArrayAccess::offsetExists() - */ - public function offsetExists($option) - { - return $this->has($option); - } - - /** - * Equivalent to {@link get()}. - * - * @param string $option The option name. - * - * @return mixed The option value. - * - * @throws \OutOfBoundsException If the option does not exist. - * @throws OptionDefinitionException If a cyclic dependency is detected - * between two lazy options. - * - * @see \ArrayAccess::offsetGet() - */ - public function offsetGet($option) - { - return $this->get($option); - } - - /** - * Equivalent to {@link set()}. - * - * @param string $option The name of the option. - * @param mixed $value The value of the option. May be a closure with a - * signature as defined in DefaultOptions::add(). - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - * - * @see \ArrayAccess::offsetSet() - */ - public function offsetSet($option, $value) - { - $this->set($option, $value); - } - - /** - * Equivalent to {@link remove()}. - * - * @param string $option The option name. - * - * @throws OptionDefinitionException If options have already been read. - * Once options are read, the container - * becomes immutable. - * - * @see \ArrayAccess::offsetUnset() - */ - public function offsetUnset($option) - { - $this->remove($option); - } - - /** - * {@inheritdoc} - */ - public function current() - { - return $this->get($this->key()); - } - - /** - * {@inheritdoc} - */ - public function next() - { - next($this->options); - } - - /** - * {@inheritdoc} - */ - public function key() - { - return key($this->options); - } - - /** - * {@inheritdoc} - */ - public function valid() - { - return null !== $this->key(); - } - - /** - * {@inheritdoc} - */ - public function rewind() - { - reset($this->options); - } - - /** - * {@inheritdoc} - */ - public function count() - { - return count($this->options); - } - - /** - * Evaluates the given lazy option. - * - * The evaluated value is written into the options array. The closure for - * evaluating the option is discarded afterwards. - * - * @param string $option The option to evaluate. - * - * @throws OptionDefinitionException If the option has a cyclic dependency - * on another option. - */ - private function resolve($option) - { - // The code duplication with normalize() exists for performance - // reasons, in order to save a method call. - // Remember that this method is potentially called a couple of thousand - // times and needs to be as efficient as possible. - if (isset($this->lock[$option])) { - $conflicts = array(); - - foreach ($this->lock as $option => $locked) { - if ($locked) { - $conflicts[] = $option; - } - } - - throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts))); - } - - $this->lock[$option] = true; - foreach ($this->lazy[$option] as $closure) { - $this->options[$option] = $closure($this, $this->options[$option]); - } - unset($this->lock[$option]); - - // The option now isn't lazy anymore - unset($this->lazy[$option]); - } - - /** - * Normalizes the given option. - * - * The evaluated value is written into the options array. - * - * @param string $option The option to normalizer. - * - * @throws OptionDefinitionException If the option has a cyclic dependency - * on another option. - */ - private function normalize($option) - { - // The code duplication with resolve() exists for performance - // reasons, in order to save a method call. - // Remember that this method is potentially called a couple of thousand - // times and needs to be as efficient as possible. - if (isset($this->lock[$option])) { - $conflicts = array(); - - foreach ($this->lock as $option => $locked) { - if ($locked) { - $conflicts[] = $option; - } - } - - throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts))); - } - - /** @var \Closure $normalizer */ - $normalizer = $this->normalizers[$option]; - - $this->lock[$option] = true; - $this->options[$option] = $normalizer($this, array_key_exists($option, $this->options) ? $this->options[$option] : null); - unset($this->lock[$option]); - - // The option is now normalized - unset($this->normalizers[$option]); - } } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 9c5dfd628c560..f841ba5e43c5b 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -11,172 +11,407 @@ namespace Symfony\Component\OptionsResolver; -use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\AccessException; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; /** - * Helper for merging default and concrete option values. + * Validates options and merges them with default values. * * @author Bernhard Schussek * @author Tobias Schultze */ -class OptionsResolver implements OptionsResolverInterface +class OptionsResolver implements Options, OptionsResolverInterface { + /** + * The fully qualified name of the {@link Options} interface. + * + * @internal + */ + const OPTIONS_INTERFACE = 'Symfony\\Component\\OptionsResolver\\Options'; + + /** + * The names of all defined options. + * + * @var array + */ + private $defined = array(); + /** * The default option values. - * @var Options + * + * @var array */ - private $defaultOptions; + private $defaults = array(); /** - * The options known by the resolver. + * The names of required options. + * * @var array */ - private $knownOptions = array(); + private $required = array(); /** - * The options without defaults that are required to be passed to resolve(). + * The resolved option values. + * * @var array */ - private $requiredOptions = array(); + private $resolved = array(); + + /** + * A list of normalizer closures. + * + * @var \Closure[] + */ + private $normalizers = 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. + * A list of closures for evaluating lazy options. + * + * @var array */ - public function __construct() - { - $this->defaultOptions = new Options(); - } + private $lazy = array(); /** - * Clones the resolver. + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + * + * @var array */ - public function __clone() - { - $this->defaultOptions = clone $this->defaultOptions; - } + private $calling = array(); /** - * {@inheritdoc} + * Whether the instance is locked for reading. + * + * Once locked, the options cannot be changed anymore. This is + * necessary in order to avoid inconsistencies during the resolving + * process. If any option is changed after being read, all evaluated + * lazy options that depend on this option would become invalid. + * + * @var bool + */ + private $locked = false; + + /** + * Sets the default value of a given option. + * + * If the default value should be set based on other options, you can pass + * a closure with the following signature: + * + * function (Options $options) { + * // ... + * } + * + * The closure will be evaluated when {@link resolve()} is called. The + * closure has access to the resolved values of other options through the + * passed {@link Options} instance: + * + * function (Options $options) { + * if (isset($options['port'])) { + * // ... + * } + * } + * + * If you want to access the previously set default value, add a second + * argument to the closure's signature: + * + * $options->setDefault('name', 'Default Name'); + * + * $options->setDefault('name', function (Options $options, $previousValue) { + * // 'Default Name' === $previousValue + * }); + * + * This is mostly useful if the configuration of the {@link Options} object + * is spread across different locations of your code, such as base and + * sub-classes. + * + * @param string $option The name of the option + * @param mixed $value The default value of the option + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already */ - public function setDefaults(array $defaultValues) + public function setDefault($option, $value) { - foreach ($defaultValues as $option => $value) { - $this->defaultOptions->overload($option, $value); - $this->knownOptions[$option] = true; - unset($this->requiredOptions[$option]); + // Setting is not possible once resolving starts, because then lazy + // options could manipulate the state of the object, leading to + // inconsistent results. + if ($this->locked) { + throw new AccessException('Default values cannot be set anymore once resolving has begun.'); } + // 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); + $params = $reflClosure->getParameters(); + + if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && self::OPTIONS_INTERFACE === $class->name) { + // Initialize the option if no previous value exists + if (!isset($this->defaults[$option])) { + $this->defaults[$option] = null; + } + + // Ignore previous lazy options if the closure has no second parameter + if (!isset($this->lazy[$option]) || !isset($params[1])) { + $this->lazy[$option] = array(); + } + + // Store closure for later evaluation + $this->lazy[$option][] = $value; + $this->defined[$option] = true; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + } + + // This option is not lazy anymore + unset($this->lazy[$option]); + + // Yet undefined options can be marked as resolved, because we only need + // to resolve options with lazy closures, normalizers or validation + // rules, none of which can exist for undefined options + // If the option was resolved before, update the resolved value + if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) { + $this->resolved[$option] = $value; + } + + $this->defaults[$option] = $value; + $this->defined[$option] = true; + return $this; } /** - * {@inheritdoc} + * Sets a list of default values. + * + * @param array $defaults The default values to set + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already */ - public function replaceDefaults(array $defaultValues) + public function setDefaults(array $defaults) { - foreach ($defaultValues as $option => $value) { - $this->defaultOptions->set($option, $value); - $this->knownOptions[$option] = true; - unset($this->requiredOptions[$option]); + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); } return $this; } /** - * {@inheritdoc} + * Returns whether a default value is set for an option. + * + * Returns true if {@link setDefault()} was called for this option. + * An option is also considered set if it was set to null. + * + * @param string $option The option name + * + * @return bool Whether a default value is set */ - public function setOptional(array $optionNames) + public function hasDefault($option) { - 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; + return array_key_exists($option, $this->defaults); } /** - * {@inheritdoc} + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already */ - public function setRequired(array $optionNames) + public function setRequired($optionNames) { - foreach ($optionNames as $key => $option) { - if (!is_int($key)) { - throw new OptionDefinitionException('You should not pass default values to setRequired()'); - } + if ($this->locked) { + throw new AccessException('Options cannot be made required anymore once resolving has begun.'); + } - $this->knownOptions[$option] = true; - // set as required if no default has been set already - if (!isset($this->defaultOptions[$option])) { - $this->requiredOptions[$option] = true; - } + foreach ((array) $optionNames as $key => $option) { + $this->defined[$option] = true; + $this->required[$option] = true; } return $this; } /** - * {@inheritdoc} + * Returns whether an option is required. + * + * An option is required if it was passed to {@link setRequired()}. + * + * @param string $option The name of the option + * + * @return bool Whether the option is required */ - public function setAllowedValues(array $allowedValues) + public function isRequired($option) { - $this->validateOptionsExistence($allowedValues); + return isset($this->required[$option]); + } - $this->allowedValues = array_replace($this->allowedValues, $allowedValues); + /** + * Returns the names of all required options. + * + * @return string[] The names of the required options + * + * @see isRequired() + */ + public function getRequiredOptions() + { + return array_keys($this->required); + } - return $this; + /** + * Returns whether an option is missing a default value. + * + * An option is missing if it was passed to {@link setRequired()}, but not + * to {@link setDefault()}. This option must be passed explicitly to + * {@link resolve()}, otherwise an exception will be thrown. + * + * @param string $option The name of the option + * + * @return bool Whether the option is missing + */ + public function isMissing($option) + { + return isset($this->required[$option]) && !array_key_exists($option, $this->defaults); } /** - * {@inheritdoc} + * Returns the names of all options missing a default value. + * + * @return string[] The names of the missing options + * + * @see isMissing() */ - public function addAllowedValues(array $allowedValues) + public function getMissingOptions() { - $this->validateOptionsExistence($allowedValues); + return array_keys(array_diff_key($this->required, $this->defaults)); + } + + /** + * Defines a valid option name. + * + * Defines an option name without setting a default value. The option will + * be accepted when passed to {@link resolve()}. When not passed, the + * option will not be included in the resolved options. + * + * @param string|string[] $optionNames One or more option names + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + */ + public function setDefined($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be defined anymore once resolving has begun.'); + } - $this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues); + foreach ((array) $optionNames as $key => $option) { + $this->defined[$option] = true; + } return $this; } /** - * {@inheritdoc} + * Returns whether an option is defined. + * + * Returns true for any option passed to {@link setDefault()}, + * {@link setRequired()} or {@link setDefined()}. + * + * @param string $option The option name + * + * @return bool Whether the option is defined */ - public function setAllowedTypes(array $allowedTypes) + public function isDefined($option) { - $this->validateOptionsExistence($allowedTypes); - - $this->allowedTypes = array_replace($this->allowedTypes, $allowedTypes); + return isset($this->defined[$option]); + } - return $this; + /** + * Returns the names of all defined options. + * + * @return string[] The names of the defined options + * + * @see isDefined() + */ + public function getDefinedOptions() + { + return array_keys($this->defined); } /** - * {@inheritdoc} + * Sets the normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * ```php + * function (Options $options, $value) { + * // ... + * } + * ``` + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + * @throws UndefinedOptionsException If the option is undefined */ - public function addAllowedTypes(array $allowedTypes) + public function setNormalizer($option, \Closure $normalizer) { - $this->validateOptionsExistence($allowedTypes); + if ($this->locked) { + throw new AccessException('Normalizers cannot be added anymore once resolving has begun.'); + } - $this->allowedTypes = array_merge_recursive($this->allowedTypes, $allowedTypes); + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Known options are: "%s"', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->normalizers[$option] = $normalizer; + + // Make sure the option is processed + unset($this->resolved[$option]); return $this; } @@ -186,91 +421,329 @@ public function addAllowedTypes(array $allowedTypes) */ public function setNormalizers(array $normalizers) { - $this->validateOptionsExistence($normalizers); - foreach ($normalizers as $option => $normalizer) { - $this->defaultOptions->setNormalizer($option, $normalizer); + $this->setNormalizer($option, $normalizer); } return $this; } /** - * {@inheritdoc} + * Sets allowed values for an option. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + * @throws UndefinedOptionsException If an option is undefined */ - public function isKnown($option) + public function setAllowedValues($option, $allowedValues = null) { - return isset($this->knownOptions[$option]); + if ($this->locked) { + throw new AccessException('Allowed values cannot be set anymore once resolving has begun.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + foreach ($option as $optionName => $optionValues) { + $this->setAllowedValues($optionName, $optionValues); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Known options are: "%s"', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedValues[$option] = $allowedValues instanceof \Closure ? array($allowedValues) : (array) $allowedValues; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; } /** - * {@inheritdoc} + * Adds allowed values for an option. + * + * The values are merged with the allowed values defined previously. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + * @throws UndefinedOptionsException If an option is undefined */ - public function isRequired($option) + public function addAllowedValues($option, $allowedValues = null) { - return isset($this->requiredOptions[$option]); + if ($this->locked) { + throw new AccessException('Allowed values cannot be added anymore once resolving has begun.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + foreach ($option as $optionName => $optionValues) { + $this->addAllowedValues($optionName, $optionValues); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Known options are: "%s"', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if ($allowedValues instanceof \Closure) { + $this->allowedValues[$option][] = $allowedValues; + } elseif (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = (array) $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], (array) $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; } /** - * {@inheritdoc} + * Sets allowed types for an option. + * + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + * @throws UndefinedOptionsException If an option is undefined */ - public function resolve(array $options = array()) + public function setAllowedTypes($option, $allowedTypes = null) { - $this->validateOptionsExistence($options); - $this->validateOptionsCompleteness($options); + if ($this->locked) { + throw new AccessException('Allowed types cannot be set anymore once resolving has begun.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + foreach ($option as $optionName => $optionTypes) { + $this->setAllowedTypes($optionName, $optionTypes); + } - // Make sure this method can be called multiple times - $combinedOptions = clone $this->defaultOptions; + return $this; + } - // Override options set by the user - foreach ($options as $option => $value) { - $combinedOptions->set($option, $value); + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Known options are: "%s"', + $option, + implode('", "', array_keys($this->defined)) + )); } - // Resolve options - $resolvedOptions = $combinedOptions->all(); + $this->allowedTypes[$option] = (array) $allowedTypes; - $this->validateOptionTypes($resolvedOptions); - $this->validateOptionValues($resolvedOptions); + // Make sure the option is processed + unset($this->resolved[$option]); - return $resolvedOptions; + return $this; } /** - * Validates that the given option names exist and throws an exception - * otherwise. + * Adds allowed types for an option. * - * @param array $options An list of option names as keys. + * The types are merged with the allowed types defined previously. * - * @throws InvalidOptionsException If any of the options has not been defined. + * Any type for which a corresponding is_() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + * @throws UndefinedOptionsException If an option is undefined */ - private function validateOptionsExistence(array $options) + public function addAllowedTypes($option, $allowedTypes = null) { - $diff = array_diff_key($options, $this->knownOptions); + if ($this->locked) { + throw new AccessException('Allowed types cannot be added anymore once resolving has begun.'); + } - if (count($diff) > 0) { - ksort($this->knownOptions); - ksort($diff); + // BC + if (is_array($option) && null === $allowedTypes) { + foreach ($option as $optionName => $optionTypes) { + $this->addAllowedTypes($optionName, $optionTypes); + } - 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)) + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Known options are: "%s"', + $option, + implode('", "', array_keys($this->defined)) )); } + + if (!isset($this->allowedTypes[$option])) { + $this->allowedTypes[$option] = (array) $allowedTypes; + } else { + $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; } /** - * Validates that all required options are given and throws an exception - * otherwise. + * Removes the option with the given name. * - * @param array $options An list of option names as keys. + * Undefined options are ignored. * - * @throws MissingOptionsException If a required option is missing. + * @param string|string[] $optionNames One or more option names + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already */ - private function validateOptionsCompleteness(array $options) + public function remove($optionNames) { - $diff = array_diff_key($this->requiredOptions, $options); + if ($this->locked) { + throw new AccessException('Options cannot be removed anymore once resolving has begun.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option]); + unset($this->defaults[$option]); + unset($this->required[$option]); + unset($this->resolved[$option]); + unset($this->lazy[$option]); + unset($this->normalizers[$option]); + unset($this->allowedTypes[$option]); + unset($this->allowedValues[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return Options This instance + * + * @throws AccessException If the options are resolved already + */ + public function clear() + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared anymore once resolving has begun.'); + } + + $this->defined = array(); + $this->defaults = array(); + $this->required = array(); + $this->resolved = array(); + $this->lazy = array(); + $this->normalizers = array(); + $this->allowedTypes = array(); + $this->allowedValues = array(); + + return $this; + } + + /** + * Merges options with the default values stored in the container and + * validates them. + * + * Exceptions are thrown if: + * + * - Undefined options are passed; + * - Required options are missing; + * - Options have invalid types; + * - Options have invalid values. + * + * @param array $options A map of option names to values + * + * @return array The merged and validated options + * + * @throws UndefinedOptionsException If an option name is undefined + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws MissingOptionsException If a required option is missing + * @throws OptionDefinitionException If a cyclic dependency is depended + * between lazy options and/or normalizers + */ + public function resolve(array $options = array()) + { + // Allow this method to be called multiple times + $clone = clone $this; + + // Make sure that no unknown options are passed + $diff = array_diff_key($options, $clone->defined); + + if (count($diff) > 0) { + ksort($clone->defined); + ksort($diff); + + throw new UndefinedOptionsException(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($clone->defined)) + )); + } + + // Override options set by the user + foreach ($options as $option => $value) { + $clone->defaults[$option] = $value; + unset($clone->resolved[$option], $clone->lazy[$option]); + } + + // Check whether any required option is missing + $diff = array_diff_key($clone->required, $clone->defaults); if (count($diff) > 0) { ksort($diff); @@ -280,73 +753,391 @@ private function validateOptionsCompleteness(array $options) implode('", "', array_keys($diff)) )); } + + // Lock the container + $clone->locked = true; + + // Now process the individual options. Use offsetGet(), which resolves + // the option itself and any options that the option depends on + foreach ($clone->defaults as $option => $_) { + $clone->offsetGet($option); + } + + return $clone->resolved; } /** - * Validates that the given option values match the allowed values and - * throws an exception otherwise. + * Returns the resolved value of an option. + * + * @param string $option The option name * - * @param array $options A list of option values. + * @return mixed The option value * - * @throws InvalidOptionsException If any of the values does not match the - * allowed values of the option. + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws \OutOfBoundsException If the option is not set + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If a cyclic dependency is detected + * between two lazily evaluated options */ - private function validateOptionValues(array $options) + public function offsetGet($option) { - 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 (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + // Shortcut for resolved options + if (array_key_exists($option, $this->resolved)) { + return $this->resolved[$option]; + } + + // Check whether the option is set at all + if (!array_key_exists($option, $this->defaults)) { + throw new \OutOfBoundsException(sprintf('The option "%s" was not set.', $option)); + } + + $value = $this->defaults[$option]; + + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + foreach ($this->lazy[$option] as $closure) { + $value = call_user_func($closure, $this, $value); + } + unset($this->calling[$option]); + // END + } + + // Validate the type of the resolved option + if (isset($this->allowedTypes[$option])) { + $valid = false; + + foreach ($this->allowedTypes[$option] as $type) { + if (function_exists($isFunction = 'is_'.$type)) { + if ($isFunction($value)) { + $valid = true; + break; + } + + continue; + } + + if ($value instanceof $type) { + $valid = true; + break; + } + } + + if (!$valid) { + throw new InvalidOptionsException(sprintf( + 'The option "%s" with value %s is expected to be of type '. + '"%s", but is of type "%s".', + $option, + $this->formatValue($value), + implode('", "', $this->allowedTypes[$option]), + $this->formatTypeOf($value) + )); + } + } + + // Validate the value of the resolved option + if (isset($this->allowedValues[$option])) { + $success = false; + $printableAllowedValues = array(); + + foreach ($this->allowedValues[$option] as $allowedValue) { + if ($allowedValue instanceof \Closure) { + if ($allowedValue($value)) { + $success = true; + break; + } + + // Don't include closures in the exception message + continue; + } elseif ($value === $allowedValue) { + $success = true; + break; } - 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])); + $printableAllowedValues[] = is_object($value) + ? get_class($value) + : (is_array($value) ? 'array' : (string) $value); + } + + if (!$success) { + $message = sprintf( + 'The option "%s" with value %s is invalid.', + $option, + $this->formatValue($value) + ); + + if (count($printableAllowedValues) > 0) { + $message .= sprintf( + ' Accepted values are: %s', + $this->formatValues($printableAllowedValues) + ); } + + throw new InvalidOptionsException($message); + } + } + + // Normalize the validated option + if (isset($this->normalizers[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); } + + $normalizer = $this->normalizers[$option]; + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + $value = call_user_func($normalizer, $this, $value); + unset($this->calling[$option]); + // END } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; } /** - * Validates that the given options match the allowed types and - * throws an exception otherwise. + * Returns whether a resolved option with the given name exists. * - * @param array $options A list of options. + * @param string $option The option name * - * @throws InvalidOptionsException If any of the types does not match the - * allowed types of the option. + * @return bool Whether the option is set + * + * @see \ArrayAccess::offsetExists() */ - private function validateOptionTypes(array $options) + public function offsetExists($option) { - foreach ($this->allowedTypes as $option => $allowedTypes) { - if (!array_key_exists($option, $options)) { - continue; - } + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } - $value = $options[$option]; - $allowedTypes = (array) $allowedTypes; + return array_key_exists($option, $this->defaults); + } - foreach ($allowedTypes as $type) { - $isFunction = 'is_'.$type; + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet($option, $value) + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } - if (function_exists($isFunction) && $isFunction($value)) { - continue 2; - } elseif ($value instanceof $type) { - continue 2; - } - } + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset($option) + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * {@inheritdoc} + */ + public function count() + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } - $printableValue = is_object($value) - ? get_class($value) - : (is_array($value) - ? 'Array' - : (string) $value); + return count($this->resolved); + } - throw new InvalidOptionsException(sprintf( - 'The option "%s" with value "%s" is expected to be of type "%s"', - $option, - $printableValue, - implode('", "', $allowedTypes) - )); + /** + * Alias of {@link setDefault()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function set($option, $value) + { + return $this->setDefault($option, $value); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function replace(array $defaults) + { + $this->clear(); + + return $this->setDefaults($defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function overload($option, $value) + { + return $this->setDefault($option, $value); + } + + /** + * Alias of {@link offsetGet()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function get($option) + { + return $this->offsetGet($option); + } + + /** + * Alias of {@link offsetExists()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function has($option) + { + return $this->offsetExists($option); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function replaceDefaults(array $defaultValues) + { + $this->clear(); + + return $this->setDefaults($defaultValues); + } + + /** + * Alias of {@link setDefined()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function setOptional(array $optionNames) + { + return $this->setDefined($optionNames); + } + + /** + * Alias of {@link isDefined()}. + * + * @deprecated Deprecated as of Symfony 2.6, to be removed in Symfony 3.0. + */ + public function isKnown($option) + { + return $this->isDefined($option); + } + + /** + * Returns a string representation of the type of the value. + * + * This method should be used if you pass the type of a value as + * message parameter to a constraint violation. Note that such + * parameters should usually not be included in messages aimed at + * non-technical people. + * + * @param mixed $value The value to return the type of + * + * @return string The type of the value + */ + private function formatTypeOf($value) + { + return is_object($value) ? get_class($value) : gettype($value); + } + + /** + * Returns a string representation of the value. + * + * This method returns the equivalent PHP tokens for most scalar types + * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped + * in double quotes ("). + * + * @param mixed $value The value to format as string + * + * @return string The string representation of the passed value + */ + private function formatValue($value) + { + if (is_object($value)) { + return get_class($value); + } + + if (is_array($value)) { + return 'array'; + } + + if (is_string($value)) { + return '"'.$value.'"'; + } + + if (is_resource($value)) { + return 'resource'; + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; } + + return (string) $value; + } + + /** + * Returns a string representation of a list of values. + * + * Each of the values is converted to a string using + * {@link formatValue()}. The values are then concatenated with commas. + * + * @param array $values A list of values + * + * @return string The string representation of the value list + * + * @see formatValue() + */ + private function formatValues(array $values) + { + foreach ($values as $key => $value) { + $values[$key] = $this->formatValue($value); + } + + return implode(', ', $values); } } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolverInterface.php b/src/Symfony/Component/OptionsResolver/OptionsResolverInterface.php index fa57fef0fda3f..fea7ea8843369 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolverInterface.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolverInterface.php @@ -11,8 +11,15 @@ namespace Symfony\Component\OptionsResolver; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; + /** * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.6, to be removed in Symfony 3.0. + * Use {@link OptionsResolver} instead. */ interface OptionsResolverInterface { @@ -68,8 +75,6 @@ public function replaceDefaults(array $defaultValues); * @param array $optionNames A list of option names. * * @return OptionsResolverInterface The resolver instance. - * - * @throws Exception\OptionDefinitionException When trying to pass default values. */ public function setOptional(array $optionNames); @@ -82,10 +87,8 @@ public function setOptional(array $optionNames); * @param array $optionNames A list of option names. * * @return OptionsResolverInterface The resolver instance. - * - * @throws Exception\OptionDefinitionException When trying to pass default values. */ - public function setRequired(array $optionNames); + public function setRequired($optionNames); /** * Sets allowed values for a list of options. @@ -96,11 +99,11 @@ public function setRequired(array $optionNames); * * @return OptionsResolverInterface The resolver instance. * - * @throws Exception\InvalidOptionsException If an option has not been defined + * @throws InvalidOptionsException If an option has not been defined * (see {@link isKnown()}) for which * an allowed value is set. */ - public function setAllowedValues(array $allowedValues); + public function setAllowedValues($allowedValues); /** * Adds allowed values for a list of options. @@ -113,11 +116,11 @@ public function setAllowedValues(array $allowedValues); * * @return OptionsResolverInterface The resolver instance. * - * @throws Exception\InvalidOptionsException If an option has not been defined + * @throws InvalidOptionsException If an option has not been defined * (see {@link isKnown()}) for which * an allowed value is set. */ - public function addAllowedValues(array $allowedValues); + public function addAllowedValues($allowedValues); /** * Sets allowed types for a list of options. @@ -127,10 +130,10 @@ public function addAllowedValues(array $allowedValues); * * @return OptionsResolverInterface The resolver instance. * - * @throws Exception\InvalidOptionsException If an option has not been defined for - * which an allowed type is set. + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. */ - public function setAllowedTypes(array $allowedTypes); + public function setAllowedTypes($allowedTypes); /** * Adds allowed types for a list of options. @@ -142,10 +145,10 @@ public function setAllowedTypes(array $allowedTypes); * * @return OptionsResolverInterface The resolver instance. * - * @throws Exception\InvalidOptionsException If an option has not been defined for - * which an allowed type is set. + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. */ - public function addAllowedTypes(array $allowedTypes); + public function addAllowedTypes($allowedTypes); /** * Sets normalizers that are applied on resolved options. @@ -175,7 +178,7 @@ public function setNormalizers(array $normalizers); * * @param string $option The name of the option. * - * @return bool Whether the option is known. + * @return bool Whether the option is known. */ public function isKnown($option); @@ -188,7 +191,7 @@ public function isKnown($option); * * @param string $option The name of the option. * - * @return bool Whether the option is required. + * @return bool Whether the option is required. */ public function isRequired($option); @@ -199,12 +202,12 @@ public function isRequired($option); * * @return array A list of options and their values. * - * @throws Exception\InvalidOptionsException If any of the passed options has not - * been defined or does not contain an - * allowed value. - * @throws Exception\MissingOptionsException If a required option is missing. - * @throws Exception\OptionDefinitionException If a cyclic dependency is detected - * between two lazy options. + * @throws InvalidOptionsException If any of the passed options has not + * been defined or does not contain an + * allowed value. + * @throws MissingOptionsException If a required option is missing. + * @throws OptionDefinitionException If a cyclic dependency is detected + * between two lazy options. */ public function resolve(array $options = array()); } diff --git a/src/Symfony/Component/OptionsResolver/README.md b/src/Symfony/Component/OptionsResolver/README.md index 65617f79a30a8..a00aec5f57c19 100644 --- a/src/Symfony/Component/OptionsResolver/README.md +++ b/src/Symfony/Component/OptionsResolver/README.md @@ -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 diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolver2Dot6Test.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolver2Dot6Test.php new file mode 100644 index 0000000000000..4a497e53b4ff6 --- /dev/null +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolver2Dot6Test.php @@ -0,0 +1,1450 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class OptionsResolver2Dot6Test extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + //////////////////////////////////////////////////////////////////////////// + // resolve() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testResolveFailsIfNonExistingOption() + { + $resolver = new OptionsResolver(); + + $resolver->resolve(array('foo' => 'bar')); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefault()/hasDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', 'bar')); + } + + public function testSetDefault() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', '20'); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultFromLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options->setDefault('default', 42); + }); + + $this->resolver->resolve(); + } + + public function testHasDefault() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', 42); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + public function testHasDefaultWithNullValue() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', null); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // overload() + //////////////////////////////////////////////////////////////////////////// + + public function testOverloadReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->overload('foo', 'bar')); + } + + public function testOverloadCallsSet() + { + $this->resolver->overload('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // lazy setDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetLazyReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', function (Options $options) {})); + } + + public function testSetLazyClosure() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testClosureWithoutTypeHintNotInvoked() + { + $closure = function ($options) { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testClosureWithoutParametersNotInvoked() + { + $closure = function () { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testAccessPreviousDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + \PHPUnit_Framework_Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testAccessPreviousLazyDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + \PHPUnit_Framework_Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + // defined by superclass + $this->resolver->setDefault('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testOverwrittenLazyOptionNotEvaluated() + { + $this->resolver->setDefault('foo', function (Options $options) { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testInvokeEachLazyOptionOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('lazy1', function (Options $options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(1, ++$calls); + + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + //////////////////////////////////////////////////////////////////////////// + // setRequired()/isRequired()/getRequiredOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testSetRequiredReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setRequired('foo')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetRequiredFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setRequired('bar'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfRequiredOptionMissing() + { + $this->resolver->setRequired('foo'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfRequiredOptionSet() + { + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfRequiredOptionPassed() + { + $this->resolver->setRequired('foo'); + + $this->resolver->resolve(array('foo' => 'bar')); + } + + public function testIsRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testRequiredIfSetBefore() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setRequired('foo'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testStillRequiredAfterSet() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterRemove() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterClear() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetRequiredOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('foo', 'bar'), $this->resolver->getRequiredOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // isMissing()/getMissingOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testIsMissingIfNotSet() + { + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingIfSet() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterRemove() + { + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterClear() + { + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetMissingOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('bar'), $this->resolver->getMissingOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefined()/isDefined()/getDefinedOptions() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefinedFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefined('bar'); + }); + + $this->resolver->resolve(); + } + + public function testDefinedOptionsNotIncludedInResolvedOptions() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetBefore() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetAfter() + { + $this->resolver->setDefined('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfPassedToResolve() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testLazyOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', function (Options $options) {}); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testRequiredOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testSetOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', 'bar'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testGetDefinedOptions() + { + $this->resolver->setDefined(array('foo', 'bar')); + $this->resolver->setDefault('baz', 'bam'); + $this->resolver->setRequired('boo'); + + $this->assertSame(array('foo', 'bar', 'baz', 'boo'), $this->resolver->getDefinedOptions()); + } + + public function testRemovedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + public function testClearedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedTypesFailsIfUnknownOption() + { + $this->resolver->setAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidType() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidTypeMultiple() + { + $this->resolver->setDefault('foo', true); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfNotInstanceOfClass() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfInstanceOfClass() + { + $this->resolver->setDefault('foo', new \stdClass()); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedTypesFailsIfUnknownOption() + { + $this->resolver->addAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedType() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testAddAllowedTypesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->resolve(); + } + + public function testAddAllowedTypesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', false); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedValuesFailsIfUnknownOption() + { + $this->resolver->setAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidValue() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidValueStrict() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', '42'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array('bar', 'baz')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValueMultiple() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + public function testResolveFailsIfClosureReturnsFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return false; + }); + + try { + $this->resolver->resolve(); + $this->fail('Should fail'); + } catch (InvalidOptionsException $e) { + } + + $this->assertSame(42, $passedValue); + } + + public function testResolveSucceedsIfClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return true; + }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + $this->assertSame('bar', $passedValue); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return false; }, + function () { return false; }, + )); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return true; }, + function () { return false; }, + )); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedValuesFailsIfUnknownOption() + { + $this->resolver->addAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValue() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllAddedClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return true; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return true; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setNormalizer() + //////////////////////////////////////////////////////////////////////////// + + public function testSetNormalizerReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + $this->assertSame($this->resolver, $this->resolver->setNormalizer('foo', function () {})); + } + + public function testSetNormalizerClosure() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetNormalizerFailsIfUnknownOption() + { + $this->resolver->setNormalizer('foo', function () {}); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetNormalizerFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setNormalizer('foo', function () {}); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testNormalizerReceivesSetOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->resolver->resolve()); + } + + public function testNormalizerReceivesPassedOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $resolved = $this->resolver->resolve(array('foo' => 'baz')); + + $this->assertEquals(array('foo' => 'normalized[baz]'), $resolved); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateTypeBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedTypes('foo', 'int'); + + $this->resolver->setNormalizer('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateValueBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedValues('foo', 'baz'); + + $this->resolver->setNormalizer('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + public function testNormalizerCanAccessOtherOptions() + { + $this->resolver->setDefault('default', 'bar'); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var \PHPUnit_Framework_TestCase $test */ + \PHPUnit_Framework_Assert::assertSame('bar', $options['default']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'default' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + public function testNormalizerCanAccessLazyOptions() + { + $this->resolver->setDefault('lazy', function (Options $options) { + return 'bar'; + }); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var \PHPUnit_Framework_TestCase $test */ + \PHPUnit_Framework_Assert::assertEquals('bar', $options['lazy']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'lazy' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizers() + { + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function (Options $options) { + $options['norm2']; + }); + + $this->resolver->setNormalizer('norm2', function (Options $options) { + $options['norm1']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizerAndLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options['norm']; + }); + + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + $options['lazy']; + }); + + $this->resolver->resolve(); + } + + public function testInvokeEachNormalizerOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function ($options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(1, ++$calls); + + $options['norm2']; + }); + $this->resolver->setNormalizer('norm2', function () use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + public function testNormalizerNotCalledForUnsetOptions() + { + $this->resolver->setDefined('norm'); + + $this->resolver->setNormalizer('norm', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefaults() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultsReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefaults(array('foo', 'bar'))); + } + + public function testSetDefaults() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', 'bar'); + + $this->resolver->setDefaults(array( + 'two' => '2', + 'three' => '3', + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + 'three' => '3', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefaults(array('two' => '2')); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // remove() + //////////////////////////////////////////////////////////////////////////// + + public function testRemoveReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame($this->resolver, $this->resolver->remove('foo')); + } + + public function testRemoveSingleOption() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->remove('foo'); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveMultipleOptions() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->setDefault('doo', 'dam'); + + $this->resolver->remove(array('foo', 'doo')); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->remove('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testRemoveNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array('baz', 'boo')); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfRemoveFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->remove('bar'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testRemoveUnknownOptionIgnored() + { + $this->resolver->remove('foo'); + } + + //////////////////////////////////////////////////////////////////////////// + // clear() + //////////////////////////////////////////////////////////////////////////// + + public function testClearReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->clear()); + } + + public function testClearRemovesAllOptions() + { + $this->resolver->setDefault('one', 1); + $this->resolver->setDefault('two', 2); + + $this->resolver->clear(); + + $this->assertEmpty($this->resolver->resolve()); + } + + public function testClearLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->clear(); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testClearNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'baz'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfClearFromLazyption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->clear(); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testClearOptionAndNormalizer() + { + $this->resolver->setDefault('foo1', 'bar'); + $this->resolver->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->resolver->setDefault('foo2', 'bar'); + $this->resolver->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->resolver->clear(); + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // ArrayAccess + //////////////////////////////////////////////////////////////////////////// + + public function testArrayAccess() + { + $this->resolver->setDefault('default1', 0); + $this->resolver->setDefault('default2', 1); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function (Options $options) { + return 'lazy'; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + \PHPUnit_Framework_Assert::assertTrue(isset($options['default1'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['default2'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['required'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['lazy1'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['lazy2'])); + \PHPUnit_Framework_Assert::assertFalse(isset($options['defined'])); + + \PHPUnit_Framework_Assert::assertSame(0, $options['default1']); + \PHPUnit_Framework_Assert::assertSame(42, $options['default2']); + \PHPUnit_Framework_Assert::assertSame('value', $options['required']); + \PHPUnit_Framework_Assert::assertSame('lazy', $options['lazy1']); + + // Obviously $options['lazy'] and $options['defined'] cannot be + // accessed + }); + + $this->resolver->resolve(array('default2' => 42, 'required' => 'value')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessGetFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + $this->resolver['default']; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessExistsFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + isset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessSetNotSupported() + { + $this->resolver['default'] = 0; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessUnsetNotSupported() + { + $this->resolver->setDefault('default', 0); + + unset($this->resolver['default']); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testFailIfGetNonExisting() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options['undefined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testFailIfGetDefinedButUnset() + { + $this->resolver->setDefined('defined'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['defined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependency() + { + $this->resolver->setDefault('lazy1', function (Options $options) { + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + $options['lazy1']; + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // Countable + //////////////////////////////////////////////////////////////////////////// + + public function testCount() + { + $this->resolver->setDefault('default', 0); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function () {}); + + $this->resolver->setDefault('lazy2', function (Options $options) { + \PHPUnit_Framework_Assert::assertCount(3, $options); + }); + } + + /** + * In resolve() we count the options that are actually set (which may be + * only a subset of the defined options). Outside of resolve(), it's not + * clear what is counted. + * + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testCountFailsOutsideResolve() + { + $this->resolver->setDefault('foo', 0); + $this->resolver->setRequired('bar'); + $this->resolver->setDefined('bar'); + $this->resolver->setDefault('lazy1', function () {}); + + count($this->resolver); + } +} diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 30f2f6eec9474..064487ff68556 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -14,6 +14,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\Options; +/** + * @deprecated Deprecated since Symfony 2.6, to be removed in Symfony 3.0. + */ class OptionsResolverTest extends \PHPUnit_Framework_TestCase { /** @@ -43,6 +46,23 @@ public function testResolve() ), $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( @@ -172,7 +192,7 @@ public function testResolveLazyReplaceDefaults() } /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException */ public function testResolveFailsIfNonExistingOption() { @@ -485,26 +505,6 @@ public function testResolveFailsIfOptionTypeNotAllowedAddTypes() )); } - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testSetRequiredFailsIfDefaultIsPassed() - { - $this->resolver->setRequired(array( - 'one' => '1', - )); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testSetOptionalFailsIfDefaultIsPassed() - { - $this->resolver->setOptional(array( - 'one' => '1', - )); - } - public function testFluidInterface() { $this->resolver->setDefaults(array('one' => '1')) @@ -568,20 +568,6 @@ public function testRequiredIfRequired() $this->assertTrue($this->resolver->isRequired('foo')); } - public function testNotRequiredIfRequiredAndDefaultValue() - { - $this->assertFalse($this->resolver->isRequired('foo')); - - $this->resolver->setRequired(array( - 'foo', - )); - $this->resolver->setDefaults(array( - 'foo' => 'bar', - )); - - $this->assertFalse($this->resolver->isRequired('foo')); - } - public function testNormalizersTransformFinalOptions() { $this->resolver->setDefaults(array( diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsTest.php index 5db0995685a00..5d569ebe02bd7 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsTest.php @@ -12,81 +12,21 @@ namespace Symfony\Component\OptionsResolver\Tests; use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @deprecated Deprecated since Symfony 2.6, to be removed in Symfony 3.0. + */ class OptionsTest extends \PHPUnit_Framework_TestCase { /** - * @var Options + * @var OptionsResolver */ private $options; protected function setUp() { - $this->options = new Options(); - } - - public function testArrayAccess() - { - $this->assertFalse(isset($this->options['foo'])); - $this->assertFalse(isset($this->options['bar'])); - - $this->options['foo'] = 0; - $this->options['bar'] = 1; - - $this->assertTrue(isset($this->options['foo'])); - $this->assertTrue(isset($this->options['bar'])); - - unset($this->options['bar']); - - $this->assertTrue(isset($this->options['foo'])); - $this->assertFalse(isset($this->options['bar'])); - $this->assertEquals(0, $this->options['foo']); - } - - public function testCountable() - { - $this->options->set('foo', 0); - $this->options->set('bar', 1); - - $this->assertCount(2, $this->options); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testGetNonExisting() - { - $this->options->get('foo'); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testSetNotSupportedAfterGet() - { - $this->options->set('foo', 'bar'); - $this->options->get('foo'); - $this->options->set('foo', 'baz'); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testRemoveNotSupportedAfterGet() - { - $this->options->set('foo', 'bar'); - $this->options->get('foo'); - $this->options->remove('foo'); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testSetNormalizerNotSupportedAfterGet() - { - $this->options->set('foo', 'bar'); - $this->options->get('foo'); - $this->options->setNormalizer('foo', function () {}); + $this->options = new OptionsResolver(); } public function testSetLazyOption() @@ -97,25 +37,7 @@ public function testSetLazyOption() return 'dynamic'; }); - $this->assertEquals('dynamic', $this->options->get('foo')); - } - - public function testSetDiscardsPreviousValue() - { - $test = $this; - - // defined by superclass - $this->options->set('foo', 'bar'); - - // defined by subclass - $this->options->set('foo', function (Options $options, $previousValue) use ($test) { - /* @var \PHPUnit_Framework_TestCase $test */ - $test->assertNull($previousValue); - - return 'dynamic'; - }); - - $this->assertEquals('dynamic', $this->options->get('foo')); + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); } public function testOverloadKeepsPreviousValue() @@ -133,7 +55,7 @@ public function testOverloadKeepsPreviousValue() return 'dynamic'; }); - $this->assertEquals('dynamic', $this->options->get('foo')); + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); } public function testPreviousValueIsEvaluatedIfLazy() @@ -153,7 +75,7 @@ public function testPreviousValueIsEvaluatedIfLazy() return 'dynamic'; }); - $this->assertEquals('dynamic', $this->options->get('foo')); + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); } public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() @@ -170,7 +92,7 @@ public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() return 'dynamic'; }); - $this->assertEquals('dynamic', $this->options->get('foo')); + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); } public function testLazyOptionCanAccessOtherOptions() @@ -186,8 +108,7 @@ public function testLazyOptionCanAccessOtherOptions() return 'dynamic'; }); - $this->assertEquals('bar', $this->options->get('foo')); - $this->assertEquals('dynamic', $this->options->get('bam')); + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); } public function testLazyOptionCanAccessOtherLazyOptions() @@ -205,8 +126,7 @@ public function testLazyOptionCanAccessOtherLazyOptions() return 'dynamic'; }); - $this->assertEquals('bar', $this->options->get('foo')); - $this->assertEquals('dynamic', $this->options->get('bam')); + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); } public function testNormalizer() @@ -217,7 +137,7 @@ public function testNormalizer() return 'normalized'; }); - $this->assertEquals('normalized', $this->options->get('foo')); + $this->assertEquals(array('foo' => 'normalized'), $this->options->resolve()); } public function testNormalizerReceivesUnnormalizedValue() @@ -228,7 +148,7 @@ public function testNormalizerReceivesUnnormalizedValue() return 'normalized['.$value.']'; }); - $this->assertEquals('normalized[bar]', $this->options->get('foo')); + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->options->resolve()); } public function testNormalizerCanAccessOtherOptions() @@ -245,8 +165,7 @@ public function testNormalizerCanAccessOtherOptions() return 'normalized'; }); - $this->assertEquals('bar', $this->options->get('foo')); - $this->assertEquals('normalized', $this->options->get('bam')); + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); } public function testNormalizerCanAccessOtherLazyOptions() @@ -265,8 +184,7 @@ public function testNormalizerCanAccessOtherLazyOptions() return 'normalized'; }); - $this->assertEquals('bar', $this->options->get('foo')); - $this->assertEquals('normalized', $this->options->get('bam')); + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); } /** @@ -282,7 +200,7 @@ public function testFailForCyclicDependencies() $options->get('foo'); }); - $this->options->get('foo'); + $this->options->resolve(); } /** @@ -301,7 +219,7 @@ public function testFailForCyclicDependenciesBetweenNormalizers() $options->get('foo'); }); - $this->options->get('foo'); + $this->options->resolve(); } /** @@ -318,50 +236,7 @@ public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption() $options->get('foo'); }); - $this->options->get('foo'); - } - - public function testAllInvokesEachLazyOptionOnlyOnce() - { - $test = $this; - $i = 1; - - $this->options->set('foo', function (Options $options) use ($test, &$i) { - $test->assertSame(1, $i); - ++$i; - - // Implicitly invoke lazy option for "bam" - $options->get('bam'); - }); - $this->options->set('bam', function (Options $options) use ($test, &$i) { - $test->assertSame(2, $i); - ++$i; - }); - - $this->options->all(); - } - - public function testAllInvokesEachNormalizerOnlyOnce() - { - $test = $this; - $i = 1; - - $this->options->set('foo', 'bar'); - $this->options->set('bam', 'baz'); - - $this->options->setNormalizer('foo', function (Options $options) use ($test, &$i) { - $test->assertSame(1, $i); - ++$i; - - // Implicitly invoke normalizer for "bam" - $options->get('bam'); - }); - $this->options->setNormalizer('bam', function (Options $options) use ($test, &$i) { - $test->assertSame(2, $i); - ++$i; - }); - - $this->options->all(); + $this->options->resolve(); } public function testReplaceClearsAndSets() @@ -378,7 +253,7 @@ public function testReplaceClearsAndSets() $this->assertEquals(array( 'two' => '2', 'three' => '3', - ), $this->options->all()); + ), $this->options->resolve()); } public function testClearRemovesAllOptions() @@ -388,45 +263,7 @@ public function testClearRemovesAllOptions() $this->options->clear(); - $this->assertEmpty($this->options->all()); - } - - /** - * @covers Symfony\Component\OptionsResolver\Options::replace - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testCannotReplaceAfterOptionWasRead() - { - $this->options->set('one', 1); - $this->options->all(); - - $this->options->replace(array( - 'two' => '2', - )); - } - - /** - * @covers Symfony\Component\OptionsResolver\Options::overload - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testCannotOverloadAfterOptionWasRead() - { - $this->options->set('one', 1); - $this->options->all(); - - $this->options->overload('one', 2); - } - - /** - * @covers Symfony\Component\OptionsResolver\Options::clear - * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - */ - public function testCannotClearAfterOptionWasRead() - { - $this->options->set('one', 1); - $this->options->all(); - - $this->options->clear(); + $this->assertEmpty($this->options->resolve()); } public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams() @@ -437,8 +274,8 @@ public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams( return 'test'; }); - $this->assertNotEquals('test', $this->options->get('foo')); - $this->assertTrue(is_callable($this->options->get('foo'))); + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); } public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint() @@ -449,24 +286,8 @@ public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint() return 'test'; }); - $this->assertNotEquals('test', $this->options->get('foo')); - $this->assertTrue(is_callable($this->options->get('foo'))); - } - - public function testOptionsIteration() - { - $this->options->set('foo', 'bar'); - $this->options->set('foo1', 'bar1'); - $expectedResult = array('foo' => 'bar', 'foo1' => 'bar1'); - - $this->assertEquals($expectedResult, iterator_to_array($this->options, true)); - } - - public function testHasWithNullValue() - { - $this->options->set('foo', null); - - $this->assertTrue($this->options->has('foo')); + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); } public function testRemoveOptionAndNormalizer() @@ -481,7 +302,7 @@ public function testRemoveOptionAndNormalizer() }); $this->options->remove('foo2'); - $this->assertEquals(array('foo1' => ''), $this->options->all()); + $this->assertEquals(array('foo1' => ''), $this->options->resolve()); } public function testReplaceOptionAndNormalizer() @@ -496,7 +317,7 @@ public function testReplaceOptionAndNormalizer() }); $this->options->replace(array('foo1' => 'new')); - $this->assertEquals(array('foo1' => 'new'), $this->options->all()); + $this->assertEquals(array('foo1' => 'new'), $this->options->resolve()); } public function testClearOptionAndNormalizer() @@ -511,18 +332,6 @@ public function testClearOptionAndNormalizer() }); $this->options->clear(); - $this->assertEmpty($this->options->all()); - } - - public function testNormalizerWithoutCorrespondingOption() - { - $test = $this; - - $this->options->setNormalizer('foo', function (Options $options, $previousValue) use ($test) { - $test->assertNull($previousValue); - - return ''; - }); - $this->assertEquals(array('foo' => ''), $this->options->all()); + $this->assertEmpty($this->options->resolve()); } }