8000 [Config] Improve prototype nodes usability, error messages, extensibility by vicb · Pull Request #3403 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Config] Improve prototype nodes usability, error messages, extensibility #3403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 22, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ private function addTemplatingSection(ArrayNodeDefinition $rootNode)
->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){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ private function addFormSection(ArrayNodeDefinition $rootNode)
->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); })
Expand Down
127 changes: 103 additions & 24 deletions src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php
8000
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition
protected $key;
protected $removeKeyItem;
protected $addDefaults;
protected $addDefaultChildren;
protected $nodeBuilder;

/**
Expand All @@ -42,6 +43,7 @@ public function __construct($name, NodeParentInterface $parent = null)

$this->children = array();
$this->addDefaults = false;
$this->addDefaultChildren = false;
$this->allowNewKeys = true;
$this->atLeastOne = false;
$this->allowEmptyValue = true;
Expand Down Expand Up @@ -98,6 +100,22 @@ public function addDefaultsIfNotSet()
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.
*
Expand Down Expand Up @@ -260,40 +278,21 @@ protected function getNodeBuilder()
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__)
);
}
$node = new ArrayNode($this->name, $this->parent);

if ($this->default) {
throw new InvalidDefinitionException(
sprintf('%s::defaultValue() is not applicable to concrete nodes.', __CLASS__)
);
}
$this->validateConcreteNode($node);

$node = new ArrayNode($this->name, $this->parent);
$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);
}
Expand All @@ -306,6 +305,13 @@ protected function createNode()
$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());
}
Expand Down Expand Up @@ -335,4 +341,77 @@ protected function createNode()
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method seems to miss a check whether "addDefaultChildrenIfNotSet()" is called as it makes only sense for arrays which contain a prototype, no?

{
$path = $node->getPath();

if (null !== $this->key) {
throw new InvalidDefinitionException(
sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s"', $path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vicb What do you think about moving those messages into that exception ? (kinda like Doctrine2 does)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have some example ?
I also would like to keep exception management consistent across the component (should other places be updated).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful (I don't have a strong opinion here).
This would probably imply:

  • Making management consistent across the component,
  • Subclass InvalidDefinitionException (i.e. InvalidArrayNodeDefinitionException)

I think there are already some inconsistencies in the way excs are managed in this component (SPL vs component specific). This might be the subject for an other PR ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be the subject for an other PR ?

Yea, this was moreover look-up after at your last commits with new "errors" along the DI component ;-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stloyd Sf2 never uses the Doctrine way to create the exceptions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof And ? There is no info that Sf2 must not use Doctrine way to create the exceptions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, Symfony must not use the Doctrine way of creating exceptions.

);
}

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)
);
}
}
}
}
28 changes: 28 additions & 0 deletions src/Symfony/Component/Config/Definition/PrototypedArrayNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class PrototypedArrayNode extends ArrayNode
protected $removeKeyAttribute;
protected $minNumberOfElements;
protected $defaultValue;
protected $defaultChildren;

/**
* Constructor.
Expand Down Expand Up @@ -120,13 +121,40 @@ public function hasDefaultValue()
return true;
}

/**
* Adds default children when none are set.
*
* @param integer|string|array|null $children The number of children|The child 10000 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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
{
Expand All @@ -21,7 +22,7 @@ public function testAppendingSomeNode()
$parent = new ArrayNodeDefinition('root');
$child = new ScalarNodeDefinition('child');

$node = $parent
$parent
->children()
->scalarNode('foo')->end()
->scalarNode('bar')->end()
Expand Down Expand Up @@ -49,6 +50,7 @@ public function providePrototypeNodeSpecificCalls()
{
return array(
array('defaultValue', array(array())),
array('addDefaultChildrenIfNoneSet', array()),
array('requiresAtLeastOneElement', array()),
array('useAttributeAsKey', array('foo'))
);
Expand All @@ -64,6 +66,89 @@ public function testConcreteNodeSpecificOption()
$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);
Expand Down
Loading
0