[Validator] Fixed: Objects are not traversed unless they are instances of Traversable
This commit is contained in:
parent
2c65a28608
commit
a3555fbd99
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -25,6 +25,8 @@ class TraversalStrategy
|
|||
|
||||
const RECURSIVE = 4;
|
||||
|
||||
const IGNORE_NON_TRAVERSABLE = 8;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Reference in New Issue