diff options
Diffstat (limited to 'library/symfony/options-resolver')
18 files changed, 4367 insertions, 0 deletions
diff --git a/library/symfony/options-resolver/CHANGELOG.md b/library/symfony/options-resolver/CHANGELOG.md new file mode 100644 index 000000000..5f6d15b2c --- /dev/null +++ b/library/symfony/options-resolver/CHANGELOG.md @@ -0,0 +1,46 @@ +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 + * [BC BREAK] an UndefinedOptionsException is now thrown instead of an + InvalidOptionsException when non-existing options are passed diff --git a/library/symfony/options-resolver/Exception/AccessException.php b/library/symfony/options-resolver/Exception/AccessException.php new file mode 100644 index 000000000..c12b68064 --- /dev/null +++ b/library/symfony/options-resolver/Exception/AccessException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option outside of or write it inside of + * {@link \Symfony\Component\OptionsResolver\Options::resolve()}. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/ExceptionInterface.php b/library/symfony/options-resolver/Exception/ExceptionInterface.php new file mode 100644 index 000000000..b62bb51d4 --- /dev/null +++ b/library/symfony/options-resolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Marker interface for all exceptions thrown by the OptionsResolver component. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +interface ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/InvalidArgumentException.php b/library/symfony/options-resolver/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..6d421d68b --- /dev/null +++ b/library/symfony/options-resolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when an argument is invalid. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/InvalidOptionsException.php b/library/symfony/options-resolver/Exception/InvalidOptionsException.php new file mode 100644 index 000000000..6fd4f125f --- /dev/null +++ b/library/symfony/options-resolver/Exception/InvalidOptionsException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when 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 <bschussek@gmail.com> + */ +class InvalidOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/Exception/MissingOptionsException.php b/library/symfony/options-resolver/Exception/MissingOptionsException.php new file mode 100644 index 000000000..faa487f16 --- /dev/null +++ b/library/symfony/options-resolver/Exception/MissingOptionsException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when a required option is missing. + * + * Add the option to the passed options array. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class MissingOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/Exception/NoSuchOptionException.php b/library/symfony/options-resolver/Exception/NoSuchOptionException.php new file mode 100644 index 000000000..4c3280f4c --- /dev/null +++ b/library/symfony/options-resolver/Exception/NoSuchOptionException.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option that has no value set. + * + * When accessing optional options from within a lazy option or normalizer you should first + * check whether the optional option is set. You can do this with `isset($options['optional'])`. + * In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can + * occur when evaluating lazy options. + * + * @author Tobias Schultze <http://tobion.de> + */ +class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/OptionDefinitionException.php b/library/symfony/options-resolver/Exception/OptionDefinitionException.php new file mode 100644 index 000000000..e8e339d44 --- /dev/null +++ b/library/symfony/options-resolver/Exception/OptionDefinitionException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when two lazy options have a cyclic dependency. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class OptionDefinitionException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/UndefinedOptionsException.php b/library/symfony/options-resolver/Exception/UndefinedOptionsException.php new file mode 100644 index 000000000..6ca3fce47 --- /dev/null +++ b/library/symfony/options-resolver/Exception/UndefinedOptionsException.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * 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 <bschussek@gmail.com> + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/LICENSE b/library/symfony/options-resolver/LICENSE new file mode 100644 index 000000000..43028bc60 --- /dev/null +++ b/library/symfony/options-resolver/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/symfony/options-resolver/Options.php b/library/symfony/options-resolver/Options.php new file mode 100644 index 000000000..d444ec423 --- /dev/null +++ b/library/symfony/options-resolver/Options.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +/** + * Contains resolved option values. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Tobias Schultze <http://tobion.de> + */ +interface Options extends \ArrayAccess, \Countable +{ +} diff --git a/library/symfony/options-resolver/OptionsResolver.php b/library/symfony/options-resolver/OptionsResolver.php new file mode 100644 index 000000000..e0578af71 --- /dev/null +++ b/library/symfony/options-resolver/OptionsResolver.php @@ -0,0 +1,1218 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; + +/** + * Validates options and merges them with default values. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Tobias Schultze <http://tobion.de> + */ +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 array + */ + private $defaults = array(); + + /** + * The names of required options. + * + * @var array + */ + private $required = array(); + + /** + * The resolved option values. + * + * @var 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(); + + /** + * A list of closures for evaluating lazy options. + * + * @var array + */ + private $lazy = array(); + + /** + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + * + * @var array + */ + private $calling = array(); + + /** + * 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; + + private static $typeAliases = array( + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float', + ); + + /** + * 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 OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefault($option, $value) + { + // 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 from a lazy option or normalizer.'); + } + + // 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; + } + + /** + * Sets a list of default values. + * + * @param array $defaults The default values to set + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefaults(array $defaults) + { + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); + } + + return $this; + } + + /** + * 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 hasDefault($option) + { + return array_key_exists($option, $this->defaults); + } + + /** + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setRequired($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + $this->required[$option] = true; + } + + return $this; + } + + /** + * 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 isRequired($option) + { + return isset($this->required[$option]); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * Returns the names of all options missing a default value. + * + * @return string[] The names of the missing options + * + * @see isMissing() + */ + public function getMissingOptions() + { + 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 OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefined($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + } + + return $this; + } + + /** + * 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 isDefined($option) + { + return isset($this->defined[$option]); + } + + /** + * 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); + } + + /** + * 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 OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setNormalizer($option, \Closure $normalizer) + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined 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; + } + + /** + * Sets the normalizers for an array of options. + * + * @param array $normalizers An array of closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + * + * @see setNormalizer() + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function setNormalizers(array $normalizers) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use setNormalizer() instead.', E_USER_DEPRECATED); + + foreach ($normalizers as $option => $normalizer) { + $this->setNormalizer($option, $normalizer); + } + + return $this; + } + + /** + * 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 OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedValues($option, $allowedValues = null) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + 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. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues); + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * 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 OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedValues($option, $allowedValues = null) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + 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. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!is_array($allowedValues)) { + $allowedValues = array($allowedValues); + } + + if (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed types for an option. + * + * Any type for which a corresponding is_<type>() 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 OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedTypes($option, $allowedTypes = null) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionTypes) { + $this->setAllowedTypes($optionName, $optionTypes); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedTypes[$option] = (array) $allowedTypes; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed types for an option. + * + * The types are merged with the allowed types defined previously. + * + * Any type for which a corresponding is_<type>() 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 OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedTypes($option, $allowedTypes = null) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionTypes) { + $this->addAllowedTypes($optionName, $optionTypes); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined 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; + } + + /** + * Removes the option with the given name. + * + * Undefined options are ignored. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function remove($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function clear() + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); + } + + $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 there is a cyclic dependency between + * lazy options and/or normalizers + * @throws NoSuchOptionException If a lazy option reads an unavailable option + * @throws AccessException If called from a lazy option or normalizer + */ + public function resolve(array $options = array()) + { + if ($this->locked) { + throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); + } + + // 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.').' Defined 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); + + throw new MissingOptionsException(sprintf( + count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', + 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; + } + + /** + * Returns the resolved value of an option. + * + * @param string $option The option name + * + * @return mixed The option value + * + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws NoSuchOptionException If the option is not set + * @throws InvalidOptionsException If the option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + */ + public function offsetGet($option) + { + 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)) { + if (!isset($this->defined[$option])) { + throw new NoSuchOptionException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + throw new NoSuchOptionException(sprintf( + 'The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', + $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; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } catch (\Exception $e) { + unset($this->calling[$option]); + throw $e; + } + 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) { + $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $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('" or "', $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; + } + + $printableAllowedValues[] = $allowedValue; + } + + 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; + try { + $value = $normalizer($this, $value); + } catch (\Exception $e) { + unset($this->calling[$option]); + throw $e; + } + unset($this->calling[$option]); + // END + } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; + } + + /** + * Returns whether a resolved option with the given name exists. + * + * @param string $option The option name + * + * @return bool Whether the option is set + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \ArrayAccess::offsetExists() + */ + public function offsetExists($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + return array_key_exists($option, $this->defaults); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet($option, $value) + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset($option) + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * Returns the number of set options. + * + * This may be only a subset of the defined options. + * + * @return int Number of options + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \Countable::count() + */ + public function count() + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } + + return count($this->defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function set($option, $value) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefaults() method instead.', E_USER_DEPRECATED); + + return $this->setDefault($option, $value); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function replace(array $defaults) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED); + + $this->clear(); + + return $this->setDefaults($defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function overload($option, $value) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefault() method instead.', E_USER_DEPRECATED); + + return $this->setDefault($option, $value); + } + + /** + * Alias of {@link offsetGet()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function get($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED); + + return $this->offsetGet($option); + } + + /** + * Alias of {@link offsetExists()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function has($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED); + + return $this->offsetExists($option); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function replaceDefaults(array $defaultValues) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED); + + $this->clear(); + + return $this->setDefaults($defaultValues); + } + + /** + * Alias of {@link setDefined()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function setOptional(array $optionNames) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefined() method instead.', E_USER_DEPRECATED); + + return $this->setDefined($optionNames); + } + + /** + * Alias of {@link isDefined()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function isKnown($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the isDefined() method instead.', E_USER_DEPRECATED); + + 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/library/symfony/options-resolver/OptionsResolverInterface.php b/library/symfony/options-resolver/OptionsResolverInterface.php new file mode 100644 index 000000000..aebc8df22 --- /dev/null +++ b/library/symfony/options-resolver/OptionsResolverInterface.php @@ -0,0 +1,212 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + * + * @deprecated since version 2.6, to be removed in 3.0. Use {@link OptionsResolver} instead. + */ +interface OptionsResolverInterface +{ + /** + * Sets default option values. + * + * The options can either be values of any types or closures that + * evaluate the option value lazily. These closures must have one + * of the following signatures: + * + * <code> + * function (Options $options) + * function (Options $options, $value) + * </code> + * + * The second parameter passed to the closure is the previously + * set default value, in case you are overwriting an existing + * default value. + * + * The closures should return the lazily created option value. + * + * @param array $defaultValues A list of option names as keys and default + * values or closures as values. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setDefaults(array $defaultValues); + + /** + * Replaces default option values. + * + * Old defaults are erased, which means that closures passed here cannot + * access the previous default value. This may be useful to improve + * performance if the previous default value is calculated by an expensive + * closure. + * + * @param array $defaultValues A list of option names as keys and default + * values or closures as values. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function replaceDefaults(array $defaultValues); + + /** + * Sets optional options. + * + * This method declares valid option names without setting default values for them. + * If these options are not passed to {@link resolve()} and no default has been set + * for them, they will be missing in the final options array. This can be helpful + * if you want to determine whether an option has been set or not because otherwise + * {@link resolve()} would trigger an exception for unknown options. + * + * @param array $optionNames A list of option names. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setOptional(array $optionNames); + + /** + * Sets required options. + * + * If these options are not passed to {@link resolve()} and no default has been set for + * them, an exception will be thrown. + * + * @param array $optionNames A list of option names. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setRequired($optionNames); + + /** + * Sets allowed values for a list of options. + * + * @param array $allowedValues A list of option names as keys and arrays + * with values acceptable for that option as + * values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined + * (see {@link isKnown()}) for which + * an allowed value is set. + */ + public function setAllowedValues($allowedValues); + + /** + * Adds allowed values for a list of options. + * + * The values are merged with the allowed values defined previously. + * + * @param array $allowedValues A list of option names as keys and arrays + * with values acceptable for that option as + * values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined + * (see {@link isKnown()}) for which + * an allowed value is set. + */ + public function addAllowedValues($allowedValues); + + /** + * Sets allowed types for a list of options. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function setAllowedTypes($allowedTypes); + + /** + * Adds allowed types for a list of options. + * + * The types are merged with the allowed types defined previously. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function addAllowedTypes($allowedTypes); + + /** + * Sets normalizers that are applied on resolved options. + * + * The normalizers should be closures with the following signature: + * + * <code> + * function (Options $options, $value) + * </code> + * + * The second parameter passed to the closure is the value of + * the option. + * + * The closure should return the normalized value. + * + * @param array $normalizers An array of closures. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setNormalizers(array $normalizers); + + /** + * Returns whether an option is known. + * + * An option is known if it has been passed to either {@link setDefaults()}, + * {@link setRequired()} or {@link setOptional()} before. + * + * @param string $option The name of the option. + * + * @return bool Whether the option is known. + */ + public function isKnown($option); + + /** + * Returns whether an option is required. + * + * An option is required if it has been passed to {@link setRequired()}, + * but not to {@link setDefaults()}. That is, the option has been declared + * as required and no default value has been set. + * + * @param string $option The name of the option. + * + * @return bool Whether the option is required. + */ + public function isRequired($option); + + /** + * Returns the combination of the default and the passed options. + * + * @param array $options The custom option values. + * + * @return array A list of options and their values. + * + * @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/library/symfony/options-resolver/README.md b/library/symfony/options-resolver/README.md new file mode 100644 index 000000000..cd7a7405d --- /dev/null +++ b/library/symfony/options-resolver/README.md @@ -0,0 +1,20 @@ +OptionsResolver Component +========================= + +This component processes and validates option arrays. + +Documentation +------------- + +The documentation for the component can be found [online] [1]. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/OptionsResolver/ + $ composer install + $ phpunit + +[1]: https://symfony.com/doc/current/components/options_resolver.html diff --git a/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php b/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php new file mode 100644 index 000000000..ee89f5279 --- /dev/null +++ b/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php @@ -0,0 +1,733 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\Options; + +/** + * @group legacy + */ +class LegacyOptionsResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + public function testResolve() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $options = array( + 'two' => '20', + ); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $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( + 'one' => '1', + 'two' => function (Options $options) { + return '20'; + }, + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve(array())); + } + + public function testTypeAliasesForAllowedTypes() + { + $this->resolver->setDefaults(array( + 'force' => false, + )); + + $this->resolver->setAllowedTypes(array( + 'force' => 'boolean', + )); + + $this->resolver->resolve(array( + 'force' => true, + )); + } + + public function testResolveLazyDependencyOnOptional() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => function (Options $options) { + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnMissingOptionalWithoutDefault() + { + $test = $this; + + $this->resolver->setOptional(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertFalse(isset($options['one'])); + + return '2'; + }, + )); + + $options = array(); + + $this->assertEquals(array( + 'two' => '2', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnOptionalWithoutDefault() + { + $test = $this; + + $this->resolver->setOptional(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertTrue(isset($options['one'])); + + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnRequired() + { + $this->resolver->setRequired(array( + 'one', + )); + $this->resolver->setDefaults(array( + 'two' => function (Options $options) { + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyReplaceDefaults() + { + $test = $this; + + $this->resolver->setDefaults(array( + 'one' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->fail('Previous closure should not be executed'); + }, + )); + + $this->resolver->replaceDefaults(array( + 'one' => function (Options $options, $previousValue) { + return '1'; + }, + )); + + $this->assertEquals(array( + 'one' => '1', + ), $this->resolver->resolve(array())); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "one", "three", "two". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setRequired(array( + 'two', + )); + + $this->resolver->setOptional(array( + 'three', + )); + + $this->resolver->resolve(array( + 'foo' => 'bar', + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfMissingRequiredOption() + { + $this->resolver->setRequired(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => '2', + )); + + $this->resolver->resolve(array( + 'two' => '20', + )); + } + + public function testResolveSucceedsIfOptionValueAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + )); + + $options = array( + 'one' => 'one', + ); + + $this->assertEquals(array( + 'one' => 'one', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionValueAllowed2() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedValues(array( + 'one' => '1', + 'two' => '2', + )); + $this->resolver->addAllowedValues(array( + 'one' => 'one', + 'two' => 'two', + )); + + $options = array( + 'one' => '1', + 'two' => 'two', + ); + + $this->assertEquals(array( + 'one' => '1', + 'two' => 'two', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionalWithAllowedValuesNotSet() + { + $this->resolver->setRequired(array( + 'one', + )); + + $this->resolver->setOptional(array( + 'two', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + 'two' => array('2', 'two'), + )); + + $options = array( + 'one' => '1', + ); + + $this->assertEquals(array( + 'one' => '1', + ), $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionValueNotAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + )); + + $this->resolver->resolve(array( + 'one' => '2', + )); + } + + public function testResolveSucceedsIfOptionTypeAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + )); + + $options = array( + 'one' => 'one', + ); + + $this->assertEquals(array( + 'one' => 'one', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassArray() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => array('string', 'bool'), + )); + + $options = array( + 'one' => true, + ); + + $this->assertEquals(array( + 'one' => true, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassObject() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'object', + )); + + $object = new \stdClass(); + $options = array( + 'one' => $object, + ); + + $this->assertEquals(array( + 'one' => $object, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassClass() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => '\stdClass', + )); + + $object = new \stdClass(); + $options = array( + 'one' => $object, + ); + + $this->assertEquals(array( + 'one' => $object, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedAddTypes() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'bool', + )); + $this->resolver->addAllowedTypes(array( + 'one' => 'float', + 'two' => 'integer', + )); + + $options = array( + 'one' => 1.23, + 'two' => false, + ); + + $this->assertEquals(array( + 'one' => 1.23, + 'two' => false, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionalWithTypeAndWithoutValue() + { + $this->resolver->setOptional(array( + 'one', + 'two', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'int', + )); + + $options = array( + 'two' => 1, + ); + + $this->assertEquals(array( + 'two' => 1, + ), $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => array('string', 'bool'), + )); + + $this->resolver->resolve(array( + 'one' => 1.23, + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowedMultipleOptions() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'bool', + )); + + $this->resolver->resolve(array( + 'one' => 'foo', + 'two' => 1.23, + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowedAddTypes() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + )); + $this->resolver->addAllowedTypes(array( + 'one' => 'bool', + )); + + $this->resolver->resolve(array( + 'one' => 1.23, + )); + } + + public function testFluidInterface() + { + $this->resolver->setDefaults(array('one' => '1')) + ->replaceDefaults(array('one' => '2')) + ->setAllowedValues(array('one' => array('1', '2'))) + ->addAllowedValues(array('one' => array('3'))) + ->setRequired(array('two')) + ->setOptional(array('three')); + + $options = array( + 'two' => '2', + ); + + $this->assertEquals(array( + 'one' => '2', + 'two' => '2', + ), $this->resolver->resolve($options)); + } + + public function testKnownIfDefaultWasSet() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testKnownIfRequired() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testKnownIfOptional() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setOptional(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testRequiredIfRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testNormalizersTransformFinalOptions() + { + $this->resolver->setDefaults(array( + 'foo' => 'bar', + 'bam' => 'baz', + )); + $this->resolver->setNormalizers(array( + 'foo' => function (Options $options, $value) { + return $options['bam'].'['.$value.']'; + }, + )); + + $expected = array( + 'foo' => 'baz[bar]', + 'bam' => 'baz', + ); + + $this->assertEquals($expected, $this->resolver->resolve(array())); + + $expected = array( + 'foo' => 'boo[custom]', + 'bam' => 'boo', + ); + + $this->assertEquals($expected, $this->resolver->resolve(array( + 'foo' => 'custom', + 'bam' => 'boo', + ))); + } + + public function testResolveWithoutOptionSucceedsIfRequiredAndDefaultValue() + { + $this->resolver->setRequired(array( + 'foo', + )); + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + + $this->assertEquals(array( + 'foo' => 'bar', + ), $this->resolver->resolve(array())); + } + + public function testResolveWithoutOptionSucceedsIfDefaultValueAndRequired() + { + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertEquals(array( + 'foo' => 'bar', + ), $this->resolver->resolve(array())); + } + + public function testResolveSucceedsIfOptionRequiredAndValueAllowed() + { + $this->resolver->setRequired(array( + 'one', 'two', + )); + $this->resolver->setAllowedValues(array( + 'two' => array('2'), + )); + + $options = array( + 'one' => '1', + 'two' => '2', + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfValueAllowedCallbackReturnsTrue() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return true; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfValueAllowedCallbackReturnsFalse() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return false; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + public function testClone() + { + $this->resolver->setDefaults(array('one' => '1')); + + $clone = clone $this->resolver; + + // Changes after cloning don't affect each other + $this->resolver->setDefaults(array('two' => '2')); + $clone->setDefaults(array('three' => '3')); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + ), $this->resolver->resolve()); + + $this->assertEquals(array( + 'one' => '1', + 'three' => '3', + ), $clone->resolve()); + } + + 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()); + } +} diff --git a/library/symfony/options-resolver/Tests/LegacyOptionsTest.php b/library/symfony/options-resolver/Tests/LegacyOptionsTest.php new file mode 100644 index 000000000..b65a09002 --- /dev/null +++ b/library/symfony/options-resolver/Tests/LegacyOptionsTest.php @@ -0,0 +1,337 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @group legacy + */ +class LegacyOptionsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $options; + + protected function setUp() + { + $this->options = new OptionsResolver(); + } + + public function testSetLazyOption() + { + $test = $this; + + $this->options->set('foo', function (Options $options) use ($test) { + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testOverloadKeepsPreviousValue() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', 'bar'); + + // defined by subclass + $this->options->overload('foo', function (Options $options, $previousValue) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $previousValue); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testPreviousValueIsEvaluatedIfLazy() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->options->overload('foo', function (Options $options, $previousValue) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $previousValue); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', function (Options $options) use ($test) { + $test->fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->options->overload('foo', function (Options $options) use ($test) { + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testLazyOptionCanAccessOtherOptions() + { + $test = $this; + + $this->options->set('foo', 'bar'); + + $this->options->set('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); + } + + public function testLazyOptionCanAccessOtherLazyOptions() + { + $test = $this; + + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + + $this->options->set('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); + } + + public function testNormalizer() + { + $this->options->set('foo', 'bar'); + + $this->options->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->options->resolve()); + } + + public function testNormalizerReceivesUnnormalizedValue() + { + $this->options->set('foo', 'bar'); + + $this->options->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->options->resolve()); + } + + public function testNormalizerCanAccessOtherOptions() + { + $test = $this; + + $this->options->set('foo', 'bar'); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); + } + + public function testNormalizerCanAccessOtherLazyOptions() + { + $test = $this; + + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependencies() + { + $this->options->set('foo', function (Options $options) { + $options->get('bam'); + }); + + $this->options->set('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependenciesBetweenNormalizers() + { + $this->options->set('foo', 'bar'); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('foo', function (Options $options) { + $options->get('bam'); + }); + + $this->options->setNormalizer('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption() + { + $this->options->set('foo', function (Options $options) { + $options->get('bam'); + }); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + public function testReplaceClearsAndSets() + { + $this->options->set('one', '1'); + + $this->options->replace(array( + 'two' => '2', + 'three' => function (Options $options) { + return '2' === $options['two'] ? '3' : 'foo'; + }, + )); + + $this->assertEquals(array( + 'two' => '2', + 'three' => '3', + ), $this->options->resolve()); + } + + public function testClearRemovesAllOptions() + { + $this->options->set('one', 1); + $this->options->set('two', 2); + + $this->options->clear(); + + $this->assertEmpty($this->options->resolve()); + } + + public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams() + { + $this->options->set('foo', 'bar'); + + $this->options->overload('foo', function () { + return 'test'; + }); + + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); + } + + public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint() + { + $this->options->set('foo', 'bar'); + + $this->options->overload('foo', function ($object) { + return 'test'; + }); + + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); + } + + public function testRemoveOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->remove('foo2'); + $this->assertEquals(array('foo1' => ''), $this->options->resolve()); + } + + public function testReplaceOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->replace(array('foo1' => 'new')); + $this->assertEquals(array('foo1' => 'new'), $this->options->resolve()); + } + + public function testClearOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->clear(); + $this->assertEmpty($this->options->resolve()); + } +} diff --git a/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php b/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php new file mode 100644 index 000000000..9158c5ba0 --- /dev/null +++ b/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php @@ -0,0 +1,1550 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\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 + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('foo' => 'bar')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The options "baz", "foo", "ping" do not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfMultipleNonExistingOptions() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('ping' => 'pong', 'foo' => 'bar', 'baz' => 'bam')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testResolveFailsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->resolve(array()); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // 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')); + } + + //////////////////////////////////////////////////////////////////////////// + // 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->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfRequiredOptionPassed() + { + $this->resolver->setRequired('foo'); + + $this->assertNotEmpty($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(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testResolveFailsIfInvalidType($actualType, $allowedType, $exceptionMessage) + { + $this->resolver->setDefined('option'); + $this->resolver->setAllowedTypes('option', $allowedType); + $this->setExpectedException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException', $exceptionMessage); + $this->resolver->resolve(array('option' => $actualType)); + } + + public function provideInvalidTypes() + { + return array( + array(true, 'string', 'The option "option" with value true is expected to be of type "string", but is of type "boolean".'), + array(false, 'string', 'The option "option" with value false is expected to be of type "string", but is of type "boolean".'), + array(fopen(__FILE__, 'r'), 'string', 'The option "option" with value resource is expected to be of type "string", but is of type "resource".'), + array(array(), 'string', 'The option "option" with value array is expected to be of type "string", but is of type "array".'), + array(new OptionsResolver(), 'string', 'The option "option" with value Symfony\Component\OptionsResolver\OptionsResolver is expected to be of type "string", but is of type "Symfony\Component\OptionsResolver\OptionsResolver".'), + array(42, 'string', 'The option "option" with value 42 is expected to be of type "string", but is of type "integer".'), + array(null, 'string', 'The option "option" with value null is expected to be of type "string", but is of type "NULL".'), + array('bar', '\stdClass', 'The option "option" with value "bar" is expected to be of type "\stdClass", but is of type "string".'), + ); + } + + public function testResolveSucceedsIfValidType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is expected to be of type "string" or "bool", but is of type "integer". + */ + 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->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfInstanceOfClass() + { + $this->resolver->setDefault('foo', new \stdClass()); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->assertNotEmpty($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->assertNotEmpty($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->assertNotEmpty($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->assertNotEmpty($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->assertNotEmpty($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 + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValue() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(array('foo' => 42)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value null is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $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()); + } + + public function testResolveSucceedsIfValidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar", false, null. + */ + public function testResolveFailsIfInvalidValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array('bar', false, null)); + + $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()); + } + + public function testResolveSucceedsIfValidAddedValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->addAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $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 testCatchedExceptionFromNormalizerDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefaults(array('catcher' => null, 'thrower' => null)); + + $this->resolver->setNormalizer('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch(\Exception $e) { + return false; + } + }); + + $this->resolver->setNormalizer('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->resolver->resolve(); + } + + public function testCatchedExceptionFromLazyDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefault('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch(\Exception $e) { + return false; + } + }); + + $this->resolver->setDefault('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $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->assertEmpty($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->assertNotNull($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 \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The option "undefined" does not exist. Defined options are: "foo", "lazy". + */ + public function testFailIfGetNonExisting() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['undefined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The optional option "defined" has no value set. You should make sure it is set with "isset" before reading it. + */ + 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(4, $options); + }); + + $this->assertCount(4, $this->resolver->resolve(array('required' => 'value'))); + } + + /** + * 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/library/symfony/options-resolver/phpunit.xml.dist b/library/symfony/options-resolver/phpunit.xml.dist new file mode 100644 index 000000000..abf84614b --- /dev/null +++ b/library/symfony/options-resolver/phpunit.xml.dist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + backupGlobals="false" + colors="true" + bootstrap="vendor/autoload.php" +> + <php> + <ini name="error_reporting" value="-1" /> + </php> + + <testsuites> + <testsuite name="Symfony OptionsResolver Component Test Suite"> + <directory>./Tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./</directory> + <exclude> + <directory>./Resources</directory> + <directory>./Tests</directory> + <directory>./vendor</directory> + </exclude> + </whitelist> + </filter> +</phpunit> |