From a3555fbd992accd92e81c43174ffd490ec69c239 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 17:20:08 +0100 Subject: [PATCH] [Validator] Fixed: Objects are not traversed unless they are instances of Traversable --- .../Component/Validator/Constraints/Valid.php | 19 +- .../Validator/Mapping/GenericMetadata.php | 3 +- .../Validator/Mapping/MemberMetadata.php | 19 ++ .../Validator/Mapping/TraversalStrategy.php | 2 + .../Component/Validator/Node/ClassNode.php | 4 +- src/Symfony/Component/Validator/Node/Node.php | 11 +- .../Component/Validator/Node/PropertyNode.php | 4 +- .../Validator/NodeTraverser/NodeTraverser.php | 295 +++++++++++++----- .../Validator/NodeVisitor/NodeValidator.php | 5 +- .../Tests/Validator/AbstractValidatorTest.php | 220 ++++++++++++- .../Tests/Validator/LegacyValidatorTest.php | 35 +++ .../Validator/Validator/AbstractValidator.php | 38 +-- .../Validator/ContextualValidator.php | 4 +- .../Validator/Validator/LegacyValidator.php | 4 +- .../Validator/Validator/Validator.php | 4 +- 15 files changed, 545 insertions(+), 122 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 6e84e9a5f0..9f15fdb04e 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -21,12 +21,27 @@ use Symfony\Component\Validator\Exception\ConstraintDefinitionException; * * @api */ -class Valid extends Traverse +class Valid extends Constraint { + /** + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Traverse} constraint instead. + */ + public $traverse = true; + + /** + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Traverse} constraint instead. + */ + public $deep = false; + public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { - throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint %s', __CLASS__)); + throw new ConstraintDefinitionException(sprintf( + 'The option "groups" is not supported by the constraint %s', + __CLASS__ + )); } parent::__construct($options); diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 75b07c8825..369276f220 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -77,8 +77,7 @@ class GenericMetadata implements MetadataInterface if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; - // Continue. Valid extends Traverse, so the return statement in the - // next block is going be executed. + return $this; } if ($constraint instanceof Traverse) { diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 80a2687458..230af7d89e 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; @@ -58,6 +59,24 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat )); } + // BC with Symfony < 2.5 + // Only process if the traversal strategy was not already set by the + // Traverse constraint + if ($constraint instanceof Valid && !$this->traversalStrategy) { + if (true === $constraint->traverse) { + // Try to traverse cascaded objects, but ignore if they do not + // implement Traversable + $this->traversalStrategy = TraversalStrategy::TRAVERSE + | TraversalStrategy::IGNORE_NON_TRAVERSABLE; + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } + } elseif (false === $constraint->traverse) { + $this->traversalStrategy = TraversalStrategy::NONE; + } + } + parent::addConstraint($constraint); return $this; diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 4a9d8c8aa1..951ec6058d 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -25,6 +25,8 @@ class TraversalStrategy const RECURSIVE = 4; + const IGNORE_NON_TRAVERSABLE = 8; + private function __construct() { } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 07554963d7..904e8651fd 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -37,13 +37,13 @@ class ClassNode extends Node * to this node * @param string[] $groups The groups in which this * node should be validated - * @param string[] $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated * * @throws UnexpectedTypeException If the given value is not an object */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 014665abf4..b301db8dda 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; /** @@ -65,11 +66,17 @@ abstract class Node * this node * @param string[] $groups The groups in which this node * should be validated - * @param string[] $cascadedGroups The groups in which cascaded + * @param string[]|null $cascadedGroups The groups in which cascaded * objects should be validated + * + * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { + if (null !== $cascadedGroups && !is_array($cascadedGroups)) { + throw new UnexpectedTypeException($cascadedGroups, 'null or array'); + } + $this->value = $value; $this->metadata = $metadata; $this->propertyPath = $propertyPath; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index bcb462b176..313da63ab7 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -46,11 +46,11 @@ class PropertyNode extends Node * to this node * @param string[] $groups The groups in which this * node should be validated - * @param string[] $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated */ - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { parent::__construct( $value, diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index d0529af005..d9cbf62c71 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -80,100 +82,219 @@ class NodeTraverser implements NodeTraverserInterface } if ($isTopLevelCall) { - $this->traversalStarted = false; - foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ $visitor->afterTraversal($nodes); } + + $this->traversalStarted = false; + } + } + + /** + * @param Node $node + * + * @return Boolean + */ + private function enterNode(Node $node) + { + $continueTraversal = true; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->enterNode($node)) { + $continueTraversal = false; + + // Continue, so that the enterNode() method of all visitors + // is called + } + } + + return $continueTraversal; + } + + /** + * @param Node $node + */ + private function leaveNode(Node $node) + { + foreach ($this->visitors as $visitor) { + $visitor->leaveNode($node); } } private function traverseNode(Node $node) { - $stopTraversal = false; + $continue = $this->enterNode($node); - foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node)) { - $stopTraversal = true; + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $continue) { + $this->leaveNode($node); + + return; + } + + if (null === $node->value) { + $this->leaveNode($node); + + return; + } + + // The "cascadedGroups" property is set by the NodeValidator when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + $this->leaveNode($node); + + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->metadata->getTraversalStrategy(); + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + + $this->leaveNode($node); + + return; + } + + if ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + + $this->leaveNode($node); + + return; + } + + // Traverse only if the TRAVERSE bit is set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + $this->leaveNode($node); + + return; + } + + if (!$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { + $this->leaveNode($node); + + return; } + + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($node->value) + )); } - // Stop the traversal, but execute the leaveNode() methods anyway to - // perform possible cleanups - if (!$stopTraversal && null !== $node->value) { - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->metadata->getTraversalStrategy(); + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); - if (is_array($node->value)) { - $this->cascadeCollection( - $node->value, - $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy - ); - } elseif ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $node->value, - $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy - ); - } - } - - foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node); - } + $this->leaveNode($node); } private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) { - $stopTraversal = false; + $continue = $this->enterNode($node); - foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node)) { - $stopTraversal = true; + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $continue) { + $this->leaveNode($node); + + return; + } + + if (0 === count($node->groups)) { + $this->leaveNode($node); + + return; + } + + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + $this->traverseNode(new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + )); } } - // Stop the traversal, but execute the leaveNode() methods anyway to - // perform possible cleanups - if (!$stopTraversal && count($node->groups) > 0) { - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $this->traverseNode(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); - } - } - - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - $traversalStrategy = $node->metadata->getTraversalStrategy(); - } - - if ($traversalStrategy & TraversalStrategy::TRAVERSE) { - $this->cascadeCollection( - $node->value, - $node->propertyPath, - $node->groups, - $traversalStrategy - ); - } + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the class' metadata + if (TraversalStrategy::IMPLICIT === $traversalStrategy) { + $traversalStrategy = $node->metadata->getTraversalStrategy(); } - foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node); + // Traverse only if the TRAVERSE bit is set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + $this->leaveNode($node); + + return; } + + if (!$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { + $this->leaveNode($node); + + return; + } + + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($node->value) + )); + } + + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + + $this->leaveNode($node); } private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) @@ -181,24 +302,31 @@ class NodeTraverser implements NodeTraverserInterface try { $classMetadata = $this->metadataFactory->getMetadataFor($object); + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + $classNode = new ClassNode( $object, $classMetadata, $propertyPath, - $groups, $groups ); $this->traverseClassNode($classNode, $traversalStrategy); } catch (NoSuchMetadataException $e) { - if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Rethrow if the TRAVERSE bit is not set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { throw $e; } - // Metadata doesn't necessarily have to exist for - // traversable objects, because we know how to validate - // them anyway. - $this->cascadeCollection( + // Rethrow if the object does not implement Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + // In that case, iterate the object and cascade each entry + $this->cascadeEachObjectIn( $object, $propertyPath, $groups, @@ -207,15 +335,25 @@ class NodeTraverser implements NodeTraverserInterface } } - private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy) { - if (!($traversalStrategy & TraversalStrategy::RECURSIVE)) { + if ($traversalStrategy & TraversalStrategy::RECURSIVE) { + // Try to traverse nested objects, but ignore if they do not + // implement Traversable + $traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE; + } else { + // If the RECURSIVE bit is not set, change the strategy to IMPLICIT + // in order to respect the metadata's traversal strategy of each entry + // in the collection $traversalStrategy = TraversalStrategy::IMPLICIT; } foreach ($collection as $key => $value) { if (is_array($value)) { - $this->cascadeCollection( + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeEachObjectIn( $value, $propertyPath.'['.$key.']', $groups, @@ -226,6 +364,7 @@ class NodeTraverser implements NodeTraverserInterface } // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) if (is_object($value)) { $this->cascadeObject( $value, diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 100c9b7d3b..6ee8b7f826 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -146,7 +146,10 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface foreach ($groupSequence->groups as $groupInSequence) { $node = clone $node; $node->groups = array($groupInSequence); - $node->cascadedGroups = array($groupSequence->cascadedGroup ?: $groupInSequence); + + if (null !== $groupSequence->cascadedGroup) { + $node->cascadedGroups = array($groupSequence->cascadedGroup); + } $this->nodeTraverser->traverse(array($node)); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 60e5ebf76e..36b13cfd96 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Validator\Tests\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; @@ -225,6 +226,45 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testArrayLegacyApi() + { + $test = $this; + $entity = new Entity(); + $array = array('key' => $entity); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ @@ -264,6 +304,45 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testRecursiveArrayLegacyApi() + { + $test = $this; + $entity = new Entity(); + $array = array(2 => array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ @@ -278,17 +357,24 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testTraversableTraverseDisabled() + public function testTraversableTraverseEnabled() { $test = $this; $entity = new Entity(); $traversable = new \ArrayIterator(array('key' => $entity)); - $callback = function () use ($test) { - $test->fail('Should not be called'); + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); }; $this->metadata->addConstraint(new Callback(array( @@ -296,10 +382,21 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase 'groups' => 'Group', ))); - $this->validator->validate($traversable, 'Group'); + $violations = $this->validator->validateCollection($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); } - public function testTraversableTraverseEnabled() + public function testTraversableTraverseEnabledLegacyApi() { $test = $this; $entity = new Entity(); @@ -338,6 +435,27 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase $this->assertNull($violations[0]->getCode()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + /** * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException */ @@ -358,6 +476,29 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase 'groups' => 'Group', ))); + $this->validator->validateCollection($traversable, 'Group'); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->validator->validate($traversable, 'Group', true); } @@ -388,6 +529,47 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($traversable, 'Group', true, true); /** @var ConstraintViolationInterface[] $violations */ @@ -1675,6 +1857,28 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase $test->assertSame('Separate violation', $violations[0]->getMessage()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverse() + { + $entity = new Entity(); + + $this->validator->validateValue($entity, new Traverse()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverseOnClass() + { + $entity = new Entity(); + + $this->metadata->addConstraint(new Traverse()); + + $this->validator->validate($entity); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index c66cde30ea..0d8ade455c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -55,6 +55,41 @@ class LegacyValidatorTest extends AbstractValidatorTest $this->markTestSkipped('Not supported in the legacy API'); } + public function testArray() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveArray() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testTraversableTraverseEnabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveTraversableRecursiveTraversalEnabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testExpectTraversableIfTraverse() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testExpectTraversableIfTraverseOnClass() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + /** * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index d4d4e62e9c..8cd2767bd0 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -72,6 +72,25 @@ abstract class AbstractValidator implements ValidatorInterface return $this->metadataFactory->hasMetadataFor($object); } + protected function traverse($value, $constraints, $groups = null) + { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->nodeTraverser->traverse(array(new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups, + $groups + ))); + } + protected function traverseObject($object, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -158,25 +177,6 @@ abstract class AbstractValidator implements ValidatorInterface $this->nodeTraverser->traverse($nodes); } - protected function traverseValue($value, $constraints, $groups = null) - { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - protected function normalizeGroups($groups) { if (is_array($groups)) { diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index a3e21863df..b975ef4bde 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -58,7 +58,7 @@ class ContextualValidator extends AbstractValidator implements ContextualValidat */ public function validate($value, $constraints, $groups = null) { - $this->traverseValue($value, $constraints, $groups); + $this->traverse($value, $constraints, $groups); return $this->context->getViolations(); } @@ -89,7 +89,7 @@ class ContextualValidator extends AbstractValidator implements ContextualValidat 'deep' => $deep, )); - $this->traverseValue($collection, $constraint, $groups); + $this->traverse($collection, $constraint, $groups); return $this->context->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 64df69e1e2..73d6948e3e 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -35,7 +35,7 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface 'deep' => $deep, )); - return $this->validateValue($value, $constraint, $groups); + return parent::validate($value, $constraint, $groups); } if ($traverse && $value instanceof \Traversable) { @@ -44,7 +44,7 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface new Traverse(array('traverse' => true, 'deep' => $deep)), ); - return $this->validateValue($value, $constraints, $groups); + return parent::validate($value, $constraints, $groups); } return $this->validateObject($value, $groups); diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 5d8b509be3..94857a3a1b 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -38,7 +38,7 @@ class Validator extends AbstractValidator { $this->contextManager->startContext($value); - $this->traverseValue($value, $constraints, $groups); + $this->traverse($value, $constraints, $groups); return $this->contextManager->stopContext()->getViolations(); } @@ -61,7 +61,7 @@ class Validator extends AbstractValidator 'deep' => $deep, )); - $this->traverseValue($collection, $constraint, $groups); + $this->traverse($collection, $constraint, $groups); return $this->contextManager->stopContext()->getViolations(); }