[Validator] Fixed: Objects are not traversed unless they are instances of Traversable

This commit is contained in:
Bernhard Schussek 2014-02-19 17:20:08 +01:00
parent 2c65a28608
commit a3555fbd99
15 changed files with 545 additions and 122 deletions

View File

@ -21,12 +21,27 @@ use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
* *
* @api * @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) public function __construct($options = null)
{ {
if (is_array($options) && array_key_exists('groups', $options)) { 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); parent::__construct($options);

View File

@ -77,8 +77,7 @@ class GenericMetadata implements MetadataInterface
if ($constraint instanceof Valid) { if ($constraint instanceof Valid) {
$this->cascadingStrategy = CascadingStrategy::CASCADE; $this->cascadingStrategy = CascadingStrategy::CASCADE;
// Continue. Valid extends Traverse, so the return statement in the return $this;
// next block is going be executed.
} }
if ($constraint instanceof Traverse) { if ($constraint instanceof Traverse) {

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Mapping; namespace Symfony\Component\Validator\Mapping;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\ValidationVisitorInterface;
use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface;
use Symfony\Component\Validator\Constraint; 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); parent::addConstraint($constraint);
return $this; return $this;

View File

@ -25,6 +25,8 @@ class TraversalStrategy
const RECURSIVE = 4; const RECURSIVE = 4;
const IGNORE_NON_TRAVERSABLE = 8;
private function __construct() private function __construct()
{ {
} }

View File

@ -37,13 +37,13 @@ class ClassNode extends Node
* 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[] $cascadedGroups The groups in which * @param string[]|null $cascadedGroups The groups in which
* cascaded objects should be * cascaded objects should be
* validated * validated
* *
* @throws UnexpectedTypeException If the given value is not an object * @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)) { if (!is_object($object)) {
throw new UnexpectedTypeException($object, 'object'); throw new UnexpectedTypeException($object, 'object');

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Node; namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\MetadataInterface;
/** /**
@ -65,11 +66,17 @@ abstract class Node
* 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[] $cascadedGroups The groups in which cascaded * @param string[]|null $cascadedGroups The groups in which cascaded
* objects should be validated * 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->value = $value;
$this->metadata = $metadata; $this->metadata = $metadata;
$this->propertyPath = $propertyPath; $this->propertyPath = $propertyPath;

View File

@ -46,11 +46,11 @@ class PropertyNode extends Node
* 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[] $cascadedGroups The groups in which * @param string[]|null $cascadedGroups The groups in which
* cascaded objects should * cascaded objects should
* be validated * 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( parent::__construct(
$value, $value,

View File

@ -12,8 +12,10 @@
namespace Symfony\Component\Validator\NodeTraverser; namespace Symfony\Component\Validator\NodeTraverser;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\NoSuchMetadataException;
use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy; 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;
@ -80,100 +82,219 @@ class NodeTraverser implements NodeTraverserInterface
} }
if ($isTopLevelCall) { if ($isTopLevelCall) {
$this->traversalStarted = false;
foreach ($this->visitors as $visitor) { foreach ($this->visitors as $visitor) {
/** @var NodeVisitorInterface $visitor */ /** @var NodeVisitorInterface $visitor */
$visitor->afterTraversal($nodes); $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) private function traverseNode(Node $node)
{ {
$stopTraversal = false; $continue = $this->enterNode($node);
foreach ($this->visitors as $visitor) { // Visitors have two possibilities to influence the traversal:
if (false === $visitor->enterNode($node)) { //
$stopTraversal = true; // 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 $this->cascadeEachObjectIn(
// perform possible cleanups $node->value,
if (!$stopTraversal && null !== $node->value) { $node->propertyPath,
$cascadingStrategy = $node->metadata->getCascadingStrategy(); $node->groups,
$traversalStrategy = $node->metadata->getTraversalStrategy(); $traversalStrategy
);
if (is_array($node->value)) { $this->leaveNode($node);
$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);
}
} }
private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT)
{ {
$stopTraversal = false; $continue = $this->enterNode($node);
foreach ($this->visitors as $visitor) { // Visitors have two possibilities to influence the traversal:
if (false === $visitor->enterNode($node)) { //
$stopTraversal = true; // 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 // If no specific traversal strategy was requested when this method
// perform possible cleanups // was called, use the traversal strategy of the class' metadata
if (!$stopTraversal && count($node->groups) > 0) { if (TraversalStrategy::IMPLICIT === $traversalStrategy) {
foreach ($node->metadata->getConstrainedProperties() as $propertyName) { $traversalStrategy = $node->metadata->getTraversalStrategy();
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
);
}
} }
foreach ($this->visitors as $visitor) { // Traverse only if the TRAVERSE bit is set
$visitor->leaveNode($node); 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) private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy)
@ -181,24 +302,31 @@ class NodeTraverser implements NodeTraverserInterface
try { try {
$classMetadata = $this->metadataFactory->getMetadataFor($object); $classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
// error
}
$classNode = new ClassNode( $classNode = new ClassNode(
$object, $object,
$classMetadata, $classMetadata,
$propertyPath, $propertyPath,
$groups,
$groups $groups
); );
$this->traverseClassNode($classNode, $traversalStrategy); $this->traverseClassNode($classNode, $traversalStrategy);
} catch (NoSuchMetadataException $e) { } catch (NoSuchMetadataException $e) {
if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) { // Rethrow if the TRAVERSE bit is not set
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
throw $e; throw $e;
} }
// Metadata doesn't necessarily have to exist for // Rethrow if the object does not implement Traversable
// traversable objects, because we know how to validate if (!$object instanceof \Traversable) {
// them anyway. throw $e;
$this->cascadeCollection( }
// In that case, iterate the object and cascade each entry
$this->cascadeEachObjectIn(
$object, $object,
$propertyPath, $propertyPath,
$groups, $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; $traversalStrategy = TraversalStrategy::IMPLICIT;
} }
foreach ($collection as $key => $value) { foreach ($collection as $key => $value) {
if (is_array($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, $value,
$propertyPath.'['.$key.']', $propertyPath.'['.$key.']',
$groups, $groups,
@ -226,6 +364,7 @@ class NodeTraverser implements NodeTraverserInterface
} }
// Scalar and null values in the collection are ignored // Scalar and null values in the collection are ignored
// (BC with Symfony < 2.5)
if (is_object($value)) { if (is_object($value)) {
$this->cascadeObject( $this->cascadeObject(
$value, $value,

View File

@ -146,7 +146,10 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface
foreach ($groupSequence->groups as $groupInSequence) { foreach ($groupSequence->groups as $groupInSequence) {
$node = clone $node; $node = clone $node;
$node->groups = array($groupInSequence); $node->groups = array($groupInSequence);
$node->cascadedGroups = array($groupSequence->cascadedGroup ?: $groupInSequence);
if (null !== $groupSequence->cascadedGroup) {
$node->cascadedGroups = array($groupSequence->cascadedGroup);
}
$this->nodeTraverser->traverse(array($node)); $this->nodeTraverser->traverse(array($node));

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Validator\Tests\Validator;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\ExecutionContextInterface; use Symfony\Component\Validator\ExecutionContextInterface;
@ -225,6 +226,45 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group', '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'); $violations = $this->validator->validate($array, 'Group');
/** @var ConstraintViolationInterface[] $violations */ /** @var ConstraintViolationInterface[] $violations */
@ -264,6 +304,45 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group', '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'); $violations = $this->validator->validate($array, 'Group');
/** @var ConstraintViolationInterface[] $violations */ /** @var ConstraintViolationInterface[] $violations */
@ -278,17 +357,24 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($violations[0]->getCode()); $this->assertNull($violations[0]->getCode());
} }
/** public function testTraversableTraverseEnabled()
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
public function testTraversableTraverseDisabled()
{ {
$test = $this; $test = $this;
$entity = new Entity(); $entity = new Entity();
$traversable = new \ArrayIterator(array('key' => $entity)); $traversable = new \ArrayIterator(array('key' => $entity));
$callback = function () use ($test) { $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) {
$test->fail('Should not be called'); $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( $this->metadata->addConstraint(new Callback(array(
@ -296,10 +382,21 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group', '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; $test = $this;
$entity = new Entity(); $entity = new Entity();
@ -338,6 +435,27 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($violations[0]->getCode()); $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 * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/ */
@ -358,6 +476,29 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group', '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); $this->validator->validate($traversable, 'Group', true);
} }
@ -388,6 +529,47 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group', '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); $violations = $this->validator->validate($traversable, 'Group', true, true);
/** @var ConstraintViolationInterface[] $violations */ /** @var ConstraintViolationInterface[] $violations */
@ -1675,6 +1857,28 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$test->assertSame('Separate violation', $violations[0]->getMessage()); $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() public function testGetMetadataFactory()
{ {
$this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory());

View File

@ -55,6 +55,41 @@ class LegacyValidatorTest extends AbstractValidatorTest
$this->markTestSkipped('Not supported in the legacy API'); $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 * @expectedException \Symfony\Component\Validator\Exception\ValidatorException
*/ */

View File

@ -72,6 +72,25 @@ abstract class AbstractValidator implements ValidatorInterface
return $this->metadataFactory->hasMetadataFor($object); 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) protected function traverseObject($object, $groups = null)
{ {
$classMetadata = $this->metadataFactory->getMetadataFor($object); $classMetadata = $this->metadataFactory->getMetadataFor($object);
@ -158,25 +177,6 @@ abstract class AbstractValidator implements ValidatorInterface
$this->nodeTraverser->traverse($nodes); $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) protected function normalizeGroups($groups)
{ {
if (is_array($groups)) { if (is_array($groups)) {

View File

@ -58,7 +58,7 @@ class ContextualValidator extends AbstractValidator implements ContextualValidat
*/ */
public function validate($value, $constraints, $groups = null) public function validate($value, $constraints, $groups = null)
{ {
$this->traverseValue($value, $constraints, $groups); $this->traverse($value, $constraints, $groups);
return $this->context->getViolations(); return $this->context->getViolations();
} }
@ -89,7 +89,7 @@ class ContextualValidator extends AbstractValidator implements ContextualValidat
'deep' => $deep, 'deep' => $deep,
)); ));
$this->traverseValue($collection, $constraint, $groups); $this->traverse($collection, $constraint, $groups);
return $this->context->getViolations(); return $this->context->getViolations();
} }

View File

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

View File

@ -38,7 +38,7 @@ class Validator extends AbstractValidator
{ {
$this->contextManager->startContext($value); $this->contextManager->startContext($value);
$this->traverseValue($value, $constraints, $groups); $this->traverse($value, $constraints, $groups);
return $this->contextManager->stopContext()->getViolations(); return $this->contextManager->stopContext()->getViolations();
} }
@ -61,7 +61,7 @@ class Validator extends AbstractValidator
'deep' => $deep, 'deep' => $deep,
)); ));
$this->traverseValue($collection, $constraint, $groups); $this->traverse($collection, $constraint, $groups);
return $this->contextManager->stopContext()->getViolations(); return $this->contextManager->stopContext()->getViolations();
} }