bug #27435 [OptionResolver] resolve arrays (Doctrs)

This PR was squashed before being merged into the 3.4 branch (closes #27435).

Discussion
----------

[OptionResolver] resolve arrays

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        |

Option resolver didn't work with nested arrays

Before:

        $resolver->setDefaults([
            'integer' => [
                [
                    12,
                    23,
                ],
            ],
        ]);

        $resolver->setAllowedTypes('integer', 'integer[][]');

Error

    The option "host" with value array is expected to be of type "integer[][]", but is of type "integer[][]".

Option expetcted type `integer[][]` but get... `integer[][]`. So strange

Now that case work correct, and we get

    array (size=1)
      'integer' =>
        array (size=1)
          0 =>
            array (size=2)
              0 => int 12
              1 => int 23

Commits
-------

6d4812e995 [OptionResolver] resolve arrays
This commit is contained in:
Nicolas Grekas 2018-07-07 17:09:44 +02:00
commit 6fea6349ec
2 changed files with 223 additions and 36 deletions

View File

@ -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;
}
}

View File

@ -483,7 +483,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()
{
@ -507,7 +507,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()
{
@ -524,7 +524,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()
{
@ -1586,4 +1586,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), ),
),
));
}
}