[Validator] Made traversal of Traversables consistent

If the traversal strategy is IMPLICIT (the default), the validator will now
traverse any object that implements \Traversable and any array
This commit is contained in:
Bernhard Schussek 2014-02-20 19:10:26 +01:00
parent 117b1b9a17
commit 51197f68a3
21 changed files with 247 additions and 231 deletions

View File

@ -51,6 +51,6 @@ class Traverse extends Constraint
*/ */
public function getTargets() public function getTargets()
{ {
return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); return self::CLASS_CONSTRAINT;
} }
} }

View File

@ -23,16 +23,8 @@ use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
*/ */
class Valid extends Constraint 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; 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 $deep = false;
public function __construct($options = null) public function __construct($options = null)

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Mapping; namespace Symfony\Component\Validator\Mapping;
use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\ValidationVisitorInterface;
use Symfony\Component\Validator\PropertyMetadataContainerInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface;
@ -190,6 +191,27 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface,
)); ));
} }
if ($constraint instanceof Traverse) {
if (true === $constraint->traverse) {
// If traverse is true, traversal should be explicitly enabled
$this->traversalStrategy = TraversalStrategy::TRAVERSE;
if (!$constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
}
} elseif (false === $constraint->traverse) {
// If traverse is false, traversal should be explicitly disabled
$this->traversalStrategy = TraversalStrategy::NONE;
} else {
// Else, traverse depending on the contextual information that
// is available during validation
$this->traversalStrategy = TraversalStrategy::IMPLICIT;
}
// The constraint is not added
return $this;
}
$constraint->addImplicitGroupName($this->getDefaultGroup()); $constraint->addImplicitGroupName($this->getDefaultGroup());
parent::addConstraint($constraint); parent::addConstraint($constraint);

View File

@ -1,48 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Validator\Mapping;
/**
* @since %%NextVersion%%
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CollectionMetadata implements MetadataInterface
{
private $traversalStrategy;
public function __construct($traversalStrategy)
{
$this->traversalStrategy = $traversalStrategy;
}
/**
* Returns all constraints for a given validation group.
*
* @param string $group The validation group.
*
* @return \Symfony\Component\Validator\Constraint[] A list of constraint instances.
*/
public function findConstraints($group)
{
return array();
}
public function getCascadingStrategy()
{
return CascadingStrategy::NONE;
}
public function getTraversalStrategy()
{
return $this->traversalStrategy;
}
}

View File

@ -77,27 +77,17 @@ class GenericMetadata implements MetadataInterface
if ($constraint instanceof Valid) { if ($constraint instanceof Valid) {
$this->cascadingStrategy = CascadingStrategy::CASCADE; $this->cascadingStrategy = CascadingStrategy::CASCADE;
return $this; if ($constraint->traverse) {
} // Traverse unless the value is not traversable
if ($constraint instanceof Traverse) {
if (true === $constraint->traverse) {
// If traverse is true, traversal should be explicitly enabled
$this->traversalStrategy = TraversalStrategy::TRAVERSE;
if ($constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::RECURSIVE;
}
} elseif (false === $constraint->traverse) {
// If traverse is false, traversal should be explicitly disabled
$this->traversalStrategy = TraversalStrategy::NONE;
} else {
// Else, traverse depending on the contextual information that
// is available during validation
$this->traversalStrategy = TraversalStrategy::IMPLICIT; $this->traversalStrategy = TraversalStrategy::IMPLICIT;
if (!$constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
}
} else {
$this->traversalStrategy = TraversalStrategy::NONE;
} }
// The constraint is not added
return $this; return $this;
} }

View File

