diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md index 5f6d15b2c7ddc..136e032bfab46 100644 --- a/src/Symfony/Component/OptionsResolver/CHANGELOG.md +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.1.0 +----- + + * Added typed array support as allowed type + 2.6.0 ----- diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index bcaea9e4cbc0f..31ce65acb28f8 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -794,19 +794,8 @@ public function offsetGet($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; + $valid = $this->verifyAllowedType($type, $value); + if ($valid) { break; } } @@ -818,7 +807,7 @@ public function offsetGet($option) $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), - $this->formatTypeOf($value) + $this->formatTypeOf($value, $option) )); } } @@ -895,6 +884,42 @@ public function offsetGet($option) return $value; } + /** + * Verify value is of the allowed type. Recursive method to support + * typed array notation like ClassName[], or scalar arrays (int[]). + * + * @param string $type the required allowedType string + * @param mixed $value the value + * + * @return bool Whether the $value is of the allowed type + */ + private function verifyAllowedType($type, $value) + { + if (mb_substr($type, -2) === '[]') { + //allowed type is typed array + if (!is_array($value)) { + return false; + } + $subType = mb_substr($type, 0, -2); + foreach ($value as $v) { + //recursive call -> check subtype + if (!$this->verifyAllowedType($subType, $v)) { + return false; + } + } + //value was array, subtypes all matched -> allowed type OK + return true; + } + + $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type; + + if (function_exists($isFunction = 'is_'.$type)) { + return $isFunction($value); + } + + return ($value instanceof $type); + } + /** * Returns whether a resolved option with the given name exists. * @@ -963,15 +988,68 @@ public function count() * parameters should usually not be included in messages aimed at * non-technical people. * - * @param mixed $value The value to return the type of + * @param mixed $value The value to return the type of + * @param string $option The option that holds the value * * @return string The type of the value */ - private function formatTypeOf($value) + private function formatTypeOf($value, $option = null) { + if (is_array($value) && $option) { + foreach ($this->allowedTypes[$option] as $type) { + if (mb_substr($type, -2) === '[]') { + return $this->formatComplexTypeOf($value, $type); + } + } + } + return is_object($value) ? get_class($value) : gettype($value); } + /** + * Returns a string representation of the complex type of the value. + * + * This method should be called in formatTypeOf, if there is a complex allowed type + * for an array value defined to get a more explicit exception message + * + * @param array $value The value to return the complex type of + * @param string $type the expected type + * + * @return string the complex type of the value + */ + private function formatComplexTypeOf(array $value, $type) + { + $suffix = '[]'; + $type = mb_substr($type, 0, -2); + while (mb_substr($type, -2) === '[]') { + $value = array_shift($value); + if (!is_array($value)) { + //expected a nested array, but we've already hit a scalar + break; + } + $type = mb_substr($type, 0, -2); + $suffix .= '[]'; + } + if (is_array($value)) { + $subTypes = array(); + foreach ($value as $v) { + $v = $this->formatTypeOf($v); + if (!isset($subTypes[$v])) { + $subTypes[$v] = $v;//build unique map from the off + } + } + $vType = implode('|', $subTypes); + } else { + $vType = is_object($value) ? get_class($value) : gettype($value); + } + + return sprintf( + '%s%s', + $vType, + $suffix + ); + } + /** * Returns a string representation of the value. * diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 4f21bbaa3b824..9f6446da27cb1 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -498,6 +498,53 @@ public function testFailIfSetAllowedTypesFromLazyOption() $this->resolver->resolve(); } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "DateTime[]". + */ + public function testResolveFailsIfInvalidTypedArray() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[]'); + + $this->resolver->resolve(array('foo' => array(new \DateTime()))); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but is of type "double[][]". + */ + public function testResolveFailsWithCorrectLevelsButWrongScalar() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + + $this->resolver->resolve( + array( + 'foo' => array( + array(1.2), + ), + ) + ); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "integer|stdClass|array|DateTime[]". + */ + public function testResolveFailsIfTypedArrayContainsInvalidTypes() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[]'); + $values = range(1, 5); + $values[] = new \stdClass(); + $values[] = array(); + $values[] = new \DateTime(); + $values[] = 123; + + $this->resolver->resolve(array('foo' => $values)); + } + /** * @dataProvider provideInvalidTypes */ @@ -551,6 +598,46 @@ public function testResolveSucceedsIfValidTypeMultiple() $this->assertNotEmpty($this->resolver->resolve()); } + public function testResolveSucceedsIfTypedArray() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedTypes('foo', array('null', '\DateTime[]')); + + $data = array( + 'foo' => array( + new \DateTime(), + new \DateTime(), + ), + ); + $result = $this->resolver->resolve($data); + $this->assertEquals($data, $result); + } + + public function testResolveSucceedsNestedTypedArray() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + + $expect = array( + 'foo' => array( + range(1, 10), + ), + ); + $result = $this->resolver->resolve($expect); + $this->assertEquals($expect, $result); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfNotInstanceOfClass() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->resolver->resolve(); + } + public function testResolveSucceedsIfInstanceOfClass() { $this->resolver->setDefault('foo', new \stdClass());