From 6d4812e995dc990a06727bd8fd3f1ba6a178dd39 Mon Sep 17 00:00:00 2001 From: Oleg Golovakhin Date: Wed, 30 May 2018 23:05:54 +0300 Subject: [PATCH] [OptionResolver] resolve arrays --- .../OptionsResolver/OptionsResolver.php | 106 ++++++++---- .../Tests/OptionsResolverTest.php | 153 +++++++++++++++++- 2 files changed, 223 insertions(+), 36 deletions(-) diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 95a492de94..a58f45c0a6 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -433,7 +433,7 @@ class OptionsResolver implements Options )); } - $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues); + $this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : array($allowedValues); // Make sure the option is processed unset($this->resolved[$option]); @@ -785,14 +785,13 @@ class OptionsResolver implements Options } 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]), - implode('|', array_keys($invalidTypes)) - )); + $keys = array_keys($invalidTypes); + + if (1 === \count($keys) && '[]' === substr($keys[0], -2)) { + throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0])); + } + + 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]), implode('|', array_keys($invalidTypes)))); } } @@ -877,23 +876,8 @@ class OptionsResolver implements Options */ private function verifyTypes($type, $value, array &$invalidTypes) { - if ('[]' === substr($type, -2) && is_array($value)) { - $originalType = $type; - $type = substr($type, 0, -2); - $invalidValues = array_filter( // Filter out valid values, keeping invalid values in the resulting array - $value, - function ($value) use ($type) { - return !self::isValueValidType($type, $value); - } - ); - - if (!$invalidValues) { - return true; - } - - $invalidTypes[$this->formatTypeOf($value, $originalType)] = true; - - return false; + if (\is_array($value) && '[]' === substr($type, -2)) { + return $this->verifyArrayType($type, $value, $invalidTypes); } if (self::isValueValidType($type, $value)) { @@ -907,6 +891,46 @@ class OptionsResolver implements Options return false; } + /** + * @return bool + */ + private function verifyArrayType($type, array $value, array &$invalidTypes, $level = 0) + { + $type = substr($type, 0, -2); + + $suffix = '[]'; + while (\strlen($suffix) <= $level * 2) { + $suffix .= '[]'; + } + + if ('[]' === substr($type, -2)) { + $success = true; + foreach ($value as $item) { + if (!\is_array($item)) { + $invalidTypes[$this->formatTypeOf($item, null).$suffix] = true; + + return false; + } + + if (!$this->verifyArrayType($type, $item, $invalidTypes, $level + 1)) { + $success = false; + } + } + + return $success; + } + + foreach ($value as $item) { + if (!self::isValueValidType($type, $item)) { + $invalidTypes[$this->formatTypeOf($item, $type).$suffix] = $value; + + return false; + } + } + + return true; + } + /** * Returns whether a resolved option with the given name exists. * @@ -990,13 +1014,13 @@ class OptionsResolver implements Options while ('[]' === substr($type, -2)) { $type = substr($type, 0, -2); $value = array_shift($value); - if (!is_array($value)) { + if (!\is_array($value)) { break; } $suffix .= '[]'; } - if (is_array($value)) { + if (\is_array($value)) { $subTypes = array(); foreach ($value as $val) { $subTypes[$this->formatTypeOf($val, null)] = true; @@ -1006,7 +1030,7 @@ class OptionsResolver implements Options } } - return (is_object($value) ? get_class($value) : gettype($value)).$suffix; + return (\is_object($value) ? get_class($value) : gettype($value)).$suffix; } /** @@ -1022,19 +1046,19 @@ class OptionsResolver implements Options */ private function formatValue($value) { - if (is_object($value)) { + if (\is_object($value)) { return get_class($value); } - if (is_array($value)) { + if (\is_array($value)) { return 'array'; } - if (is_string($value)) { + if (\is_string($value)) { return '"'.$value.'"'; } - if (is_resource($value)) { + if (\is_resource($value)) { return 'resource'; } @@ -1078,4 +1102,20 @@ class OptionsResolver implements Options { return (function_exists($isFunction = 'is_'.$type) && $isFunction($value)) || $value instanceof $type; } + + /** + * @return array + */ + private function getInvalidValues(array $arrayValues, $type) + { + $invalidValues = array(); + + foreach ($arrayValues as $key => $value) { + if (!self::isValueValidType($type, $value)) { + $invalidValues[$key] = $value; + } + } + + return $invalidValues; + } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 440af8b578..034f42a8f6 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -511,7 +511,7 @@ class OptionsResolverTest extends TestCase /** * @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[]". + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but one of the elements is of type "DateTime[]". */ public function testResolveFailsIfInvalidTypedArray() { @@ -535,7 +535,7 @@ class OptionsResolverTest extends TestCase /** * @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[]". + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but one of the elements is of type "stdClass[]". */ public function testResolveFailsIfTypedArrayContainsInvalidTypes() { @@ -552,7 +552,7 @@ class OptionsResolverTest extends TestCase /** * @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[][]". + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "double[][]". */ public function testResolveFailsWithCorrectLevelsButWrongScalar() { @@ -1650,4 +1650,151 @@ class OptionsResolverTest extends TestCase count($this->resolver); } + + public function testNestedArrays() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + + $this->assertEquals(array( + 'foo' => array( + array( + 1, 2, + ), + ), + ), $this->resolver->resolve( + array( + 'foo' => array( + array(1, 2), + ), + ) + )); + } + + public function testNested2Arrays() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][][][]'); + + $this->assertEquals(array( + 'foo' => array( + array( + array( + array( + 1, 2, + ), + ), + ), + ), + ), $this->resolver->resolve( + array( + 'foo' => array( + array( + array( + array(1, 2), + ), + ), + ), + ) + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "float[][][][]", but one of the elements is of type "integer[][][][]". + */ + public function testNestedArraysException() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'float[][][][]'); + + $this->resolver->resolve( + array( + 'foo' => array( + array( + 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 one of the elements is of type "boolean[][]". + */ + public function testNestedArrayException1() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + $this->resolver->resolve(array( + 'foo' => array( + array(1, true, 'str', array(2, 3)), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "boolean[][]". + */ + public function testNestedArrayException2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + $this->resolver->resolve(array( + 'foo' => array( + array(true, 'str', array(2, 3)), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[][][]", but one of the elements is of type "string[][]". + */ + public function testNestedArrayException3() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'string[][][]'); + $this->resolver->resolve(array( + 'foo' => array( + array('str', array(1, 2)), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[][][]", but one of the elements is of type "integer[][][]". + */ + public function testNestedArrayException4() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'string[][][]'); + $this->resolver->resolve(array( + 'foo' => array( + array( + array('str'), array(1, 2), ), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[]", but one of the elements is of type "array[]". + */ + public function testNestedArrayException5() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'string[]'); + $this->resolver->resolve(array( + 'foo' => array( + array( + array('str'), array(1, 2), ), + ), + )); + } }