Skip to content

Commit

Permalink
feature #23112 [OptionsResolver] Support array of types in allowed ty…
Browse files Browse the repository at this point in the history
…pe (pierredup)

This PR was merged into the 3.4 branch.

Discussion
----------

[OptionsResolver] Support array of types in allowed type

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

This replaces #17032 with a simpler approach to allow an array of types in the allowed types for the options resolver

Note: This implementation doesn't support nested values (I.E `int[][]`), but if there is a strong need for it, I'll add it in another PR

Commits
-------

d066a23 Support array of types in allowed type
  • Loading branch information
fabpot committed Oct 12, 2017
2 parents f35996d + d066a23 commit 3c3d642
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/OptionsResolver/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
85 changes: 70 additions & 15 deletions src/Symfony/Component/OptionsResolver/OptionsResolver.php
Expand Up @@ -792,21 +792,12 @@ public function offsetGet($option)
// 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;
}
}
Expand All @@ -818,7 +809,7 @@ public function offsetGet($option)
$option,
$this->formatValue($value),
implode('" or "', $this->allowedTypes[$option]),
$this->formatTypeOf($value)
implode('|', array_keys($invalidTypes))
));
}
}
Expand Down Expand Up @@ -895,6 +886,45 @@ public function offsetGet($option)
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.
*
Expand Down Expand Up @@ -963,13 +993,38 @@ 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 $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;
}

/**
Expand Down
Expand Up @@ -500,6 +500,65 @@ 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 "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
*/
Expand Down Expand Up @@ -568,6 +627,32 @@ public function testResolveSucceedsIfInstanceOfClass()
$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()
////////////////////////////////////////////////////////////////////////////
Expand Down

0 comments on commit 3c3d642

Please sign in to comment.