Support array of types in allowed type

This commit is contained in:
Pierre du Plessis 2017-06-08 22:30:26 +02:00 committed by Nicolas Grekas
parent 0f5e38c732
commit d066a23860
3 changed files with 156 additions and 15 deletions

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* added `OptionsResolverIntrospector` to inspect options definitions inside an `OptionsResolver` instance
* added array of types support in allowed types (e.g int[])
2.6.0
-----

View File

@ -792,21 +792,12 @@ class OptionsResolver implements Options
// Validate the type of the resolved option
if (isset($this->allowedTypes[$option])) {
$valid = false;
$invalidTypes = array();
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;
if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) {
break;
}
}
@ -818,7 +809,7 @@ class OptionsResolver implements Options
$option,
$this->formatValue($value),
implode('" or "', $this->allowedTypes[$option]),
$this->formatTypeOf($value)
implode('|', array_keys($invalidTypes))
));
}
}
@ -895,6 +886,45 @@ class OptionsResolver implements Options
return $value;
}
/**
* @param string $type
* @param mixed $value
* @param array &$invalidTypes
*
* @return bool
*/
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 (function_exists($isFunction = 'is_'.$type) && !$isFunction($value)) || !$value instanceof $type;
}
);
if (!$invalidValues) {
return true;
}
$invalidTypes[$this->formatTypeOf($value, $originalType)] = true;
return false;
}
if ((function_exists($isFunction = 'is_'.$type) && $isFunction($value)) || $value instanceof $type) {
return true;
}
if (!$invalidTypes) {
$invalidTypes[$this->formatTypeOf($value, null)] = true;
}
return false;
}
/**
* Returns whether a resolved option with the given name exists.
*
@ -963,13 +993,38 @@ class OptionsResolver implements Options
* 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 $type
*
* @return string The type of the value
*/
private function formatTypeOf($value)
private function formatTypeOf($value, $type)
{
return is_object($value) ? get_class($value) : gettype($value);
$suffix = '';
if ('[]' === substr($type, -2)) {
$suffix = '[]';
$type = substr($type, 0, -2);
while ('[]' === substr($type, -2)) {
$type = substr($type, 0, -2);
$value = array_shift($value);
if (!is_array($value)) {
break;
}
$suffix .= '[]';
}
if (is_array($value)) {
$subTypes = array();
foreach ($value as $val) {
$subTypes[$this->formatTypeOf($val, null)] = true;
}
return implode('|', array_keys($subTypes)).$suffix;
}
}
return (is_object($value) ? get_class($value) : gettype($value)).$suffix;
}
/**

View File

@ -500,6 +500,65 @@ class OptionsResolverTest extends TestCase
$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 "bar" is expected to be of type "int[]", but is of type "string".
*/
public function testResolveFailsWithNonArray()
{
$this->resolver->setDefined('foo');
$this->resolver->setAllowedTypes('foo', 'int[]');
$this->resolver->resolve(array('foo' => 'bar'));
}
/**
* @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));
}
/**
* @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),
),
)
);
}
/**
* @dataProvider provideInvalidTypes
*/
@ -568,6 +627,32 @@ class OptionsResolverTest extends TestCase
$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);
}
/**
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfNotInstanceOfClass()
{
$this->resolver->setDefault('foo', 'bar');
$this->resolver->setAllowedTypes('foo', '\stdClass');
$this->resolver->resolve();
}
////////////////////////////////////////////////////////////////////////////
// addAllowedTypes()
////////////////////////////////////////////////////////////////////////////