diff --git a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php index ae1a76663a..1c3c218832 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', + ), + ), + ); + } } diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php index 5dcf787c99..aaa6fdac25 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AbstractVoter.php @@ -30,7 +30,7 @@ abstract class AbstractVoter implements VoterInterface */ public function supportsAttribute($attribute) { - return in_array($attribute, $this->getSupportedAttributes()); + return in_array($attribute, $this->getSupportedAttributes(), true); } /** diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AbstractVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AbstractVoterTest.php index b537c1b2ef..7c60007d98 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AbstractVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AbstractVoterTest.php @@ -18,6 +18,9 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; */ class AbstractVoterTest extends \PHPUnit_Framework_TestCase { + /** + * @var TokenInterface + */ protected $token; protected function setUp() @@ -25,6 +28,9 @@ class AbstractVoterTest extends \PHPUnit_Framework_TestCase $this->token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); } + /** + * @return array + */ public function getTests() { return array( @@ -55,4 +61,69 @@ class AbstractVoterTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedVote, $voter->vote($this->token, $object, $attributes), $message); } + + /** + * @return array + */ + public function getSupportsAttributeData() + { + return array( + 'positive_string_edit' => array( + 'expected' => true, + 'attribute' => 'EDIT', + 'message' => 'expected TRUE given as attribute EDIT is supported', + ), + 'positive_string_create' => array( + 'expected' => true, + 'attribute' => 'CREATE', + 'message' => 'expected TRUE as given attribute CREATE is supported', + ), + + 'negative_string_read' => array( + 'expected' => false, + 'attribute' => 'READ', + 'message' => 'expected FALSE as given attribute READ is not supported', + ), + 'negative_string_random' => array( + 'expected' => false, + 'attribute' => 'random', + 'message' => 'expected FALSE as given attribute "random" is not supported', + ), + 'negative_string_0' => array( + 'expected' => false, + 'attribute' => '0', + 'message' => 'expected FALSE as given attribute "0" is not supported', + ), + // this set of data gives false positive if in_array is not used with strict flag set to 'true' + 'negative_int_0' => array( + 'expected' => false, + 'attribute' => 0, + 'message' => 'expected FALSE as given attribute 0 is not string', + ), + 'negative_int_1' => array( + 'expected' => false, + 'attribute' => 1, + 'message' => 'expected FALSE as given attribute 1 is not string', + ), + 'negative_int_7' => array( + 'expected' => false, + 'attribute' => 7, + 'message' => 'expected FALSE as attribute 7 is not string', + ), + ); + } + + /** + * @dataProvider getSupportsAttributeData + * + * @param bool $expected + * @param string $attribute + * @param string $message + */ + public function testSupportsAttribute($expected, $attribute, $message) + { + $voter = new AbstractVoterTest_Voter(); + + $this->assertEquals($expected, $voter->supportsAttribute($attribute), $message); + } } diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf index c7061470dc..ce8db48639 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf @@ -310,6 +310,10 @@ This value does not match the expected {{ charset }} charset. Nilai ini tidak memenuhi set karakter {{ charset }} yang diharapkan. + + This is not a valid Business Identifier Code (BIC). + Ini bukan Business Identifier Code (BIC) yang sah. +