From 71db836e1f27c5b2039a7c5c4346179f83ca783c Mon Sep 17 00:00:00 2001 From: Jeanmonod David Date: Tue, 12 Jun 2012 17:47:41 +0200 Subject: [PATCH] Better config validation handling for numerical values: * New node type Integer and Float * New expressions: min() and max() --- .../Builder/FloatNodeDefinition.php | 32 +++++++ .../Builder/IntegerNodeDefinition.php | 32 +++++++ .../Config/Definition/Builder/NodeBuilder.php | 26 ++++++ .../Builder/NumericNodeDefinition.php | 58 +++++++++++++ .../Component/Config/Definition/FloatNode.php | 44 ++++++++++ .../Config/Definition/IntegerNode.php | 39 +++++++++ .../Config/Definition/NumericNode.php | 56 +++++++++++++ .../Definition/Builder/ExprBuilderTest.php | 13 +-- .../Definition/Builder/NodeBuilderTest.php | 11 +++ .../Builder/NumericNodeDefinitionTest.php | 84 +++++++++++++++++++ .../Config/Tests/Definition/FloatNodeTest.php | 64 ++++++++++++++ .../Tests/Definition/IntegerNodeTest.php | 61 ++++++++++++++ 12 files changed, 514 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php create mode 100644 src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php create mode 100644 src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php create mode 100644 src/Symfony/Component/Config/Definition/FloatNode.php create mode 100644 src/Symfony/Component/Config/Definition/IntegerNode.php create mode 100644 src/Symfony/Component/Config/Definition/NumericNode.php create mode 100755 src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php create mode 100644 src/Symfony/Component/Config/Tests/Definition/FloatNodeTest.php create mode 100644 src/Symfony/Component/Config/Tests/Definition/IntegerNodeTest.php diff --git a/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php new file mode 100644 index 000000000000..b8fc34d0ead4 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\FloatNode; + +/** + * This class provides a fluent interface for defining a float node. + * + * @author Jeanmonod David + */ +class FloatNodeDefinition extends NumericNodeDefinition +{ + /** + * Instantiate a Node + * + * @return FloatNode The node + */ + protected function instantiateNode() + { + return new FloatNode($this->name, $this->parent, $this->min, $this->max); + } +} diff --git a/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php new file mode 100644 index 000000000000..d3d6179276ed --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +use Symfony\Component\Config\Definition\IntegerNode; + +/** + * This class provides a fluent interface for defining an integer node. + * + * @author Jeanmonod David + */ +class IntegerNodeDefinition extends NumericNodeDefinition +{ + /** + * Instantiate a Node + * + * @return IntegerNode The node + */ + protected function instantiateNode() + { + return new IntegerNode($this->name, $this->parent, $this->min, $this->max); + } +} diff --git a/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php b/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php index c960ac43a6c1..a53685a22e57 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/NodeBuilder.php @@ -31,6 +31,8 @@ public function __construct() 'variable' => __NAMESPACE__.'\\VariableNodeDefinition', 'scalar' => __NAMESPACE__.'\\ScalarNodeDefinition', 'boolean' => __NAMESPACE__.'\\BooleanNodeDefinition', + 'integer' => __NAMESPACE__.'\\IntegerNodeDefinition', + 'float' => __NAMESPACE__.'\\FloatNodeDefinition', 'array' => __NAMESPACE__.'\\ArrayNodeDefinition', 'enum' => __NAMESPACE__.'\\EnumNodeDefinition', ); @@ -86,6 +88,30 @@ public function booleanNode($name) return $this->node($name, 'boolean'); } + /** + * Creates a child integer node. + * + * @param string $name the name of the node + * + * @return IntegerNodeDefinition The child node + */ + public function integerNode($name) + { + return $this->node($name, 'integer'); + } + + /** + * Creates a child float node. + * + * @param string $name the name of the node + * + * @return FloatNodeDefinition The child node + */ + public function floatNode($name) + { + return $this->node($name, 'float'); + } + /** * Creates a child EnumNode. * diff --git a/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php new file mode 100644 index 000000000000..54f6d86f6d91 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Builder; + +/** + * Abstract class that contain common code of integer and float node definition. + * + * @author David Jeanmonod + */ +abstract class NumericNodeDefinition extends ScalarNodeDefinition +{ + + protected $min; + protected $max; + + /** + * Ensure the value is smaller than the given reference + * + * @param mixed $max + * + * @return NumericNodeDefinition + */ + public function max($max) + { + if (isset($this->min) && $this->min > $max) { + throw new \InvalidArgumentException(sprintf('You cannot define a max(%s) as you already have a min(%s)', $max, $this->min)); + } + $this->max = $max; + + return $this; + } + + /** + * Ensure the value is bigger than the given reference + * + * @param mixed $min + * + * @return NumericNodeDefinition + */ + public function min($min) + { + if (isset($this->max) && $this->max < $min) { + throw new \InvalidArgumentException(sprintf('You cannot define a min(%s) as you already have a max(%s)', $min, $this->max)); + } + $this->min = $min; + + return $this; + } +} diff --git a/src/Symfony/Component/Config/Definition/FloatNode.php b/src/Symfony/Component/Config/Definition/FloatNode.php new file mode 100644 index 000000000000..782bd8b708d9 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/FloatNode.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * This node represents a float value in the config tree. + * + * @author Jeanmonod David + */ +class FloatNode extends NumericNode +{ + /** + * {@inheritDoc} + */ + protected function validateType($value) + { + // Integers are also accepted, we just cast them + if (is_int($value)) { + $value = (float) $value; + } + + if (!is_float($value)) { + $ex = new InvalidTypeException(sprintf( + 'Invalid type for path "%s". Expected float, but got %s.', + $this->getPath(), + gettype($value) + )); + $ex->setPath($this->getPath()); + + throw $ex; + } + } +} diff --git a/src/Symfony/Component/Config/Definition/IntegerNode.php b/src/Symfony/Component/Config/Definition/IntegerNode.php new file mode 100644 index 000000000000..7a5c394513e8 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/IntegerNode.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; + +/** + * This node represents an integer value in the config tree. + * + * @author Jeanmonod David + */ +class IntegerNode extends NumericNode +{ + /** + * {@inheritDoc} + */ + protected function validateType($value) + { + if (!is_int($value)) { + $ex = new InvalidTypeException(sprintf( + 'Invalid type for path "%s". Expected int, but got %s.', + $this->getPath(), + gettype($value) + )); + $ex->setPath($this->getPath()); + + throw $ex; + } + } +} diff --git a/src/Symfony/Component/Config/Definition/NumericNode.php b/src/Symfony/Component/Config/Definition/NumericNode.php new file mode 100644 index 000000000000..88c0caa628d7 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/NumericNode.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + +/** + * This node represents a numeric value in the config tree + * + * @author David Jeanmonod + */ +class NumericNode extends ScalarNode +{ + protected $min; + protected $max; + + public function __construct($name, NodeInterface $parent = null, $min = null, $max = null) + { + parent::__construct($name, $parent); + $this->min = $min; + $this->max = $max; + } + + /** + * {@inheritDoc} + */ + protected function finalizeValue($value) + { + $value = parent::finalizeValue($value); + + $errorMsg = null; + if (isset($this->min) && $value < $this->min) { + $errorMsg = sprintf('The value %s is too small for path "%s". Should be greater than: %s', $value, $this->getPath(), $this->min); + } + if (isset($this->max) && $value > $this->max) { + $errorMsg = sprintf('The value %s is too big for path "%s". Should be less than: %s', $value, $this->getPath(), $this->max); + } + if (isset($errorMsg)) { + $ex = new InvalidConfigurationException($errorMsg); + $ex->setPath($this->getPath()); + throw $ex; + } + + return $value; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php index 7d4c72bb26b6..9c7b71902e11 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php @@ -13,7 +13,6 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; - class ExprBuilderTest extends \PHPUnit_Framework_TestCase { @@ -160,6 +159,7 @@ public function testThenUnsetExpression() protected function getTestBuilder() { $builder = new TreeBuilder(); + return $builder ->root('test') ->children() @@ -171,7 +171,7 @@ protected function getTestBuilder() /** * Close the validation process and finalize with the given config * @param TreeBuilder $testBuilder The tree builder to finalize - * @param array $config The config you want to use for the finalization, if nothing provided + * @param array $config The config you want to use for the finalization, if nothing provided * a simple array('key'=>'value') will be used * @return array The finalized config values */ @@ -191,7 +191,8 @@ protected function finalizeTestBuilder($testBuilder, $config=null) * @param $val The value that the closure must return * @return Closure */ - protected function returnClosure($val) { + protected function returnClosure($val) + { return function($v) use ($val) { return $val; }; @@ -199,9 +200,9 @@ protected function returnClosure($val) { /** * Assert that the given test builder, will return the given value - * @param mixed $value The value to test - * @param TreeBuilder $test The tree builder to finalize - * @param mixed $config The config values that new to be finalized + * @param mixed $value The value to test + * @param TreeBuilder $test The tree builder to finalize + * @param mixed $config The config values that new to be finalized */ protected function assertFinalizedValueIs($value, $treeBuilder, $config=null) { diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/NodeBuilderTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/NodeBuilderTest.php index c8295916908a..8d0a84530994 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/NodeBuilderTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/NodeBuilderTest.php @@ -76,6 +76,17 @@ public function testNodeTypesAreNotCaseSensitive() $this->assertEquals(get_class($node1), get_class($node2)); } + + public function testNumericNodeCreation() + { + $builder = new NodeBuilder(); + + $node = $builder->integerNode('foo')->min(3)->max(5); + $this->assertEquals('Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition', get_class($node)); + + $node = $builder->floatNode('bar')->min(3.0)->max(5.0); + $this->assertEquals('Symfony\Component\Config\Definition\Builder\FloatNodeDefinition', get_class($node)); + } } class SomeNodeDefinition extends BaseVariableNodeDefinition diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php new file mode 100755 index 000000000000..0add0db5682c --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php @@ -0,0 +1,84 @@ +max(3)->min(4); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage You cannot define a max(2) as you already have a min(3) + */ + public function testIncoherentMaxAssertion() + { + $node = new NumericNodeDefinition('foo'); + $node->min(3)->max(2); + } + + /** + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage The value 4 is too small for path "foo". Should be greater than: 5 + */ + public function testIntegerMinAssertion() + { + $def = new IntegerNodeDefinition('foo'); + $def->min(5)->getNode()->finalize(4); + } + + /** + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage The value 4 is too big for path "foo". Should be less than: 3 + */ + public function testIntegerMaxAssertion() + { + $def = new IntegerNodeDefinition('foo'); + $def->max(3)->getNode()->finalize(4); + } + + public function testIntegerValidMinMaxAssertion() + { + $def = new IntegerNodeDefinition('foo'); + $node = $def->min(3)->max(7)->getNode(); + $this->assertEquals(4, $node->finalize(4)); + } + + /** + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage The value 400 is too small for path "foo". Should be greater than: 500 + */ + public function testFloatMinAssertion() + { + $def = new FloatNodeDefinition('foo'); + $def->min(5E2)->getNode()->finalize(4e2); + } + + /** + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage The value 4.3 is too big for path "foo". Should be less than: 0.3 + */ + public function testFloatMaxAssertion() + { + $def = new FloatNodeDefinition('foo'); + $def->max(0.3)->getNode()->finalize(4.3); + } + + public function testFloatValidMinMaxAssertion() + { + $def = new FloatNodeDefinition('foo'); + $node = $def->min(3.0)->max(7e2)->getNode(); + $this->assertEquals(4.5, $node->finalize(4.5)); + } +} diff --git a/src/Symfony/Component/Config/Tests/Definition/FloatNodeTest.php b/src/Symfony/Component/Config/Tests/Definition/FloatNodeTest.php new file mode 100644 index 000000000000..b91446e1c8c9 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Definition/FloatNodeTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Definition; + +use Symfony\Component\Config\Definition\FloatNode; + +class FloatNodeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getValidValues + */ + public function testNormalize($value) + { + $node = new FloatNode('test'); + $this->assertSame($value, $node->normalize($value)); + } + + public function getValidValues() + { + return array( + array(1798.0), + array(-678.987), + array(12.56E45), + array(0.0), + // Integer are accepted too, they will be cast + array(17), + array(-10), + array(0) + ); + } + + /** + * @dataProvider getInvalidValues + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidTypeException + */ + public function testNormalizeThrowsExceptionOnInvalidValues($value) + { + $node = new FloatNode('test'); + $node->normalize($value); + } + + public function getInvalidValues() + { + return array( + array(null), + array(''), + array('foo'), + array(true), + array(false), + array(array()), + array(array('foo' => 'bar')), + array(new \stdClass()), + ); + } +} diff --git a/src/Symfony/Component/Config/Tests/Definition/IntegerNodeTest.php b/src/Symfony/Component/Config/Tests/Definition/IntegerNodeTest.php new file mode 100644 index 000000000000..11d53153069f --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Definition/IntegerNodeTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Definition; + +use Symfony\Component\Config\Definition\IntegerNode; + +class IntegerNodeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getValidValues + */ + public function testNormalize($value) + { + $node = new IntegerNode('test'); + $this->assertSame($value, $node->normalize($value)); + } + + public function getValidValues() + { + return array( + array(1798), + array(-678), + array(0), + ); + } + + /** + * @dataProvider getInvalidValues + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidTypeException + */ + public function testNormalizeThrowsExceptionOnInvalidValues($value) + { + $node = new IntegerNode('test'); + $node->normalize($value); + } + + public function getInvalidValues() + { + return array( + array(null), + array(''), + array('foo'), + array(true), + array(false), + array(0.0), + array(0.1), + array(array()), + array(array('foo' => 'bar')), + array(new \stdClass()), + ); + } +}