@ -60,17 +60,14 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
} }
// BC with Symfony < 2.5 // BC with Symfony < 2.5
// Only process if the traversal strategy was not already set by the if ($constraint instanceof Valid) {
// Traverse constraint
if ($constraint instanceof Valid && !$this->traversalStrategy) {
if (true === $constraint->traverse) { if (true === $constraint->traverse) {
// Try to traverse cascaded objects, but ignore if they do not // Try to traverse cascaded objects, but ignore if they do not
// implement Traversable // implement Traversable
$this->traversalStrategy = TraversalStrategy::TRAVERSE $this->traversalStrategy = TraversalStrategy::IMPLICIT;
| TraversalStrategy::IGNORE_NON_TRAVERSABLE;
if ($constraint->deep) { if (!$constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::RECURSIVE; $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
} }
} elseif (false === $constraint->traverse) { } elseif (false === $constraint->traverse) {
$this->traversalStrategy = TraversalStrategy::NONE; $this->traversalStrategy = TraversalStrategy::NONE;
@ -180,7 +177,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
*/ */
public function isCollectionCascaded() public function isCollectionCascaded()
{ {
return (boolean) ($this->traversalStrategy & TraversalStrategy::TRAVERSE); return (boolean) ($this->traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE));
} }
/** /**
@ -191,7 +188,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
*/ */
public function isCollectionCascadedDeeply() public function isCollectionCascadedDeeply()
{ {
return (boolean) ($this->traversalStrategy & TraversalStrategy::RECURSIVE); return !($this->traversalStrategy & TraversalStrategy::STOP_RECURSION);
} }
/** /**

View File

@ -17,15 +17,16 @@ namespace Symfony\Component\Validator\Mapping;
*/ */
class TraversalStrategy class TraversalStrategy
{ {
const IMPLICIT = 0; /**
* @var integer
*/
const IMPLICIT = 1;
const NONE = 1; const NONE = 2;
const TRAVERSE = 2; const TRAVERSE = 4;
const RECURSIVE = 4; const STOP_RECURSION = 8;
const IGNORE_NON_TRAVERSABLE = 8;
private function __construct() private function __construct()
{ {

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/** /**
* Represents an object and its class metadata in the validation graph. * Represents an object and its class metadata in the validation graph.
@ -40,10 +41,11 @@ class ClassNode extends Node
* @param string[]|null $cascadedGroups The groups in which * @param string[]|null $cascadedGroups The groups in which
* cascaded objects should be * cascaded objects should be
* validated * validated
* @param integer $traversalStrategy
* *
* @throws UnexpectedTypeException If the given value is not an object * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
*/ */
public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
{ {
if (!is_object($object)) { if (!is_object($object)) {
throw new UnexpectedTypeException($object, 'object'); throw new UnexpectedTypeException($object, 'object');
@ -56,5 +58,7 @@ class ClassNode extends Node
$groups, $groups,
$cascadedGroups $cascadedGroups
); );
$this->traversalStrategy = $traversalStrategy;
} }
} }

View File

@ -11,8 +11,10 @@
namespace Symfony\Component\Validator\Node; namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\MetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/** /**
* Represents an traversable collection in the validation graph. * Represents an traversable collection in the validation graph.
@ -25,32 +27,35 @@ class CollectionNode extends Node
/** /**
* Creates a new collection node. * Creates a new collection node.
* *
* @param array|\Traversable $collection The validated collection * @param array|\Traversable $collection The validated collection
* @param MetadataInterface $metadata The class metadata of that * @param string $propertyPath The property path leading
* object
* @param string $propertyPath The property path leading
* to this node * to this node
* @param string[] $groups The groups in which this * @param string[] $groups The groups in which this
* node should be validated * node should be validated
* @param string[]|null $cascadedGroups The groups in which * @param string[]|null $cascadedGroups The groups in which
* cascaded objects should be * cascaded objects should be
* validated * validated
* @param integer $traversalStrategy The traversal strategy
* *
* @throws UnexpectedTypeException If the given value is not an array or * @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* an instance of {@link \Traversable}
*/ */
public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE)
{ {
if (!is_array($collection) && !$collection instanceof \Traversable) { if (!is_array($collection) && !$collection instanceof \Traversable) {
throw new UnexpectedTypeException($collection, 'object'); throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($collection)
));
} }
parent::__construct( parent::__construct(
$collection, $collection,
$metadata, null,
$propertyPath, $propertyPath,
$groups, $groups,
$cascadedGroups $cascadedGroups,
$traversalStrategy
); );
} }
} }

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\MetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/** /**
* A node in the validated graph. * A node in the validated graph.
@ -32,7 +33,7 @@ abstract class Node
/** /**
* The metadata specifying how the value should be validated. * The metadata specifying how the value should be validated.
* *
* @var MetadataInterface * @var MetadataInterface|null
*/ */
public $metadata; public $metadata;
@ -57,21 +58,27 @@ abstract class Node
*/ */
public $cascadedGroups; public $cascadedGroups;
/**
* @var integer
*/
public $traversalStrategy;
/** /**
* Creates a new property node. * Creates a new property node.
* *
* @param mixed $value The property value * @param mixed $value The property value
* @param MetadataInterface $metadata The property's metadata * @param MetadataInterface|null $metadata The property's metadata
* @param string $propertyPath The property path leading to * @param string $propertyPath The property path leading to
* this node * this node
* @param string[] $groups The groups in which this node * @param string[] $groups The groups in which this node
* should be validated * should be validated
* @param string[]|null $cascadedGroups The groups in which cascaded * @param string[]|null $cascadedGroups The groups in which cascaded
* objects should be validated * objects should be validated
* @param integer $traversalStrategy
* *
* @throws UnexpectedTypeException If $cascadedGroups is invalid * @throws UnexpectedTypeException If $cascadedGroups is invalid
*/ */
public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
{ {
if (null !== $cascadedGroups && !is_array($cascadedGroups)) { if (null !== $cascadedGroups && !is_array($cascadedGroups)) {
throw new UnexpectedTypeException($cascadedGroups, 'null or array'); throw new UnexpectedTypeException($cascadedGroups, 'null or array');
@ -82,5 +89,6 @@ abstract class Node
$this->propertyPath = $propertyPath; $this->propertyPath = $propertyPath;
$this->groups = $groups; $this->groups = $groups;
$this->cascadedGroups = $cascadedGroups; $this->cascadedGroups = $cascadedGroups;
$this->traversalStrategy = $traversalStrategy;
} }
} }

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/** /**
* Represents the value of a property and its associated metadata. * Represents the value of a property and its associated metadata.
@ -57,10 +58,11 @@ class PropertyNode extends Node
* @param string[]|null $cascadedGroups The groups in which * @param string[]|null $cascadedGroups The groups in which
* cascaded objects should * cascaded objects should
* be validated * be validated
* @param integer $traversalStrategy
* *
* @throws UnexpectedTypeException If $object is not an object * @throws UnexpectedTypeException If $object is not an object
*/ */
public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
{ {
if (!is_object($object)) { if (!is_object($object)) {
throw new UnexpectedTypeException($object, 'object'); throw new UnexpectedTypeException($object, 'object');
@ -71,7 +73,8 @@ class PropertyNode extends Node
$metadata, $metadata,
$propertyPath, $propertyPath,
$groups, $groups,
$cascadedGroups $cascadedGroups,
$traversalStrategy
); );
$this->object = $object; $this->object = $object;

View File

@ -117,17 +117,17 @@ class NodeTraverser implements NodeTraverserInterface
private function traverseNode(Node $node, Traversal $traversal) private function traverseNode(Node $node, Traversal $traversal)
{ {
// 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 === $this->visit($node, $traversal->context)) { if (false === $this->visit($node, $traversal->context)) {
return; return;
} }
// Visitors have two possibilities to influence the traversal:
//
// 1. If a visitor's visit() method returns false, the traversal is
// skipped entirely.
// 2. If a visitor's visit() method removes a group from the node,
// that group will be skipped in the subtree of that node.
if (null === $node->value) { if (null === $node->value) {
return; return;
} }
@ -151,9 +151,10 @@ class NodeTraverser implements NodeTraverserInterface
// (BC with Symfony < 2.5) // (BC with Symfony < 2.5)
$traversal->nodeQueue->enqueue(new CollectionNode( $traversal->nodeQueue->enqueue(new CollectionNode(
$node->value, $node->value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath, $node->propertyPath,
$cascadedGroups $cascadedGroups,
null,
$traversalStrategy
)); ));
return; return;
@ -174,38 +175,28 @@ class NodeTraverser implements NodeTraverserInterface
return; return;
} }
// Traverse only if the TRAVERSE bit is set // Traverse only if IMPLICIT or TRAVERSE
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
return; return;
} }
if (!$node->value instanceof \Traversable) { // If IMPLICIT, stop unless we deal with a Traversable
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) {
return; return;
}
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($node->value)
));
} }
// If TRAVERSE, the constructor will fail if we have no Traversable
$traversal->nodeQueue->enqueue(new CollectionNode( $traversal->nodeQueue->enqueue(new CollectionNode(
$node->value, $node->value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath, $node->propertyPath,
$node->groups, $cascadedGroups,
$node->cascadedGroups null,
$traversalStrategy
)); ));
} }
private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) private function traverseClassNode(ClassNode $node, Traversal $traversal)
{ {
if (false === $this->visit($node, $traversal->context)) {
return;
}
// Visitors have two possibilities to influence the traversal: // Visitors have two possibilities to influence the traversal:
// //
// 1. If a visitor's enterNode() method returns false, the traversal is // 1. If a visitor's enterNode() method returns false, the traversal is
@ -213,6 +204,10 @@ class NodeTraverser implements NodeTraverserInterface
// 2. If a visitor's enterNode() method removes a group from the node, // 2. If a visitor's enterNode() method removes a group from the node,
// that group will be skipped in the subtree of that node. // that group will be skipped in the subtree of that node.
if (false === $this->visit($node, $traversal->context)) {
return;
}
if (0 === count($node->groups)) { if (0 === count($node->groups)) {
return; return;
} }
@ -232,54 +227,58 @@ class NodeTraverser implements NodeTraverserInterface
} }
} }
$traversalStrategy = $node->traversalStrategy;
// If no specific traversal strategy was requested when this method // If no specific traversal strategy was requested when this method
// was called, use the traversal strategy of the class' metadata // was called, use the traversal strategy of the class' metadata
if (TraversalStrategy::IMPLICIT === $traversalStrategy) { if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
$traversalStrategy = $node->metadata->getTraversalStrategy(); // Keep the STOP_RECURSION flag, if it was set
$traversalStrategy = $node->metadata->getTraversalStrategy()
| ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
} }
// Traverse only if the TRAVERSE bit is set // Traverse only if IMPLICIT or TRAVERSE
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
return; return;
} }
if (!$node->value instanceof \Traversable) { // If IMPLICIT, stop unless we deal with a Traversable
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) {
return; return;
}
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($node->value)
));
} }
// If TRAVERSE, the constructor will fail if we have no Traversable
$traversal->nodeQueue->enqueue(new CollectionNode( $traversal->nodeQueue->enqueue(new CollectionNode(
$node->value, $node->value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath, $node->propertyPath,
$node->groups, $node->groups,
$node->cascadedGroups $node->cascadedGroups,
$traversalStrategy
)); ));
} }
private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) private function traverseCollectionNode(CollectionNode $node, Traversal $traversal)
{ {
// 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 === $this->visit($node, $traversal->context)) { if (false === $this->visit($node, $traversal->context)) {
return; return;
} }
$traversalStrategy = $node->metadata->getTraversalStrategy(); if (0 === count($node->groups)) {
return;
}
if ($traversalStrategy & TraversalStrategy::RECURSIVE) { $traversalStrategy = $node->traversalStrategy;
// Try to traverse nested objects, but ignore if they do not
// implement Traversable if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) {
$traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE; $traversalStrategy = TraversalStrategy::NONE;
} else { } 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; $traversalStrategy = TraversalStrategy::IMPLICIT;
} }
@ -290,9 +289,10 @@ class NodeTraverser implements NodeTraverserInterface
// (BC with Symfony < 2.5) // (BC with Symfony < 2.5)
$traversal->nodeQueue->enqueue(new CollectionNode( $traversal->nodeQueue->enqueue(new CollectionNode(
$value, $value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath.'['.$key.']', $node->propertyPath.'['.$key.']',
$node->groups $node->groups,
null,
$traversalStrategy
)); ));
continue; continue;
@ -325,24 +325,27 @@ class NodeTraverser implements NodeTraverserInterface
$object, $object,
$classMetadata, $classMetadata,
$propertyPath, $propertyPath,
$groups $groups,
null,
$traversalStrategy
)); ));
} catch (NoSuchMetadataException $e) { } catch (NoSuchMetadataException $e) {
// Rethrow if the TRAVERSE bit is not set // Rethrow if not Traversable
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { if (!$object instanceof \Traversable) {
throw $e; throw $e;
} }
// Rethrow if the object does not implement Traversable // Rethrow unless IMPLICIT or TRAVERSE
if (!$object instanceof \Traversable) { if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
throw $e; throw $e;
} }
$traversal->nodeQueue->enqueue(new CollectionNode( $traversal->nodeQueue->enqueue(new CollectionNode(
$object, $object,
new CollectionMetadata($traversalStrategy),
$propertyPath, $propertyPath,
$groups $groups,
null,
$traversalStrategy
)); ));
} }
} }

