From 97de0041a1994a5242f584c28a794b676a06b07b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 23 May 2012 16:46:54 +0200 Subject: [PATCH] [OptionsResolver] Added option type validation capabilities --- .../Component/OptionsResolver/Options.php | 4 +- .../OptionsResolver/OptionsResolver.php | 90 +++++++++- .../Tests/OptionsResolverTest.php | 165 ++++++++++++++++++ 3 files changed, 256 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/OptionsResolver/Options.php b/src/Symfony/Component/OptionsResolver/Options.php index 079e0a4050..21bca78676 100644 --- a/src/Symfony/Component/OptionsResolver/Options.php +++ b/src/Symfony/Component/OptionsResolver/Options.php @@ -120,10 +120,10 @@ class Options implements \ArrayAccess, \Iterator, \Countable * Passed closures should have the following signature: * * - * function (Options $options, $previousValue) + * function (Options $options, $value) * * - * The second parameter passed to the closure is the previous default + * The second parameter passed to the closure is the current default * value of the option. * * @param string $option The option name. diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 4605822ad3..5d8dc937dc 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -47,6 +47,12 @@ class OptionsResolver */ private $allowedValues = array(); + /** + * A list of accepted types for each option. + * @var array + */ + private $allowedTypes = array(); + /** * A list of filters transforming each resolved options. * @var array @@ -222,6 +228,48 @@ class OptionsResolver return $this; } + /** + * Sets allowed types for a list of options. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolver The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function setAllowedTypes(array $allowedTypes) + { + $this->validateOptionNames(array_keys($allowedTypes)); + + $this->allowedTypes = array_replace($this->allowedTypes, $allowedTypes); + + return $this; + } + + /** + * Adds allowed types for a list of options. + * + * The types are merged with the allowed types defined previously. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolver The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function addAllowedTypes(array $allowedTypes) + { + $this->validateOptionNames(array_keys($allowedTypes)); + + $this->allowedTypes = array_merge_recursive($this->allowedTypes, $allowedTypes); + + return $this; + } + /** * Sets filters that are applied on resolved options. * @@ -312,8 +360,8 @@ class OptionsResolver // Resolve options $resolvedOptions = $combinedOptions->all(); - // Validate against allowed values $this->validateOptionValues($resolvedOptions); + $this->validateOptionTypes($resolvedOptions); return $resolvedOptions; } @@ -381,4 +429,44 @@ class OptionsResolver } } } + + /** + * Validates that the given options match the allowed types and + * throws an exception otherwise. + * + * @param array $options A list of options. + * + * @throws InvalidOptionsException If any of the types does not match the + * allowed types of the option. + */ + private function validateOptionTypes(array $options) + { + foreach ($this->allowedTypes as $option => $allowedTypes) { + $value = $options[$option]; + $allowedTypes = (array) $allowedTypes; + + foreach ($allowedTypes as $type) { + $isFunction = 'is_' . $type; + + if (function_exists($isFunction) && $isFunction($value)) { + continue 2; + } elseif ($value instanceof $type) { + continue 2; + } + } + + $printableValue = is_object($value) + ? get_class($value) + : (is_array($value) + ? 'Array' + : (string) $value); + + throw new InvalidOptionsException(sprintf( + 'The option "%s" with value "%s" is expected to be of type "%s"', + $option, + $printableValue, + implode('", "', $allowedTypes) + )); + } + } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 76d1611a8b..aaa800e373 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -276,6 +276,171 @@ class OptionsResolverTest extends \PHPUnit_Framework_TestCase )); } + 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)); + } + + /** + * @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, + )); + } + /** * @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException */