[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
*/
class Valid extends Traverse
class Valid extends Constraint
{
/**
* @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0.
* Use the {@link Traverse} constraint instead.
*/
public $traverse = true;
/**
* @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0.
* Use the {@link Traverse} constraint instead.
*/
public $deep = false;
public function __construct($options = null)
{
if (is_array($options) && array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint %s', __CLASS__));
throw new ConstraintDefinitionException(sprintf(
'The option "groups" is not supported by the constraint %s',
__CLASS__
));
}
parent::__construct($options);

View File

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

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Mapping;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ValidationVisitorInterface;
use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface;
use Symfony\Component\Validator\Constraint;
@ -58,6 +59,24 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
));
}
// BC with Symfony < 2.5
// Only process if the traversal strategy was not already set by the
// Traverse constraint
if ($constraint instanceof Valid && !$this->traversalStrategy) {
if (true === $constraint->traverse) {
// Try to traverse cascaded objects, but ignore if they do not
// implement Traversable
$this->traversalStrategy = TraversalStrategy::TRAVERSE
| TraversalStrategy::IGNORE_NON_TRAVERSABLE;
if ($constraint->deep) {
$this->traversalStrategy |= TraversalStrategy::RECURSIVE;
}
} elseif (false === $constraint->traverse) {
$this->traversalStrategy = TraversalStrategy::NONE;
}
}
parent::addConstraint($constraint);
return $this;

View File

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

View File

@ -37,13 +37,13 @@ class ClassNode extends Node
* to this node
* @param string[] $groups The groups in which this
* node should be validated
* @param string[] $cascadedGroups The groups in which
* @param string[]|null $cascadedGroups The groups in which
* cascaded objects should be
* validated
*
* @throws UnexpectedTypeException If the given value is not an object
*/
public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups)
public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
{
if (!is_object($object)) {
throw new UnexpectedTypeException($object, 'object');

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\MetadataInterface;
/**
@ -65,11 +66,17 @@ abstract class Node
* this node
* @param string[] $groups The groups in which this node
* should be validated
* @param string[] $cascadedGroups The groups in which cascaded
* @param string[]|null $cascadedGroups The groups in which cascaded
* objects should be validated
*
* @throws UnexpectedTypeException If $cascadedGroups is invalid
*/
public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups)
public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
{
if (null !== $cascadedGroups && !is_array($cascadedGroups)) {
throw new UnexpectedTypeException($cascadedGroups, 'null or array');
}
$this->value = $value;
$this->metadata = $metadata;
$this->propertyPath = $propertyPath;

View File

@ -46,11 +46,11 @@ class PropertyNode extends Node
* to this node
* @param string[] $groups The groups in which this
* node should be validated
* @param string[] $cascadedGroups The groups in which
* @param string[]|null $cascadedGroups The groups in which
* cascaded objects should
* be validated
*/
public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups)
public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
{
parent::__construct(
$value,

View File

@ -12,8 +12,10 @@
namespace Symfony\Component\Validator\NodeTraverser;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Node\ClassNode;
@ -80,100 +82,219 @@ class NodeTraverser implements NodeTraverserInterface
}
if ($isTopLevelCall) {
$this->traversalStarted = false;
foreach ($this->visitors as $visitor) {
/** @var NodeVisitorInterface $visitor */
$visitor->afterTraversal($nodes);
}
$this->traversalStarted = false;
}
}
/**
* @param Node $node
*
* @return Boolean
*/
private function enterNode(Node $node)
{
$continueTraversal = true;
foreach ($this->visitors as $visitor) {
if (false === $visitor->enterNode($node)) {
$continueTraversal = false;
// Continue, so that the enterNode() method of all visitors
// is called
}
}
return $continueTraversal;
}
/**
* @param Node $node
*/
private function leaveNode(Node $node)
{
foreach ($this->visitors as $visitor) {
$visitor->leaveNode($node);
}
}
private function traverseNode(Node $node)
{
$stopTraversal = false;
$continue = $this->enterNode($node);
foreach ($this->visitors as $visitor) {
if (false === $visitor->enterNode($node)) {
$stopTraversal = true;
// Visitors have two possibilities to influence the traversal:
//
// 1. If a visitor's enterNode() method returns false, the traversal is
// skipped entirely.
// 2. If a visitor's enterNode() method removes a group from the node,
// that group will be skipped in the subtree of that node.
if (false === $continue) {
$this->leaveNode($node);
return;
}
if (null === $node->value) {
$this->leaveNode($node);
return;
}
// The "cascadedGroups" property is set by the NodeValidator when
// traversing group sequences
$cascadedGroups = null !== $node->cascadedGroups
? $node->cascadedGroups
: $node->groups;
if (0 === count($cascadedGroups)) {
$this->leaveNode($node);
return;
}
$cascadingStrategy = $node->metadata->getCascadingStrategy();
$traversalStrategy = $node->metadata->getTraversalStrategy();
if (is_array($node->value)) {
// Arrays are always traversed, independent of the specified
// traversal strategy
// (BC with Symfony < 2.5)
$this->cascadeEachObjectIn(
$node->value,
$node->propertyPath,
$node->cascadedGroups,
$traversalStrategy
);
$this->leaveNode($node);
return;
}
if ($cascadingStrategy & CascadingStrategy::CASCADE) {
// If the value is a scalar, pass it anyway, because we want
// a NoSuchMetadataException to be thrown in that case
// (BC with Symfony < 2.5)
$this->cascadeObject(
$node->value,
$node->propertyPath,
$node->cascadedGroups,
$traversalStrategy
);
$this->leaveNode($node);
return;
}
// Traverse only if the TRAVERSE bit is set
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
$this->leaveNode($node);
return;
}
if (!$node->value instanceof \Traversable) {
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) {
$this->leaveNode($node);
return;
}
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($node->value)
));
}
// Stop the traversal, but execute the leaveNode() methods anyway to
// perform possible cleanups
if (!$stopTraversal && null !== $node->value) {
$cascadingStrategy = $node->metadata->getCascadingStrategy();
$traversalStrategy = $node->metadata->getTraversalStrategy();
$this->cascadeEachObjectIn(
$node->value,
$node->propertyPath,
$node->groups,
$traversalStrategy
);
if (is_array($node->value)) {
$this->cascadeCollection(
$node->value,
$node->propertyPath,
$node->cascadedGroups,
$traversalStrategy
);
} elseif ($cascadingStrategy & CascadingStrategy::CASCADE) {
// If the value is a scalar, pass it anyway, because we want
// a NoSuchMetadataException to be thrown in that case
// (BC with Symfony < 2.5)
$this->cascadeObject(
$node->value,
$node->propertyPath,
$node->cascadedGroups,
$traversalStrategy
);
}
}
foreach ($this->visitors as $visitor) {
$visitor->leaveNode($node);
}
$this->leaveNode($node);
}
private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT)
{
$stopTraversal = false;
$continue = $this->enterNode($node);
foreach ($this->visitors as $visitor) {
if (false === $visitor->enterNode($node)) {
$stopTraversal = true;
// Visitors have two possibilities to influence the traversal:
//
// 1. If a visitor's enterNode() method returns false, the traversal is
// skipped entirely.
// 2. If a visitor's enterNode() method removes a group from the node,
// that group will be skipped in the subtree of that node.
if (false === $continue) {
$this->leaveNode($node);
return;
}
if (0 === count($node->groups)) {
$this->leaveNode($node);
return;
}
foreach ($node->metadata->getConstrainedProperties() as $propertyName) {
foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
$this->traverseNode(new PropertyNode(
$propertyMetadata->getPropertyValue($node->value),
$propertyMetadata,
$node->propertyPath
? $node->propertyPath.'.'.$propertyName
: $propertyName,
$node->groups,
$node->cascadedGroups
));
}
}
// Stop the traversal, but execute the leaveNode() methods anyway to
// perform possible cleanups
if (!$stopTraversal && count($node->groups) > 0) {
foreach ($node->metadata->getConstrainedProperties() as $propertyName) {
foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
$this->traverseNode(new PropertyNode(
$propertyMetadata->getPropertyValue($node->value),
$propertyMetadata,
$node->propertyPath
? $node->propertyPath.'.'.$propertyName
: $propertyName,
$node->groups,
$node->cascadedGroups
));
}
}
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
$traversalStrategy = $node->metadata->getTraversalStrategy();
}
if ($traversalStrategy & TraversalStrategy::TRAVERSE) {
$this->cascadeCollection(
$node->value,
$node->propertyPath,
$node->groups,
$traversalStrategy
);
}
// If no specific traversal strategy was requested when this method
// was called, use the traversal strategy of the class' metadata
if (TraversalStrategy::IMPLICIT === $traversalStrategy) {
$traversalStrategy = $node->metadata->getTraversalStrategy();
}
foreach ($this->visitors as $visitor) {
$visitor->leaveNode($node);
// Traverse only if the TRAVERSE bit is set
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
$this->leaveNode($node);
return;
}
if (!$node->value instanceof \Traversable) {
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) {
$this->leaveNode($node);
return;
}
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($node->value)
));
}
$this->cascadeEachObjectIn(
$node->value,
$node->propertyPath,
$node->groups,
$traversalStrategy
);
$this->leaveNode($node);
}
private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy)
@ -181,24 +302,31 @@ class NodeTraverser implements NodeTraverserInterface
try {
$classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
// error
}
$classNode = new ClassNode(
$object,
$classMetadata,
$propertyPath,
$groups,
$groups
);
$this->traverseClassNode($classNode, $traversalStrategy);
} catch (NoSuchMetadataException $e) {
if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) {
// Rethrow if the TRAVERSE bit is not set
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
throw $e;
}
// Metadata doesn't necessarily have to exist for
// traversable objects, because we know how to validate
// them anyway.
$this->cascadeCollection(
// Rethrow if the object does not implement Traversable
if (!$object instanceof \Traversable) {
throw $e;
}
// In that case, iterate the object and cascade each entry
$this->cascadeEachObjectIn(
$object,
$propertyPath,
$groups,
@ -207,15 +335,25 @@ class NodeTraverser implements NodeTraverserInterface
}
}
private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy)
private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy)
{
if (!($traversalStrategy & TraversalStrategy::RECURSIVE)) {
if ($traversalStrategy & TraversalStrategy::RECURSIVE) {
// Try to traverse nested objects, but ignore if they do not
// implement Traversable
$traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE;
} else {
// If the RECURSIVE bit is not set, change the strategy to IMPLICIT
// in order to respect the metadata's traversal strategy of each entry
// in the collection
$traversalStrategy = TraversalStrategy::IMPLICIT;
}
foreach ($collection as $key => $value) {
if (is_array($value)) {
$this->cascadeCollection(
// Arrays are always cascaded, independent of the specified
// traversal strategy
// (BC with Symfony < 2.5)
$this->cascadeEachObjectIn(
$value,
$propertyPath.'['.$key.']',
$groups,
@ -226,6 +364,7 @@ class NodeTraverser implements NodeTraverserInterface
}
// Scalar and null values in the collection are ignored
// (BC with Symfony < 2.5)
if (is_object($value)) {
$this->cascadeObject(
$value,

View File

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

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Validator\Tests\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\ExecutionContextInterface;
@ -225,6 +226,45 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group',
)));
$violations = $this->validator->validateCollection($array, 'Group');
/** @var ConstraintViolationInterface[] $violations */
$this->assertCount(1, $violations);
$this->assertSame('Message value', $violations[0]->getMessage());
$this->assertSame('Message %param%', $violations[0]->getMessageTemplate());
$this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters());
$this->assertSame('[key]', $violations[0]->getPropertyPath());
$this->assertSame($array, $violations[0]->getRoot());
$this->assertSame($entity, $violations[0]->getInvalidValue());
$this->assertNull($violations[0]->getMessagePluralization());
$this->assertNull($violations[0]->getCode());
}
public function testArrayLegacyApi()
{
$test = $this;
$entity = new Entity();
$array = array('key' => $entity);
$callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) {
$test->assertSame($test::ENTITY_CLASS, $context->getClassName());
$test->assertNull($context->getPropertyName());
$test->assertSame('[key]', $context->getPropertyPath());
$test->assertSame('Group', $context->getGroup());
$test->assertSame($test->metadata, $context->getMetadata());
$test->assertSame($test->metadataFactory, $context->getMetadataFactory());
$test->assertSame($array, $context->getRoot());
$test->assertSame($entity, $context->getValue());
$test->assertSame($entity, $value);
$context->addViolation('Message %param%', array('%param%' => 'value'));
};
$this->metadata->addConstraint(new Callback(array(
'callback' => $callback,
'groups' => 'Group',
)));
$violations = $this->validator->validate($array, 'Group');
/** @var ConstraintViolationInterface[] $violations */
@ -264,6 +304,45 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group',
)));
$violations = $this->validator->validateCollection($array, 'Group');
/** @var ConstraintViolationInterface[] $violations */
$this->assertCount(1, $violations);
$this->assertSame('Message value', $violations[0]->getMessage());
$this->assertSame('Message %param%', $violations[0]->getMessageTemplate());
$this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters());
$this->assertSame('[2][key]', $violations[0]->getPropertyPath());
$this->assertSame($array, $violations[0]->getRoot());
$this->assertSame($entity, $violations[0]->getInvalidValue());
$this->assertNull($violations[0]->getMessagePluralization());
$this->assertNull($violations[0]->getCode());
}
public function testRecursiveArrayLegacyApi()
{
$test = $this;
$entity = new Entity();
$array = array(2 => array('key' => $entity));
$callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) {
$test->assertSame($test::ENTITY_CLASS, $context->getClassName());
$test->assertNull($context->getPropertyName());
$test->assertSame('[2][key]', $context->getPropertyPath());
$test->assertSame('Group', $context->getGroup());
$test->assertSame($test->metadata, $context->getMetadata());
$test->assertSame($test->metadataFactory, $context->getMetadataFactory());
$test->assertSame($array, $context->getRoot());
$test->assertSame($entity, $context->getValue());
$test->assertSame($entity, $value);
$context->addViolation('Message %param%', array('%param%' => 'value'));
};
$this->metadata->addConstraint(new Callback(array(
'callback' => $callback,
'groups' => 'Group',
)));
$violations = $this->validator->validate($array, 'Group');
/** @var ConstraintViolationInterface[] $violations */
@ -278,17 +357,24 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($violations[0]->getCode());
}
/**
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
public function testTraversableTraverseDisabled()
public function testTraversableTraverseEnabled()
{
$test = $this;
$entity = new Entity();
$traversable = new \ArrayIterator(array('key' => $entity));
$callback = function () use ($test) {
$test->fail('Should not be called');
$callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) {
$test->assertSame($test::ENTITY_CLASS, $context->getClassName());
$test->assertNull($context->getPropertyName());
$test->assertSame('[key]', $context->getPropertyPath());
$test->assertSame('Group', $context->getGroup());
$test->assertSame($test->metadata, $context->getMetadata());
$test->assertSame($test->metadataFactory, $context->getMetadataFactory());
$test->assertSame($traversable, $context->getRoot());
$test->assertSame($entity, $context->getValue());
$test->assertSame($entity, $value);
$context->addViolation('Message %param%', array('%param%' => 'value'));
};
$this->metadata->addConstraint(new Callback(array(
@ -296,10 +382,21 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group',
)));
$this->validator->validate($traversable, 'Group');
$violations = $this->validator->validateCollection($traversable, 'Group', true);
/** @var ConstraintViolationInterface[] $violations */
$this->assertCount(1, $violations);
$this->assertSame('Message value', $violations[0]->getMessage());
$this->assertSame('Message %param%', $violations[0]->getMessageTemplate());
$this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters());
$this->assertSame('[key]', $violations[0]->getPropertyPath());
$this->assertSame($traversable, $violations[0]->getRoot());
$this->assertSame($entity, $violations[0]->getInvalidValue());
$this->assertNull($violations[0]->getMessagePluralization());
$this->assertNull($violations[0]->getCode());
}
public function testTraversableTraverseEnabled()
public function testTraversableTraverseEnabledLegacyApi()
{
$test = $this;
$entity = new Entity();
@ -338,6 +435,27 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertNull($violations[0]->getCode());
}
/**
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
public function testTraversableTraverseDisabledLegacyApi()
{
$test = $this;
$entity = new Entity();
$traversable = new \ArrayIterator(array('key' => $entity));
$callback = function () use ($test) {
$test->fail('Should not be called');
};
$this->metadata->addConstraint(new Callback(array(
'callback' => $callback,
'groups' => 'Group',
)));
$this->validator->validate($traversable, 'Group');
}
/**
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
@ -358,6 +476,29 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group',
)));
$this->validator->validateCollection($traversable, 'Group');
}
/**
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
*/
public function testRecursiveTraversableRecursiveTraversalDisabledLegacyApi()
{
$test = $this;
$entity = new Entity();
$traversable = new \ArrayIterator(array(
2 => new \ArrayIterator(array('key' => $entity)),
));
$callback = function () use ($test) {
$test->fail('Should not be called');
};
$this->metadata->addConstraint(new Callback(array(
'callback' => $callback,
'groups' => 'Group',
)));
$this->validator->validate($traversable, 'Group', true);
}
@ -388,6 +529,47 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
'groups' => 'Group',
)));
$violations = $this->validator->validateCollection($traversable, 'Group', true);
/** @var ConstraintViolationInterface[] $violations */
$this->assertCount(1, $violations);
$this->assertSame('Message value', $violations[0]->getMessage());
$this->assertSame('Message %param%', $violations[0]->getMessageTemplate());
$this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters());
$this->assertSame('[2][key]', $violations[0]->getPropertyPath());
$this->assertSame($traversable, $violations[0]->getRoot());
$this->assertSame($entity, $violations[0]->getInvalidValue());
$this->assertNull($violations[0]->getMessagePluralization());
$this->assertNull($violations[0]->getCode());
}
public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi()
{
$test = $this;
$entity = new Entity();
$traversable = new \ArrayIterator(array(
2 => new \ArrayIterator(array('key' => $entity)),
));
$callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) {
$test->assertSame($test::ENTITY_CLASS, $context->getClassName());
$test->assertNull($context->getPropertyName());
$test->assertSame('[2][key]', $context->getPropertyPath());
$test->assertSame('Group', $context->getGroup());
$test->assertSame($test->metadata, $context->getMetadata());
$test->assertSame($test->metadataFactory, $context->getMetadataFactory());
$test->assertSame($traversable, $context->getRoot());
$test->assertSame($entity, $context->getValue());
$test->assertSame($entity, $value);
$context->addViolation('Message %param%', array('%param%' => 'value'));
};
$this->metadata->addConstraint(new Callback(array(
'callback' => $callback,
'groups' => 'Group',
)));
$violations = $this->validator->validate($traversable, 'Group', true, true);
/** @var ConstraintViolationInterface[] $violations */
@ -1675,6 +1857,28 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
$test->assertSame('Separate violation', $violations[0]->getMessage());
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testExpectTraversableIfTraverse()
{
$entity = new Entity();
$this->validator->validateValue($entity, new Traverse());
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testExpectTraversableIfTraverseOnClass()
{
$entity = new Entity();
$this->metadata->addConstraint(new Traverse());
$this->validator->validate($entity);
}
public function testGetMetadataFactory()
{
$this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory());

View File

@ -55,6 +55,41 @@ class LegacyValidatorTest extends AbstractValidatorTest
$this->markTestSkipped('Not supported in the legacy API');
}
public function testArray()
{
$this->markTestSkipped('Not supported in the legacy API');
}
public function testRecursiveArray()
{
$this->markTestSkipped('Not supported in the legacy API');
}
public function testTraversableTraverseEnabled()
{
$this->markTestSkipped('Not supported in the legacy API');
}
public function testRecursiveTraversableRecursiveTraversalDisabled()
{
$this->markTestSkipped('Not supported in the legacy API');
}
public function testRecursiveTraversableRecursiveTraversalEnabled()
{
$this->markTestSkipped('Not supported in the legacy API');
}
public function testExpectTraversableIfTraverse()
{
$this->markTestSkipped('Not supported in the legacy API');
}
public function testExpectTraversableIfTraverseOnClass()
{
$this->markTestSkipped('Not supported in the legacy API');
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ValidatorException
*/

View File

@ -72,6 +72,25 @@ abstract class AbstractValidator implements ValidatorInterface
return $this->metadataFactory->hasMetadataFor($object);
}
protected function traverse($value, $constraints, $groups = null)
{
if (!is_array($constraints)) {
$constraints = array($constraints);
}
$metadata = new GenericMetadata();
$metadata->addConstraints($constraints);
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
$this->nodeTraverser->traverse(array(new GenericNode(
$value,
$metadata,
$this->defaultPropertyPath,
$groups,
$groups
)));
}
protected function traverseObject($object, $groups = null)
{
$classMetadata = $this->metadataFactory->getMetadataFor($object);
@ -158,25 +177,6 @@ abstract class AbstractValidator implements ValidatorInterface
$this->nodeTraverser->traverse($nodes);
}
protected function traverseValue($value, $constraints, $groups = null)
{
if (!is_array($constraints)) {
$constraints = array($constraints);
}
$metadata = new GenericMetadata();
$metadata->addConstraints($constraints);
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
$this->nodeTraverser->traverse(array(new GenericNode(
$value,
$metadata,
$this->defaultPropertyPath,
$groups,
$groups
)));
}
protected function normalizeGroups($groups)
{
if (is_array($groups)) {

View File

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

View File

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

View File

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