diff --git a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php
index ae1a76663a..5c0fc1c603 100644
--- a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php
+++ b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php
@@ -29,6 +29,10 @@ class PrototypedArrayNode extends ArrayNode
protected $minNumberOfElements = 0;
protected $defaultValue = array();
protected $defaultChildren;
+ /**
+ * @var NodeInterface[] An array of the prototypes of the simplified value children
+ */
+ private $valuePrototypes = array();
/**
* Sets the minimum number of elements that a prototype based node must
@@ -194,9 +198,9 @@ class PrototypedArrayNode extends ArrayNode
}
foreach ($value as $k => $v) {
- $this->prototype->setName($k);
+ $prototype = $this->getPrototypeForChild($k);
try {
- $value[$k] = $this->prototype->finalize($v);
+ $value[$k] = $prototype->finalize($v);
} catch (UnsetKeyException $e) {
unset($value[$k]);
}
@@ -250,8 +254,18 @@ class PrototypedArrayNode extends ArrayNode
}
// if only "value" is left
- if (1 == count($v) && isset($v['value'])) {
+ if (array_keys($v) === array('value')) {
$v = $v['value'];
+ if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && array_key_exists('value', $children)) {
+ $valuePrototype = current($this->valuePrototypes) ?: clone($children['value']);
+ $valuePrototype->parent = $this;
+ $originalClosures = $this->prototype->normalizationClosures;
+ if (is_array($originalClosures)) {
+ $valuePrototypeClosures = $valuePrototype->normalizationClosures;
+ $valuePrototype->normalizationClosures = is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures;
+ }
+ $this->valuePrototypes[$k] = $valuePrototype;
+ }
}
}
@@ -264,11 +278,11 @@ class PrototypedArrayNode extends ArrayNode
}
}
- $this->prototype->setName($k);
+ $prototype = $this->getPrototypeForChild($k);
if (null !== $this->keyAttribute || $isAssoc) {
- $normalized[$k] = $this->prototype->normalize($v);
+ $normalized[$k] = $prototype->normalize($v);
} else {
- $normalized[] = $this->prototype->normalize($v);
+ $normalized[] = $prototype->normalize($v);
}
}
@@ -322,10 +336,54 @@ class PrototypedArrayNode extends ArrayNode
continue;
}
- $this->prototype->setName($k);
- $leftSide[$k] = $this->prototype->merge($leftSide[$k], $v);
+ $prototype = $this->getPrototypeForChild($k);
+ $leftSide[$k] = $prototype->merge($leftSide[$k], $v);
}
return $leftSide;
}
+
+ /**
+ * Returns a prototype for the child node that is associated to $key in the value array.
+ * For general child nodes, this will be $this->prototype.
+ * But if $this->removeKeyAttribute is true and there are only two keys in the child node:
+ * one is same as this->keyAttribute and the other is 'value', then the prototype will be different.
+ *
+ * For example, assume $this->keyAttribute is 'name' and the value array is as follows:
+ * array(
+ * array(
+ * 'name' => 'name001',
+ * 'value' => 'value001'
+ * )
+ * )
+ *
+ * Now, the key is 0 and the child node is:
+ * array(
+ * 'name' => 'name001',
+ * 'value' => 'value001'
+ * )
+ *
+ * When normalizing the value array, the 'name' element will removed from the child node
+ * and its value becomes the new key of the child node:
+ * array(
+ * 'name001' => array('value' => 'value001')
+ * )
+ *
+ * Now only 'value' element is left in the child node which can be further simplified into a string:
+ * array('name001' => 'value001')
+ *
+ * Now, the key becomes 'name001' and the child node becomes 'value001' and
+ * the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance.
+ *
+ * @param string $key The key of the child node
+ *
+ * @return mixed The prototype instance
+ */
+ private function getPrototypeForChild($key)
+ {
+ $prototype = isset($this->valuePrototypes[$key]) ? $this->valuePrototypes[$key] : $this->prototype;
+ $prototype->setName($key);
+
+ return $prototype;
+ }
}
diff --git a/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php b/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php
index c343fcfd94..77013a14b2 100644
--- a/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php
+++ b/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php
@@ -14,6 +14,7 @@ namespace Symfony\Component\Config\Tests\Definition;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;
+use Symfony\Component\Config\Definition\VariableNode;
class PrototypedArrayNodeTest extends \PHPUnit_Framework_TestCase
{
@@ -177,4 +178,164 @@ class PrototypedArrayNodeTest extends \PHPUnit_Framework_TestCase
return $node;
}
+
+ /**
+ * Tests that when a key attribute is mapped, that key is removed from the array.
+ * And if only 'value' element is left in the array, it will replace its wrapper array.
+ *
+ *
+ *
+ *
+ * The above should finally be mapped to an array that looks like this
+ * (because "id" is the key attribute).
+ *
+ * array(
+ * 'things' => array(
+ * 'option1' => 'value1'
+ * )
+ * )
+ *
+ * It's also possible to mix 'value-only' and 'non-value-only' elements in the array.
+ *
+ *
+ *
+ *
+ * The above should finally be mapped to an array as follows
+ *
+ * array(
+ * 'things' => array(
+ * 'option1' => 'value1',
+ * 'option2' => array(
+ * 'value' => 'value2',
+ * 'foo' => 'foo2'
+ * )
+ * )
+ * )
+ *
+ * The 'value' element can also be ArrayNode:
+ *
+ *
+ *
+ *
+ *
+ * The above should be finally be mapped to an array as follows
+ *
+ * array(
+ * 'things' => array(
+ * 'option1' => array(
+ * 'foo' => 'foo1',
+ * 'bar' => 'bar1'
+ * )
+ * )
+ * )
+ *
+ * If using VariableNode for value node, it's also possible to mix different types of value nodes:
+ *
+ *
+ *
+ *
+ *
+ * The above should be finally mapped to an array as follows
+ *
+ * array(
+ * 'things' => array(
+ * 'option1' => array(
+ * 'foo' => 'foo1',
+ * 'bar' => 'bar1'
+ * ),
+ * 'option2' => 'value2'
+ * )
+ * )
+ *
+ *
+ * @dataProvider getDataForKeyRemovedLeftValueOnly
+ */
+ public function testMappedAttributeKeyIsRemovedLeftValueOnly($value, $children, $expected)
+ {
+ $node = new PrototypedArrayNode('root');
+ $node->setKeyAttribute('id', true);
+
+ // each item under the root is an array, with one scalar item
+ $prototype = new ArrayNode(null, $node);
+ $prototype->addChild(new ScalarNode('id'));
+ $prototype->addChild(new ScalarNode('foo'));
+ $prototype->addChild($value);
+ $node->setPrototype($prototype);
+
+ $normalized = $node->normalize($children);
+ $this->assertEquals($expected, $normalized);
+ }
+
+ public function getDataForKeyRemovedLeftValueOnly()
+ {
+ $scalarValue = new ScalarNode('value');
+
+ $arrayValue = new ArrayNode('value');
+ $arrayValue->addChild(new ScalarNode('foo'));
+ $arrayValue->addChild(new ScalarNode('bar'));
+
+ $variableValue = new VariableNode('value');
+
+ return array(
+ array(
+ $scalarValue,
+ array(
+ array('id' => 'option1', 'value' => 'value1'),
+ ),
+ array('option1' => 'value1'),
+ ),
+
+ array(
+ $scalarValue,
+ array(
+ array('id' => 'option1', 'value' => 'value1'),
+ array('id' => 'option2', 'value' => 'value2', 'foo' => 'foo2'),
+ ),
+ array(
+ 'option1' => 'value1',
+ 'option2' => array('value' => 'value2', 'foo' => 'foo2'),
+ ),
+ ),
+
+ array(
+ $arrayValue,
+ array(
+ array(
+ 'id' => 'option1',
+ 'value' => array('foo' => 'foo1', 'bar' => 'bar1'),
+ ),
+ ),
+ array(
+ 'option1' => array('foo' => 'foo1', 'bar' => 'bar1'),
+ ),
+ ),
+
+ array($variableValue,
+ array(
+ array(
+ 'id' => 'option1', 'value' => array('foo' => 'foo1', 'bar' => 'bar1'),
+ ),
+ array('id' => 'option2', 'value' => 'value2'),
+ ),
+ array(
+ 'option1' => array('foo' => 'foo1', 'bar' => 'bar1'),
+ 'option2' => 'value2',
+ ),
+ ),
+ );
+ }
}