diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index d96705d1ff..38eac5879a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -217,8 +217,8 @@ class Configuration implements ConfigurationInterface ->fixXmlConfig('resource') ->children() ->arrayNode('resources') - ->defaultValue(array('FrameworkBundle:Form')) - ->prototype('scalar')->end() + ->addDefaultChildrenIfNoneSet() + ->prototype('scalar')->defaultValue('FrameworkBundle:Form')->end() ->validate() ->ifTrue(function($v) {return !in_array('FrameworkBundle:Form', $v); }) ->then(function($v){ diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index 4e5fe7ce6b..9802094b70 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -54,8 +54,8 @@ class Configuration implements ConfigurationInterface ->fixXmlConfig('resource') ->children() ->arrayNode('resources') - ->defaultValue(array('form_div_layout.html.twig')) - ->prototype('scalar')->end() + ->addDefaultChildrenIfNoneSet() + ->prototype('scalar')->defaultValue('form_div_layout.html.twig')->end() ->setExample(array('MyBundle::form.html.twig')) ->validate() ->ifTrue(function($v) { return !in_array('form_div_layout.html.twig', $v); }) diff --git a/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php index 669ced43a6..28c93d49ce 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php @@ -31,6 +31,7 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition protected $key; protected $removeKeyItem; protected $addDefaults; + protected $addDefaultChildren; protected $nodeBuilder; /** @@ -42,6 +43,7 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition $this->children = array(); $this->addDefaults = false; + $this->addDefaultChildren = false; $this->allowNewKeys = true; $this->atLeastOne = false; $this->allowEmptyValue = true; @@ -98,6 +100,22 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition return $this; } + /** + * Adds children with a default value when none are defined. + * + * @param integer|string|array|null $children The number of children|The child name|The children names to be added + * + * This method is applicable to prototype nodes only. + * + * @return ArrayNodeDefinition + */ + public function addDefaultChildrenIfNoneSet($children = null) + { + $this->addDefaultChildren = $children; + + return $this; + } + /** * Requires the node to have at least one element. * @@ -260,40 +278,21 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition protected function createNode() { if (null === $this->prototype) { - if (null !== $this->key) { - throw new InvalidDefinitionException( - sprintf('%s::useAttributeAsKey() is not applicable to concrete nodes.', __CLASS__) - ); - } - - if (true === $this->atLeastOne) { - throw new InvalidDefinitionException( - sprintf('%s::requiresAtLeastOneElement() is not applicable to concrete nodes.', __CLASS__) - ); - } - - if ($this->default) { - throw new InvalidDefinitionException( - sprintf('%s::defaultValue() is not applicable to concrete nodes.', __CLASS__) - ); - } - $node = new ArrayNode($this->name, $this->parent); + + $this->validateConcreteNode($node); + $node->setAddIfNotSet($this->addDefaults); - + foreach ($this->children as $child) { $child->parent = $node; $node->addChild($child->getNode()); } } else { - if ($this->addDefaults) { - throw new InvalidDefinitionException( - sprintf('%s::addDefaultsIfNotSet() is not applicable to prototype nodes.', __CLASS__) - ); - } - $node = new PrototypedArrayNode($this->name, $this->parent); + $this->validatePrototypeNode($node); + if (null !== $this->key) { $node->setKeyAttribute($this->key, $this->removeKeyItem); } @@ -306,6 +305,13 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition $node->setDefaultValue($this->defaultValue); } + if (false !== $this->addDefaultChildren) { + $node->setAddChildrenIfNoneSet($this->addDefaultChildren); + if ($this->prototype instanceof static && null === $this->prototype->prototype) { + $this->prototype->addDefaultsIfNotSet(); + } + } + $this->prototype->parent = $node; $node->setPrototype($this->prototype->getNode()); } @@ -335,4 +341,77 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition return $node; } + /** + * Validate the confifuration of a concrete node. + * + * @param NodeInterface $node The related node + * + * @throws InvalidDefinitionException When an error is detected in the configuration + */ + protected function validateConcreteNode(ArrayNode $node) + { + $path = $node->getPath(); + + if (null !== $this->key) { + throw new InvalidDefinitionException( + sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s"', $path) + ); + } + + if (true === $this->atLeastOne) { + throw new InvalidDefinitionException( + sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s"', $path) + ); + } + + if ($this->default) { + throw new InvalidDefinitionException( + sprintf('->defaultValue() is not applicable to concrete nodes at path "%s"', $path) + ); + } + + if (false !== $this->addDefaultChildren) { + throw new InvalidDefinitionException( + sprintf('->addDefaultChildrenIfNoneSet() is not applicable to concrete nodes at path "%s"', $path) + ); + } + } + + /** + * Validate the configuration of a prototype node. + * + * @param NodeInterface $node The related node + * + * @throws InvalidDefinitionException When an error is detected in the configuration + */ + protected function validatePrototypeNode(PrototypedArrayNode $node) + { + $path = $node->getPath(); + + if ($this->addDefaults) { + throw new InvalidDefinitionException( + sprintf('->addDefaultsIfNotSet() is not applicable to prototype nodes at path "%s"', $path) + ); + } + + if (false !== $this->addDefaultChildren) { + if ($this->default) { + throw new InvalidDefinitionException( + sprintf('A default value and default children might not be used together at path "%s"', $path) + ); + } + + if (null !== $this->key && (null === $this->addDefaultChildren || is_integer($this->addDefaultChildren) && $this->addDefaultChildren > 0)) { + throw new InvalidDefinitionException( + sprintf('->addDefaultChildrenIfNoneSet() should set default children names as ->useAttributeAsKey() is used at path "%s"', $path) + ); + } + + if (null === $this->key && (is_string($this->addDefaultChildren) || is_array($this->addDefaultChildren))) { + throw new InvalidDefinitionException( + sprintf('->addDefaultChildrenIfNoneSet() might not set default children names as ->useAttributeAsKey() is not used at path "%s"', $path) + ); + } + } + } } diff --git a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php index e49d0d49f0..f800a3f8f5 100644 --- a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php +++ b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php @@ -28,6 +28,7 @@ class PrototypedArrayNode extends ArrayNode protected $removeKeyAttribute; protected $minNumberOfElements; protected $defaultValue; + protected $defaultChildren; /** * Constructor. @@ -120,13 +121,40 @@ class PrototypedArrayNode extends ArrayNode return true; } + /** + * Adds default children when none are set. + * + * @param integer|string|array|null $children The number of children|The child name|The children names to be added + */ + public function setAddChildrenIfNoneSet($children = array('defaults')) + { + if (null === $children) { + $this->defaultChildren = array('defaults'); + } else { + $this->defaultChildren = is_integer($children) && $children > 0 ? range(1, $children) : (array) $children; + } + } + /** * Retrieves the default value. * + * The default value could be either explicited or derived from the prototype + * default value. + * * @return array The default value */ public function getDefaultValue() { + if (null !== $this->defaultChildren) { + $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : array(); + $defaults = array(); + foreach (array_values($this->defaultChildren) as $i => $name) { + $defaults[null === $this->keyAttribute ? $i : $name] = $default; + } + + return $defaults; + } + return $this->defaultValue; } diff --git a/tests/Symfony/Tests/Component/Config/Definition/Builder/ArrayNodeDefinitionTest.php b/tests/Symfony/Tests/Component/Config/Definition/Builder/ArrayNodeDefinitionTest.php index 71ae7c865f..b3495d4e44 100644 --- a/tests/Symfony/Tests/Component/Config/Definition/Builder/ArrayNodeDefinitionTest.php +++ b/tests/Symfony/Tests/Component/Config/Definition/Builder/ArrayNodeDefinitionTest.php @@ -13,6 +13,7 @@ namespace Symfony\Tests\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; class ArrayNodeDefinitionTest extends \PHPUnit_Framework_TestCase { @@ -21,7 +22,7 @@ class ArrayNodeDefinitionTest extends \PHPUnit_Framework_TestCase $parent = new ArrayNodeDefinition('root'); $child = new ScalarNodeDefinition('child'); - $node = $parent + $parent ->children() ->scalarNode('foo')->end() ->scalarNode('bar')->end() @@ -49,6 +50,7 @@ class ArrayNodeDefinitionTest extends \PHPUnit_Framework_TestCase { return array( array('defaultValue', array(array())), + array('addDefaultChildrenIfNoneSet', array()), array('requiresAtLeastOneElement', array()), array('useAttributeAsKey', array('foo')) ); @@ -64,6 +66,89 @@ class ArrayNodeDefinitionTest extends \PHPUnit_Framework_TestCase $node->getNode(); } + /** + * @expectedException Symfony\Component\Config\Definition\Exception\InvalidDefinitionException + */ + public function testPrototypeNodesCantHaveADefaultValueWhenUsingDefaulChildren() + { + $node = new ArrayNodeDefinition('root'); + $node + ->defaultValue(array()) + ->addDefaultChildrenIfNoneSet('foo') + ->prototype('array') + ; + $node->getNode(); + } + + public function testPrototypedArrayNodeDefaultWhenUsingDefaultChildren() + { + $node = new ArrayNodeDefinition('root'); + $node + ->addDefaultChildrenIfNoneSet() + ->prototype('array') + ; + $tree = $node->getNode(); + $this->assertEquals(array(array()), $tree->getDefaultValue()); + } + + /** + * @dataProvider providePrototypedArrayNodeDefaults + */ + public function testPrototypedArrayNodeDefault($args, $shouldThrowWhenUsingAttrAsKey, $shouldThrowWhenNotUsingAttrAsKey, $defaults) + { + $node = new ArrayNodeDefinition('root'); + $node + ->addDefaultChildrenIfNoneSet($args) + ->prototype('array') + ; + + try { + $tree = $node->getNode(); + $this->assertFalse($shouldThrowWhenNotUsingAttrAsKey); + $this->assertEquals($defaults, $tree->getDefaultValue()); + } catch (InvalidDefinitionException $e) { + $this->assertTrue($shouldThrowWhenNotUsingAttrAsKey); + } + + $node = new ArrayNodeDefinition('root'); + $node + ->useAttributeAsKey('attr') + ->addDefaultChildrenIfNoneSet($args) + ->prototype('array') + ; + + try { + $tree = $node->getNode(); + $this->assertFalse($shouldThrowWhenUsingAttrAsKey); + $this->assertEquals($defaults, $tree->getDefaultValue()); + } catch (InvalidDefinitionException $e) { + $this->assertTrue($shouldThrowWhenUsingAttrAsKey); + } + } + + public function providePrototypedArrayNodeDefaults() + { + return array( + array(null, true, false, array(array())), + array(2, true, false, array(array(), array())), + array('2', false, true, array('2' => array())), + array('foo', false, true, array('foo' => array())), + array(array('foo'), false, true, array('foo' => array())), + array(array('foo', 'bar'), false, true, array('foo' => array(), 'bar' => array())), + ); + } + + public function testNestedPrototypedArrayNodes() + { + $node = new ArrayNodeDefinition('root'); + $node + ->addDefaultChildrenIfNoneSet() + ->prototype('array') + ->prototype('array') + ; + $node->getNode(); + } + protected function getField($object, $field) { $reflection = new \ReflectionProperty($object, $field); diff --git a/tests/Symfony/Tests/Component/Config/Definition/PrototypedArrayNodeTest.php b/tests/Symfony/Tests/Component/Config/Definition/PrototypedArrayNodeTest.php index 042e6671ac..4045b63b78 100644 --- a/tests/Symfony/Tests/Component/Config/Definition/PrototypedArrayNodeTest.php +++ b/tests/Symfony/Tests/Component/Config/Definition/PrototypedArrayNodeTest.php @@ -43,7 +43,7 @@ class PrototypedArrayNodeTest extends \PHPUnit_Framework_TestCase $node->addChild($mappingsNode); // each item under mappings is just a scalar - $prototype= new ScalarNode(null, $mappingsNode); + $prototype = new ScalarNode(null, $mappingsNode); $mappingsNode->setPrototype($prototype); $remappings = array(); @@ -78,7 +78,7 @@ class PrototypedArrayNodeTest extends \PHPUnit_Framework_TestCase $node->setKeyAttribute('id', true); // each item under the root is an array, with one scalar item - $prototype= new ArrayNode(null, $node); + $prototype = new ArrayNode(null, $node); $prototype->addChild(new ScalarNode('foo')); $node->setPrototype($prototype); @@ -101,7 +101,7 @@ class PrototypedArrayNodeTest extends \PHPUnit_Framework_TestCase $node->setKeyAttribute('id', false); // each item under the root is an array, with two scalar items - $prototype= new ArrayNode(null, $node); + $prototype = new ArrayNode(null, $node); $prototype->addChild(new ScalarNode('foo')); $prototype->addChild(new ScalarNode('id')); // the key attribute will remain $node->setPrototype($prototype); @@ -114,4 +114,68 @@ class PrototypedArrayNodeTest extends \PHPUnit_Framework_TestCase $expected['item_name'] = array('id' => 'item_name', 'foo' => 'bar'); $this->assertEquals($expected, $normalized); } + + public function testAddDefaultChildren() + { + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setAddChildrenIfNoneSet(); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array(array('foo' => 'bar')), $node->getDefaultValue()); + + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setKeyAttribute('foobar'); + $node->setAddChildrenIfNoneSet(); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array('defaults' => array('foo' => 'bar')), $node->getDefaultValue()); + + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setKeyAttribute('foobar'); + $node->setAddChildrenIfNoneSet('defaultkey'); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array('defaultkey' => array('foo' => 'bar')), $node->getDefaultValue()); + + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setKeyAttribute('foobar'); + $node->setAddChildrenIfNoneSet(array('defaultkey')); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array('defaultkey' => array('foo' => 'bar')), $node->getDefaultValue()); + + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setKeyAttribute('foobar'); + $node->setAddChildrenIfNoneSet(array('dk1', 'dk2')); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array('dk1' => array('foo' => 'bar'), 'dk2' => array('foo' => 'bar')), $node->getDefaultValue()); + + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setAddChildrenIfNoneSet(array(5, 6)); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array(0 => array('foo' => 'bar'), 1 => array('foo' => 'bar')), $node->getDefaultValue()); + + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setAddChildrenIfNoneSet(2); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array(array('foo' => 'bar'), array('foo' => 'bar')), $node->getDefaultValue()); + } + + public function testDefaultChildrenWinsOverDefaultValue() + { + $node = $this->getPrototypeNodeWithDefaultChildren(); + $node->setAddChildrenIfNoneSet(); + $node->setDefaultValue(array('bar' => 'foo')); + $this->assertTrue($node->hasDefaultValue()); + $this->assertEquals(array(array('foo' => 'bar')), $node->getDefaultValue()); + } + + protected function getPrototypeNodeWithDefaultChildren() + { + $node = new PrototypedArrayNode('root'); + $prototype = new ArrayNode(null, $node); + $child = new ScalarNode('foo'); + $child->setDefaultValue('bar'); + $prototype->addChild($child); + $prototype->setAddIfNotSet(true); + $node->setPrototype($prototype); + + return $node; + } }