Skip to content

Commit 4d7ac97

Browse files
Doctrsnicolas-grekas
authored andcommitted
[OptionResolver] resolve arrays
1 parent f3109a6 commit 4d7ac97

File tree

2 files changed

+223
-36
lines changed

2 files changed

+223
-36
lines changed

OptionsResolver.php

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ public function setAllowedValues($option, $allowedValues)
433433
));
434434
}
435435

436-
$this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues);
436+
$this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : array($allowedValues);
437437

438438
// Make sure the option is processed
439439
unset($this->resolved[$option]);
@@ -785,14 +785,13 @@ public function offsetGet($option)
785785
}
786786

787787
if (!$valid) {
788-
throw new InvalidOptionsException(sprintf(
789-
'The option "%s" with value %s is expected to be of type '.
790-
'"%s", but is of type "%s".',
791-
$option,
792-
$this->formatValue($value),
793-
implode('" or "', $this->allowedTypes[$option]),
794-
implode('|', array_keys($invalidTypes))
795-
));
788+
$keys = array_keys($invalidTypes);
789+
790+
if (1 === \count($keys) && '[]' === substr($keys[0], -2)) {
791+
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]));
792+
}
793+
794+
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))));
796795
}
797796
}
798797

@@ -877,23 +876,8 @@ public function offsetGet($option)
877876
*/
878877
private function verifyTypes($type, $value, array &$invalidTypes)
879878
{
880-
if ('[]' === substr($type, -2) && is_array($value)) {
881-
$originalType = $type;
882-
$type = substr($type, 0, -2);
883-
$invalidValues = array_filter( // Filter out valid values, keeping invalid values in the resulting array
884-
$value,
885-
function ($value) use ($type) {
886-
return !self::isValueValidType($type, $value);
887-
}
888-
);
889-
890-
if (!$invalidValues) {
891-
return true;
892-
}
893-
894-
$invalidTypes[$this->formatTypeOf($value, $originalType)] = true;
895-
896-
return false;
879+
if (\is_array($value) && '[]' === substr($type, -2)) {
880+
return $this->verifyArrayType($type, $value, $invalidTypes);
897881
}
898882

899883
if (self::isValueValidType($type, $value)) {
@@ -907,6 +891,46 @@ function ($value) use ($type) {
907891
return false;
908892
}
909893

894+
/**
895+
* @return bool
896+
*/
897+
private function verifyArrayType($type, array $value, array &$invalidTypes, $level = 0)
898+
{
899+
$type = substr($type, 0, -2);
900+
901+
$suffix = '[]';
902+
while (\strlen($suffix) <= $level * 2) {
903+
$suffix .= '[]';
904+
}
905+
906+
if ('[]' === substr($type, -2)) {
907+
$success = true;
908+
foreach ($value as $item) {
909+
if (!\is_array($item)) {
910+
$invalidTypes[$this->formatTypeOf($item, null).$suffix] = true;
911+
912+
return false;
913+
}
914+
915+
if (!$this->verifyArrayType($type, $item, $invalidTypes, $level + 1)) {
916+
$success = false;
917+
}
918+
}
919+
920+
return $success;
921+
}
922+
923+
foreach ($value as $item) {
924+
if (!self::isValueValidType($type, $item)) {
925+
$invalidTypes[$this->formatTypeOf($item, $type).$suffix] = $value;
926+
927+
return false;
928+
}
929+
}
930+
931+
return true;
932+
}
933+
910934
/**
911935
* Returns whether a resolved option with the given name exists.
912936
*
@@ -990,13 +1014,13 @@ private function formatTypeOf($value, $type)
9901014
while ('[]' === substr($type, -2)) {
9911015
$type = substr($type, 0, -2);
9921016
$value = array_shift($value);
993-
if (!is_array($value)) {
1017+
if (!\is_array($value)) {
9941018
break;
9951019
}
9961020
$suffix .= '[]';
9971021
}
9981022

999-
if (is_array($value)) {
1023+
if (\is_array($value)) {
10001024
$subTypes = array();
10011025
foreach ($value as $val) {
10021026
$subTypes[$this->formatTypeOf($val, null)] = true;
@@ -1006,7 +1030,7 @@ private function formatTypeOf($value, $type)
10061030
}
10071031
}
10081032

1009-
return (is_object($value) ? get_class($value) : gettype($value)).$suffix;
1033+
return (\is_object($value) ? get_class($value) : gettype($value)).$suffix;
10101034
}
10111035

10121036
/**
@@ -1022,19 +1046,19 @@ private function formatTypeOf($value, $type)
10221046
*/
10231047
private function formatValue($value)
10241048
{
1025-
if (is_object($value)) {
1049+
if (\is_object($value)) {
10261050
return get_class($value);
10271051
}
10281052

1029-
if (is_array($value)) {
1053+
if (\is_array($value)) {
10301054
return 'array';
10311055
}
10321056

1033-
if (is_string($value)) {
1057+
if (\is_string($value)) {
10341058
return '"'.$value.'"';
10351059
}
10361060

1037-
if (is_resource($value)) {
1061+
if (\is_resource($value)) {
10381062
return 'resource';
10391063
}
10401064

@@ -1078,4 +1102,20 @@ private static function isValueValidType($type, $value)
10781102
{
10791103
return (function_exists($isFunction = 'is_'.$type) && $isFunction($value)) || $value instanceof $type;
10801104
}
1105+
1106+
/**
1107+
* @return array
1108+
*/
1109+
private function getInvalidValues(array $arrayValues, $type)
1110+
{
1111+
$invalidValues = array();
1112+
1113+
foreach ($arrayValues as $key => $value) {
1114+
if (!self::isValueValidType($type, $value)) {
1115+
$invalidValues[$key] = $value;
1116+
}
1117+
}
1118+
1119+
return $invalidValues;
1120+
}
10811121
}

Tests/OptionsResolverTest.php

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ public function testFailIfSetAllowedTypesFromLazyOption()
511511

512512
/**
513513
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
514-
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "DateTime[]".
514+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but one of the elements is of type "DateTime[]".
515515
*/
516516
public function testResolveFailsIfInvalidTypedArray()
517517
{
@@ -535,7 +535,7 @@ public function testResolveFailsWithNonArray()
535535

536536
/**
537537
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
538-
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "integer|stdClass|array|DateTime[]".
538+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but one of the elements is of type "stdClass[]".
539539
*/
540540
public function testResolveFailsIfTypedArrayContainsInvalidTypes()
541541
{
@@ -552,7 +552,7 @@ public function testResolveFailsIfTypedArrayContainsInvalidTypes()
552552

553553
/**
554554
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
555-
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but is of type "double[][]".
555+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "double[][]".
556556
*/
557557
public function testResolveFailsWithCorrectLevelsButWrongScalar()
558558
{
@@ -1650,4 +1650,151 @@ public function testCountFailsOutsideResolve()
16501650

16511651
count($this->resolver);
16521652
}
1653+
1654+
public function testNestedArrays()
1655+
{
1656+
$this->resolver->setDefined('foo');
1657+
$this->resolver->setAllowedTypes('foo', 'int[][]');
1658+
1659+
$this->assertEquals(array(
1660+
'foo' => array(
1661+
array(
1662+
1, 2,
1663+
),
1664+
),
1665+
), $this->resolver->resolve(
1666+
array(
1667+
'foo' => array(
1668+
array(1, 2),
1669+
),
1670+
)
1671+
));
1672+
}
1673+
1674+
public function testNested2Arrays()
1675+
{
1676+
$this->resolver->setDefined('foo');
1677+
$this->resolver->setAllowedTypes('foo', 'int[][][][]');
1678+
1679+
$this->assertEquals(array(
1680+
'foo' => array(
1681+
array(
1682+
array(
1683+
array(
1684+
1, 2,
1685+
),
1686+
),
1687+
),
1688+
),
1689+
), $this->resolver->resolve(
1690+
array(
1691+
'foo' => array(
1692+
array(
1693+
array(
1694+
array(1, 2),
1695+
),
1696+
),
1697+
),
1698+
)
1699+
));
1700+
}
1701+
1702+
/**
1703+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1704+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "float[][][][]", but one of the elements is of type "integer[][][][]".
1705+
*/
1706+
public function testNestedArraysException()
1707+
{
1708+
$this->resolver->setDefined('foo');
1709+
$this->resolver->setAllowedTypes('foo', 'float[][][][]');
1710+
1711+
$this->resolver->resolve(
1712+
array(
1713+
'foo' => array(
1714+
array(
1715+
array(
1716+
array(1, 2),
1717+
),
1718+
),
1719+
),
1720+
)
1721+
);
1722+
}
1723+
1724+
/**
1725+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1726+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "boolean[][]".
1727+
*/
1728+
public function testNestedArrayException1()
1729+
{
1730+
$this->resolver->setDefined('foo');
1731+
$this->resolver->setAllowedTypes('foo', 'int[][]');
1732+
$this->resolver->resolve(array(
1733+
'foo' => array(
1734+
array(1, true, 'str', array(2, 3)),
1735+
),
1736+
));
1737+
}
1738+
1739+
/**
1740+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1741+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "boolean[][]".
1742+
*/
1743+
public function testNestedArrayException2()
1744+
{
1745+
$this->resolver->setDefined('foo');
1746+
$this->resolver->setAllowedTypes('foo', 'int[][]');
1747+
$this->resolver->resolve(array(
1748+
'foo' => array(
1749+
array(true, 'str', array(2, 3)),
1750+
),
1751+
));
1752+
}
1753+
1754+
/**
1755+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1756+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[][][]", but one of the elements is of type "string[][]".
1757+
*/
1758+
public function testNestedArrayException3()
1759+
{
1760+
$this->resolver->setDefined('foo');
1761+
$this->resolver->setAllowedTypes('foo', 'string[][][]');
1762+
$this->resolver->resolve(array(
1763+
'foo' => array(
1764+
array('str', array(1, 2)),
1765+
),
1766+
));
1767+
}
1768+
1769+
/**
1770+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1771+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[][][]", but one of the elements is of type "integer[][][]".
1772+
*/
1773+
public function testNestedArrayException4()
1774+
{
1775+
$this->resolver->setDefined('foo');
1776+
$this->resolver->setAllowedTypes('foo', 'string[][][]');
1777+
$this->resolver->resolve(array(
1778+
'foo' => array(
1779+
array(
1780+
array('str'), array(1, 2), ),
1781+
),
1782+
));
1783+
}
1784+
1785+
/**
1786+
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
1787+
* @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[]", but one of the elements is of type "array[]".
1788+
*/
1789+
public function testNestedArrayException5()
1790+
{
1791+
$this->resolver->setDefined('foo');
1792+
$this->resolver->setAllowedTypes('foo', 'string[]');
1793+
$this->resolver->resolve(array(
1794+
'foo' => array(
1795+
array(
1796+
array('str'), array(1, 2), ),
1797+
),
1798+
));
1799+
}
16531800
}

0 commit comments

Comments
 (0)