Skip to content

Commit

Permalink
[OptionsResolver] Added option type validation capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
webmozart committed May 25, 2012
1 parent 0af5f06 commit 97de004
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/Symfony/Component/OptionsResolver/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ public function replace(array $options)
* Passed closures should have the following signature:
*
* <code>
* function (Options $options, $previousValue)
* function (Options $options, $value)
* </code>
*
* The second parameter passed to the closure is the previous default
* The second parameter passed to the closure is the current default
* value of the option.
*
* @param string $option The option name.
Expand Down
90 changes: 89 additions & 1 deletion src/Symfony/Component/OptionsResolver/OptionsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ class OptionsResolver
*/
private $allowedValues = array();

/**
* A list of accepted types for each option.
* @var array
*/
private $allowedTypes = array();

/**
* A list of filters transforming each resolved options.
* @var array
Expand Down Expand Up @@ -222,6 +228,48 @@ public function addAllowedValues(array $allowedValues)
return $this;
}

/**
* Sets allowed types for a list of options.
*
* @param array $allowedTypes A list of option names as keys and type
* names passed as string or array as values.
*
* @return OptionsResolver The resolver instance.
*
* @throws InvalidOptionsException If an option has not been defined for
* which an allowed type is set.
*/
public function setAllowedTypes(array $allowedTypes)
{
$this->validateOptionNames(array_keys($allowedTypes));

$this->allowedTypes = array_replace($this->allowedTypes, $allowedTypes);

return $this;
}

/**
* Adds allowed types for a list of options.
*
* The types are merged with the allowed types defined previously.
*
* @param array $allowedTypes A list of option names as keys and type
* names passed as string or array as values.
*
* @return OptionsResolver The resolver instance.
*
* @throws InvalidOptionsException If an option has not been defined for
* which an allowed type is set.
*/
public function addAllowedTypes(array $allowedTypes)
{
$this->validateOptionNames(array_keys($allowedTypes));

$this->allowedTypes = array_merge_recursive($this->allowedTypes, $allowedTypes);

return $this;
}

/**
* Sets filters that are applied on resolved options.
*
Expand Down Expand Up @@ -312,8 +360,8 @@ public function resolve(array $options)
// Resolve options
$resolvedOptions = $combinedOptions->all();

// Validate against allowed values
$this->validateOptionValues($resolvedOptions);
$this->validateOptionTypes($resolvedOptions);

return $resolvedOptions;
}
Expand Down Expand Up @@ -381,4 +429,44 @@ private function validateOptionValues(array $options)
}
}
}

/**
* Validates that the given options match the allowed types and
* throws an exception otherwise.
*
* @param array $options A list of options.
*
* @throws InvalidOptionsException If any of the types does not match the
* allowed types of the option.
*/
private function validateOptionTypes(array $options)
{
foreach ($this->allowedTypes as $option => $allowedTypes) {
$value = $options[$option];
$allowedTypes = (array) $allowedTypes;

foreach ($allowedTypes as $type) {
$isFunction = 'is_' . $type;

if (function_exists($isFunction) && $isFunction($value)) {
continue 2;
} elseif ($value instanceof $type) {
continue 2;
}
}

$printableValue = is_object($value)
? get_class($value)
: (is_array($value)
? 'Array'
: (string) $value);

throw new InvalidOptionsException(sprintf(
'The option "%s" with value "%s" is expected to be of type "%s"',
$option,
$printableValue,
implode('", "', $allowedTypes)
));
}
}
}
165 changes: 165 additions & 0 deletions src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,171 @@ public function testResolveFailsIfOptionValueNotAllowed()
));
}

public function testResolveSucceedsIfOptionTypeAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setAllowedTypes(array(
'one' => 'string',
));

$options = array(
'one' => 'one',
);

$this->assertEquals(array(
'one' => 'one',
), $this->resolver->resolve($options));
}

public function testResolveSucceedsIfOptionTypeAllowedPassArray()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setAllowedTypes(array(
'one' => array('string', 'bool'),
));

$options = array(
'one' => true,
);

$this->assertEquals(array(
'one' => true,
), $this->resolver->resolve($options));
}

public function testResolveSucceedsIfOptionTypeAllowedPassObject()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setAllowedTypes(array(
'one' => 'object',
));

$object = new \stdClass();
$options = array(
'one' => $object,
);

$this->assertEquals(array(
'one' => $object,
), $this->resolver->resolve($options));
}

public function testResolveSucceedsIfOptionTypeAllowedPassClass()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setAllowedTypes(array(
'one' => '\stdClass',
));

$object = new \stdClass();
$options = array(
'one' => $object,
);

$this->assertEquals(array(
'one' => $object,
), $this->resolver->resolve($options));
}

public function testResolveSucceedsIfOptionTypeAllowedAddTypes()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));

$this->resolver->setAllowedTypes(array(
'one' => 'string',
'two' => 'bool',
));
$this->resolver->addAllowedTypes(array(
'one' => 'float',
'two' => 'integer',
));

$options = array(
'one' => 1.23,
'two' => false,
);

$this->assertEquals(array(
'one' => 1.23,
'two' => false,
), $this->resolver->resolve($options));
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionTypeNotAllowed()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setAllowedTypes(array(
'one' => array('string', 'bool'),
));

$this->resolver->resolve(array(
'one' => 1.23,
));
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionTypeNotAllowedMultipleOptions()
{
$this->resolver->setDefaults(array(
'one' => '1',
'two' => '2',
));

$this->resolver->setAllowedTypes(array(
'one' => 'string',
'two' => 'bool',
));

$this->resolver->resolve(array(
'one' => 'foo',
'two' => 1.23,
));
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testResolveFailsIfOptionTypeNotAllowedAddTypes()
{
$this->resolver->setDefaults(array(
'one' => '1',
));

$this->resolver->setAllowedTypes(array(
'one' => 'string',
));
$this->resolver->addAllowedTypes(array(
'one' => 'bool',
));

$this->resolver->resolve(array(
'one' => 1.23,
));
}

/**
* @expectedException Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
*/
Expand Down

0 comments on commit 97de004

Please sign in to comment.