View File

@ -16,6 +16,7 @@ use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Group\GroupManagerInterface;
use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
@ -56,6 +57,10 @@ class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterf
public function visit(Node $node, ExecutionContextInterface $context) public function visit(Node $node, ExecutionContextInterface $context)
{ {
if ($node instanceof CollectionNode) {
return true;
}
if ($node instanceof ClassNode) { if ($node instanceof ClassNode) {
$objectHash = spl_object_hash($node->value); $objectHash = spl_object_hash($node->value);
} elseif ($node instanceof PropertyNode) { } elseif ($node instanceof PropertyNode) {

View File

@ -17,6 +17,7 @@ use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Tests\Fixtures\Reference; use Symfony\Component\Validator\Tests\Fixtures\Reference;
@ -354,14 +355,43 @@ abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest
$this->assertNull($violations[0]->getCode()); $this->assertNull($violations[0]->getCode());
} }
/** public function testTraverseTraversableByDefault()
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testExpectTraversableIfTraverse()
{ {
$test = $this;
$entity = new Entity(); $entity = new Entity();
$traversable = new \ArrayIterator(array('key' => $entity));
$this->validator->validate($entity, new Traverse()); $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($traversable, $context->getRoot());
$test->assertSame($entity, $context->getValue());
$test->assertSame($entity, $value);
$context->addViolation('Message %param%', array('%param%' => 'value'));
};
$this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator'));
$this->metadata->addConstraint(new Callback(array(
'callback' => $callback,
'groups' => 'Group',
)));
$violations = $this->validateObject($traversable, '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($traversable, $violations[0]->getRoot());
$this->assertSame($entity, $violations[0]->getInvalidValue());
$this->assertNull($violations[0]->getMessagePluralization());
$this->assertNull($violations[0]->getCode());
} }
/** /**

View File

@ -93,6 +93,29 @@ abstract class AbstractLegacyApiTest extends AbstractValidatorTest
$this->validator->validate($traversable, 'Group'); $this->validator->validate($traversable, 'Group');
} }
/**
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
public function testRecursiveTraversableRecursiveTraversalDisabled()
{
$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');
}
public function testValidateInContext() public function testValidateInContext()
{ {
$test = $this; $test = $this;

View File

@ -339,30 +339,7 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($violations[0]->getCode()); $this->assertNull($violations[0]->getCode());
} }
/** public function testRecursiveTraversable()
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
public function testRecursiveTraversableRecursiveTraversalDisabled()
{
$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->validateObjects($traversable, 'Group');
}
public function testRecursiveTraversableRecursiveTraversalEnabled()
{ {
$test = $this; $test = $this;
$entity = new Entity(); $entity = new Entity();

View File

@ -14,11 +14,15 @@ namespace Symfony\Component\Validator\Validator;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Mapping\GenericMetadata;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\Node\GenericNode;
use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
@ -110,14 +114,26 @@ class ContextualValidator implements ContextualValidatorInterface
return $this; return $this;
} }
public function validateObjects($objects, $groups = null, $deep = false) public function validateObjects($objects, $groups = null)
{ {
$constraint = new Traverse(array( if (!is_array($objects) && !$objects instanceof \Traversable) {
'traverse' => true, throw new UnexpectedTypeException($objects, 'array or \Traversable');
'deep' => $deep, }
));
return $this->validate($objects, $constraint, $groups); $traversalStrategy = TraversalStrategy::TRAVERSE;
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
$node = new CollectionNode(
$objects,
$this->defaultPropertyPath,
$groups,
null,
$traversalStrategy
);
$this->nodeTraverser->traverse(array($node), $this->context);
return $this;
} }
public function validateProperty($object, $propertyName, $groups = null) public function validateProperty($object, $propertyName, $groups = null)

View File

@ -51,7 +51,7 @@ interface ContextualValidatorInterface
*/ */
public function validateObject($object, $groups = null); public function validateObject($object, $groups = null);
public function validateObjects($objects, $groups = null, $deep = false); public function validateObjects($objects, $groups = null);
/** /**
* Validates a property of a value against its current value. * Validates a property of a value against its current value.

View File

@ -29,24 +29,12 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface
return parent::validate($value, $groups, $traverse); return parent::validate($value, $groups, $traverse);
} }
if (is_array($value)) { if (is_array($value) || ($traverse && $value instanceof \Traversable)) {
$constraint = new Traverse(array( $constraint = new Valid(array('deep' => $deep));
'traverse' => true,
'deep' => $deep,
));
return parent::validate($value, $constraint, $groups); return parent::validate($value, $constraint, $groups);
} }
if ($traverse && $value instanceof \Traversable) {
$constraints = array(
new Valid(),
new Traverse(array('traverse' => true, 'deep' => $deep)),
);
return parent::validate($value, $constraints, $groups);
}
return $this->validateObject($value, $groups); return $this->validateObject($value, $groups);
} }

View File

@ -98,10 +98,10 @@ class Validator implements ValidatorInterface
->getViolations(); ->getViolations();
} }
public function validateObjects($objects, $groups = null, $deep = false) public function validateObjects($objects, $groups = null)
{ {
return $this->startContext($objects) return $this->startContext($objects)
->validateObjects($objects, $groups, $deep) ->validateObjects($objects, $groups)
->getViolations(); ->getViolations();
} }

View File

@ -47,7 +47,7 @@ interface ValidatorInterface
*/ */
public function validateObject($object, $groups = null); public function validateObject($object, $groups = null);
public function validateObjects($objects, $groups = null, $deep = false); public function validateObjects($objects, $groups = null);
/** /**
* Validates a property of a value against its current value. * Validates a property of a value against its current value.