[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()
{
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
{
/**
* @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)

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Mapping;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ValidationVisitorInterface;
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());
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) {
$this->cascadingStrategy = CascadingStrategy::CASCADE;
return $this;
}
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
if ($constraint->traverse) {
// Traverse unless the value is not traversable
$this->traversalStrategy = TraversalStrategy::IMPLICIT;
if (!$constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
}
} else {
$this->traversalStrategy = TraversalStrategy::NONE;
}
// The constraint is not added
return $this;
}

View File

@ -60,17 +60,14 @@ 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 ($constraint instanceof Valid) {
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;
$this->traversalStrategy = TraversalStrategy::IMPLICIT;
if ($constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::RECURSIVE;
if (!$constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
}
} elseif (false === $constraint->traverse) {
$this->traversalStrategy = TraversalStrategy::NONE;
@ -180,7 +177,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
*/
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()
{
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
{
const IMPLICIT = 0;
/**
* @var integer
*/
const IMPLICIT = 1;
const NONE = 1;
const NONE = 2;
const TRAVERSE = 2;
const TRAVERSE = 4;
const RECURSIVE = 4;
const IGNORE_NON_TRAVERSABLE = 8;
const STOP_RECURSION = 8;
private function __construct()
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* 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
* cascaded objects should be
* 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)) {
throw new UnexpectedTypeException($object, 'object');
@ -56,5 +58,7 @@ class ClassNode extends Node
$groups,
$cascadedGroups
);
$this->traversalStrategy = $traversalStrategy;
}
}

View File

@ -11,8 +11,10 @@
namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\MetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* Represents an traversable collection in the validation graph.
@ -25,32 +27,35 @@ class CollectionNode extends Node
/**
* Creates a new collection node.
*
* @param array|\Traversable $collection The validated collection
* @param MetadataInterface $metadata The class metadata of that
* object
* @param string $propertyPath The property path leading
* @param array|\Traversable $collection The validated collection
* @param string $propertyPath The property path leading
* to this node
* @param string[] $groups The groups in which this
* @param string[] $groups The groups in which this
* node should be validated
* @param string[]|null $cascadedGroups The groups in which
* @param string[]|null $cascadedGroups The groups in which
* cascaded objects should be
* validated
* @param integer $traversalStrategy The traversal strategy
*
* @throws UnexpectedTypeException If the given value is not an array or
* an instance of {@link \Traversable}
* @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
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) {
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(
$collection,
$metadata,
null,
$propertyPath,
$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\Mapping\MetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* A node in the validated graph.
@ -32,7 +33,7 @@ abstract class Node
/**
* The metadata specifying how the value should be validated.
*
* @var MetadataInterface
* @var MetadataInterface|null
*/
public $metadata;
@ -57,21 +58,27 @@ abstract class Node
*/
public $cascadedGroups;
/**
* @var integer
*/
public $traversalStrategy;
/**
* Creates a new property node.
*
* @param mixed $value The property value
* @param MetadataInterface $metadata The property's metadata
* @param string $propertyPath The property path leading to
* this node
* @param string[] $groups The groups in which this node
* should be validated
* @param string[]|null $cascadedGroups The groups in which cascaded
* objects should be validated
* @param mixed $value The property value
* @param MetadataInterface|null $metadata The property's metadata
* @param string $propertyPath The property path leading to
* this node
* @param string[] $groups The groups in which this node
* should be validated
* @param string[]|null $cascadedGroups The groups in which cascaded
* objects should be validated
* @param integer $traversalStrategy
*
* @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)) {
throw new UnexpectedTypeException($cascadedGroups, 'null or array');
@ -82,5 +89,6 @@ abstract class Node
$this->propertyPath = $propertyPath;
$this->groups = $groups;
$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\Mapping\PropertyMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* 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
* cascaded objects should
* be validated
* @param integer $traversalStrategy
*
* @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)) {
throw new UnexpectedTypeException($object, 'object');
@ -71,7 +73,8 @@ class PropertyNode extends Node
$metadata,
$propertyPath,
$groups,
$cascadedGroups
$cascadedGroups,
$traversalStrategy
);
$this->object = $object;

View File

@ -117,17 +117,17 @@ class NodeTraverser implements NodeTraverserInterface
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)) {
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) {
return;
}
@ -151,9 +151,10 @@ class NodeTraverser implements NodeTraverserInterface
// (BC with Symfony < 2.5)
$traversal->nodeQueue->enqueue(new CollectionNode(
$node->value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath,
$cascadedGroups
$cascadedGroups,
null,
$traversalStrategy
));
return;
@ -174,38 +175,28 @@ class NodeTraverser implements NodeTraverserInterface
return;
}
// Traverse only if the TRAVERSE bit is set
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
// Traverse only if IMPLICIT or TRAVERSE
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
return;
}
if (!$node->value instanceof \Traversable) {
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) {
return;
}
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($node->value)
));
// If IMPLICIT, stop unless we deal with a Traversable
if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) {
return;
}
// If TRAVERSE, the constructor will fail if we have no Traversable
$traversal->nodeQueue->enqueue(new CollectionNode(
$node->value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath,
$node->groups,
$node->cascadedGroups
$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:
//
// 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,
// that group will be skipped in the subtree of that node.
if (false === $this->visit($node, $traversal->context)) {
return;
}
if (0 === count($node->groups)) {
return;
}
@ -232,54 +227,58 @@ class NodeTraverser implements NodeTraverserInterface
}
}
$traversalStrategy = $node->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();
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
// 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
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
// Traverse only if IMPLICIT or TRAVERSE
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
return;
}
if (!$node->value instanceof \Traversable) {
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) {
return;
}
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($node->value)
));
// If IMPLICIT, stop unless we deal with a Traversable
if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) {
return;
}
// If TRAVERSE, the constructor will fail if we have no Traversable
$traversal->nodeQueue->enqueue(new CollectionNode(
$node->value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath,
$node->groups,
$node->cascadedGroups
$node->cascadedGroups,
$traversalStrategy
));
}
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)) {
return;
}
$traversalStrategy = $node->metadata->getTraversalStrategy();
if (0 === count($node->groups)) {
return;
}
if ($traversalStrategy & TraversalStrategy::RECURSIVE) {
// Try to traverse nested objects, but ignore if they do not
// implement Traversable
$traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE;
$traversalStrategy = $node->traversalStrategy;
if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) {
$traversalStrategy = TraversalStrategy::NONE;
} 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;
}
@ -290,9 +289,10 @@ class NodeTraverser implements NodeTraverserInterface
// (BC with Symfony < 2.5)
$traversal->nodeQueue->enqueue(new CollectionNode(
$value,
new CollectionMetadata($traversalStrategy),
$node->propertyPath.'['.$key.']',
$node->groups
$node->groups,
null,
$traversalStrategy
));
continue;
@ -325,24 +325,27 @@ class NodeTraverser implements NodeTraverserInterface
$object,
$classMetadata,
$propertyPath,
$groups
$groups,
null,
$traversalStrategy
));
} catch (NoSuchMetadataException $e) {
// Rethrow if the TRAVERSE bit is not set
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
// Rethrow if not Traversable
if (!$object instanceof \Traversable) {
throw $e;
}
// Rethrow if the object does not implement Traversable
if (!$object instanceof \Traversable) {
// Rethrow unless IMPLICIT or TRAVERSE
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
throw $e;
}
$traversal->nodeQueue->enqueue(new CollectionNode(
$object,
new CollectionMetadata($traversalStrategy),
$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\Group\GroupManagerInterface;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
@ -56,6 +57,10 @@ class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterf
public function visit(Node $node, ExecutionContextInterface $context)
{
if ($node instanceof CollectionNode) {
return true;
}
if ($node instanceof ClassNode) {
$objectHash = spl_object_hash($node->value);
} 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\ConstraintViolationInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Tests\Fixtures\Reference;
@ -354,14 +355,43 @@ abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest
$this->assertNull($violations[0]->getCode());
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testExpectTraversableIfTraverse()
public function testTraverseTraversableByDefault()
{
$test = $this;
$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');
}
/**
* @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()
{
$test = $this;

View File

@ -339,30 +339,7 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($violations[0]->getCode());
}
/**
* @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()
public function testRecursiveTraversable()
{
$test = $this;
$entity = new Entity();

View File

@ -14,11 +14,15 @@ namespace Symfony\Component\Validator\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Traverse;
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\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\GenericMetadata;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\GenericNode;
use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
@ -110,14 +114,26 @@ class ContextualValidator implements ContextualValidatorInterface
return $this;
}
public function validateObjects($objects, $groups = null, $deep = false)
public function validateObjects($objects, $groups = null)
{
$constraint = new Traverse(array(
'traverse' => true,
'deep' => $deep,
));
if (!is_array($objects) && !$objects instanceof \Traversable) {
throw new UnexpectedTypeException($objects, 'array or \Traversable');
}
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)

View File

@ -51,7 +51,7 @@ interface ContextualValidatorInterface
*/
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.

View File

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

View File

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

View File

@ -47,7 +47,7 @@ interface ValidatorInterface
*/
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.