[Validator] Made traversal of Traversables consistent
If the traversal strategy is IMPLICIT (the default), the validator will now traverse any object that implements \Traversable and any array
This commit is contained in:
parent
117b1b9a17
commit
51197f68a3
|
@ -51,6 +51,6 @@ class Traverse extends Constraint
|
|||
*/
|
||||
public function getTargets()
|
||||
{
|
||||
return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT);
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,16 +23,8 @@ use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
|||
*/
|
||||
class Valid extends Constraint
|
||||
{
|
||||
/**
|
||||
* @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0.
|
||||
* Use the {@link Traverse} constraint instead.
|
||||
*/
|
||||
public $traverse = true;
|
||||
|
||||
/**
|
||||
* @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0.
|
||||
* Use the {@link Traverse} constraint instead.
|
||||
*/
|
||||
public $deep = false;
|
||||
|
||||
public function __construct($options = null)
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
namespace Symfony\Component\Validator\Mapping;
|
||||
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
use Symfony\Component\Validator\Constraints\Traverse;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\ValidationVisitorInterface;
|
||||
use Symfony\Component\Validator\PropertyMetadataContainerInterface;
|
||||
|
@ -190,6 +191,27 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface,
|
|||
));
|
||||
}
|
||||
|
||||
if ($constraint instanceof Traverse) {
|
||||
if (true === $constraint->traverse) {
|
||||
// If traverse is true, traversal should be explicitly enabled
|
||||
$this->traversalStrategy = TraversalStrategy::TRAVERSE;
|
||||
|
||||
if (!$constraint->deep) {
|
||||
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
|
||||
}
|
||||
} elseif (false === $constraint->traverse) {
|
||||
// If traverse is false, traversal should be explicitly disabled
|
||||
$this->traversalStrategy = TraversalStrategy::NONE;
|
||||
} else {
|
||||
// Else, traverse depending on the contextual information that
|
||||
// is available during validation
|
||||
$this->traversalStrategy = TraversalStrategy::IMPLICIT;
|
||||
}
|
||||
|
||||
// The constraint is not added
|
||||
return $this;
|
||||
}
|
||||
|
||||
$constraint->addImplicitGroupName($this->getDefaultGroup());
|
||||
|
||||
parent::addConstraint($constraint);
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Validator\Mapping;
|
||||
|
||||
/**
|
||||
* @since %%NextVersion%%
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class CollectionMetadata implements MetadataInterface
|
||||
{
|
||||
private $traversalStrategy;
|
||||
|
||||
public function __construct($traversalStrategy)
|
||||
{
|
||||
$this->traversalStrategy = $traversalStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all constraints for a given validation group.
|
||||
*
|
||||
* @param string $group The validation group.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\Constraint[] A list of constraint instances.
|
||||
*/
|
||||
public function findConstraints($group)
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function getCascadingStrategy()
|
||||
{
|
||||
return CascadingStrategy::NONE;
|
||||
}
|
||||
|
||||
public function getTraversalStrategy()
|
||||
{
|
||||
return $this->traversalStrategy;
|
||||
}
|
||||
}
|
|
@ -77,27 +77,17 @@ class GenericMetadata implements MetadataInterface
|
|||
if ($constraint instanceof Valid) {
|
||||
$this->cascadingStrategy = CascadingStrategy::CASCADE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($constraint instanceof Traverse) {
|
||||
if (true === $constraint->traverse) {
|
||||
// If traverse is true, traversal should be explicitly enabled
|
||||
$this->traversalStrategy = TraversalStrategy::TRAVERSE;
|
||||
|
||||
if ($constraint->deep) {
|
||||
$this->traversalStrategy |= TraversalStrategy::RECURSIVE;
|
||||
}
|
||||
} elseif (false === $constraint->traverse) {
|
||||
// If traverse is false, traversal should be explicitly disabled
|
||||
$this->traversalStrategy = TraversalStrategy::NONE;
|
||||
} else {
|
||||
// Else, traverse depending on the contextual information that
|
||||
// is available during validation
|
||||
if ($constraint->traverse) {
|
||||
// Traverse unless the value is not traversable
|
||||
$this->traversalStrategy = TraversalStrategy::IMPLICIT;
|
||||
|
||||
if (!$constraint->deep) {
|
||||
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
|
||||
}
|
||||
} else {
|
||||
$this->traversalStrategy = TraversalStrategy::NONE;
|
||||
}
|
||||
|
||||
// The constraint is not added
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -60,17 +60,14 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
|
|||
}
|
||||
|
||||
// BC with Symfony < 2.5
|
||||
// Only process if the traversal strategy was not already set by the
|
||||
// Traverse constraint
|
||||
if ($constraint instanceof Valid && !$this->traversalStrategy) {
|
||||
if ($constraint instanceof Valid) {
|
||||
if (true === $constraint->traverse) {
|
||||
// Try to traverse cascaded objects, but ignore if they do not
|
||||
// implement Traversable
|
||||
$this->traversalStrategy = TraversalStrategy::TRAVERSE
|
||||
| TraversalStrategy::IGNORE_NON_TRAVERSABLE;
|
||||
$this->traversalStrategy = TraversalStrategy::IMPLICIT;
|
||||
|
||||
if ($constraint->deep) {
|
||||
$this->traversalStrategy |= TraversalStrategy::RECURSIVE;
|
||||
if (!$constraint->deep) {
|
||||
$this->traversalStrategy |= TraversalStrategy::STOP_RECURSION;
|
||||
}
|
||||
} elseif (false === $constraint->traverse) {
|
||||
$this->traversalStrategy = TraversalStrategy::NONE;
|
||||
|
@ -180,7 +177,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
|
|||
*/
|
||||
public function isCollectionCascaded()
|
||||
{
|
||||
return (boolean) ($this->traversalStrategy & TraversalStrategy::TRAVERSE);
|
||||
return (boolean) ($this->traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,7 +188,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat
|
|||
*/
|
||||
public function isCollectionCascadedDeeply()
|
||||
{
|
||||
return (boolean) ($this->traversalStrategy & TraversalStrategy::RECURSIVE);
|
||||
return !($this->traversalStrategy & TraversalStrategy::STOP_RECURSION);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,15 +17,16 @@ namespace Symfony\Component\Validator\Mapping;
|
|||
*/
|
||||
class TraversalStrategy
|
||||
{
|
||||
const IMPLICIT = 0;
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
const IMPLICIT = 1;
|
||||
|
||||
const NONE = 1;
|
||||
const NONE = 2;
|
||||
|
||||
const TRAVERSE = 2;
|
||||
const TRAVERSE = 4;
|
||||
|
||||
const RECURSIVE = 4;
|
||||
|
||||
const IGNORE_NON_TRAVERSABLE = 8;
|
||||
const STOP_RECURSION = 8;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
|
|||
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
|
||||
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||||
|
||||
/**
|
||||
* Represents an object and its class metadata in the validation graph.
|
||||
|
@ -40,10 +41,11 @@ class ClassNode extends Node
|
|||
* @param string[]|null $cascadedGroups The groups in which
|
||||
* cascaded objects should be
|
||||
* validated
|
||||
* @param integer $traversalStrategy
|
||||
*
|
||||
* @throws UnexpectedTypeException If the given value is not an object
|
||||
* @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
|
||||
*/
|
||||
public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
|
||||
public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
|
||||
{
|
||||
if (!is_object($object)) {
|
||||
throw new UnexpectedTypeException($object, 'object');
|
||||
|
@ -56,5 +58,7 @@ class ClassNode extends Node
|
|||
$groups,
|
||||
$cascadedGroups
|
||||
);
|
||||
|
||||
$this->traversalStrategy = $traversalStrategy;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@
|
|||
|
||||
namespace Symfony\Component\Validator\Node;
|
||||
|
||||
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Mapping\MetadataInterface;
|
||||
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||||
|
||||
/**
|
||||
* Represents an traversable collection in the validation graph.
|
||||
|
@ -25,32 +27,35 @@ class CollectionNode extends Node
|
|||
/**
|
||||
* Creates a new collection node.
|
||||
*
|
||||
* @param array|\Traversable $collection The validated collection
|
||||
* @param MetadataInterface $metadata The class metadata of that
|
||||
* object
|
||||
* @param string $propertyPath The property path leading
|
||||
* @param array|\Traversable $collection The validated collection
|
||||
* @param string $propertyPath The property path leading
|
||||
* to this node
|
||||
* @param string[] $groups The groups in which this
|
||||
* @param string[] $groups The groups in which this
|
||||
* node should be validated
|
||||
* @param string[]|null $cascadedGroups The groups in which
|
||||
* @param string[]|null $cascadedGroups The groups in which
|
||||
* cascaded objects should be
|
||||
* validated
|
||||
* @param integer $traversalStrategy The traversal strategy
|
||||
*
|
||||
* @throws UnexpectedTypeException If the given value is not an array or
|
||||
* an instance of {@link \Traversable}
|
||||
* @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException
|
||||
*/
|
||||
public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
|
||||
public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE)
|
||||
{
|
||||
if (!is_array($collection) && !$collection instanceof \Traversable) {
|
||||
throw new UnexpectedTypeException($collection, 'object');
|
||||
throw new ConstraintDefinitionException(sprintf(
|
||||
'Traversal was enabled for "%s", but this class '.
|
||||
'does not implement "\Traversable".',
|
||||
get_class($collection)
|
||||
));
|
||||
}
|
||||
|
||||
parent::__construct(
|
||||
$collection,
|
||||
$metadata,
|
||||
null,
|
||||
$propertyPath,
|
||||
$groups,
|
||||
$cascadedGroups
|
||||
$cascadedGroups,
|
||||
$traversalStrategy
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
|
|||
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Mapping\MetadataInterface;
|
||||
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||||
|
||||
/**
|
||||
* A node in the validated graph.
|
||||
|
@ -32,7 +33,7 @@ abstract class Node
|
|||
/**
|
||||
* The metadata specifying how the value should be validated.
|
||||
*
|
||||
* @var MetadataInterface
|
||||
* @var MetadataInterface|null
|
||||
*/
|
||||
public $metadata;
|
||||
|
||||
|
@ -57,21 +58,27 @@ abstract class Node
|
|||
*/
|
||||
public $cascadedGroups;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
public $traversalStrategy;
|
||||
|
||||
/**
|
||||
* Creates a new property node.
|
||||
*
|
||||
* @param mixed $value The property value
|
||||
* @param MetadataInterface $metadata The property's metadata
|
||||
* @param string $propertyPath The property path leading to
|
||||
* this node
|
||||
* @param string[] $groups The groups in which this node
|
||||
* should be validated
|
||||
* @param string[]|null $cascadedGroups The groups in which cascaded
|
||||
* objects should be validated
|
||||
* @param mixed $value The property value
|
||||
* @param MetadataInterface|null $metadata The property's metadata
|
||||
* @param string $propertyPath The property path leading to
|
||||
* this node
|
||||
* @param string[] $groups The groups in which this node
|
||||
* should be validated
|
||||
* @param string[]|null $cascadedGroups The groups in which cascaded
|
||||
* objects should be validated
|
||||
* @param integer $traversalStrategy
|
||||
*
|
||||
* @throws UnexpectedTypeException If $cascadedGroups is invalid
|
||||
*/
|
||||
public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
|
||||
public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
|
||||
{
|
||||
if (null !== $cascadedGroups && !is_array($cascadedGroups)) {
|
||||
throw new UnexpectedTypeException($cascadedGroups, 'null or array');
|
||||
|
@ -82,5 +89,6 @@ abstract class Node
|
|||
$this->propertyPath = $propertyPath;
|
||||
$this->groups = $groups;
|
||||
$this->cascadedGroups = $cascadedGroups;
|
||||
$this->traversalStrategy = $traversalStrategy;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Node;
|
|||
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
|
||||
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||||
|
||||
/**
|
||||
* Represents the value of a property and its associated metadata.
|
||||
|
@ -57,10 +58,11 @@ class PropertyNode extends Node
|
|||
* @param string[]|null $cascadedGroups The groups in which
|
||||
* cascaded objects should
|
||||
* be validated
|
||||
* @param integer $traversalStrategy
|
||||
*
|
||||
* @throws UnexpectedTypeException If $object is not an object
|
||||
*/
|
||||
public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null)
|
||||
public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
|
||||
{
|
||||
if (!is_object($object)) {
|
||||
throw new UnexpectedTypeException($object, 'object');
|
||||
|
@ -71,7 +73,8 @@ class PropertyNode extends Node
|
|||
$metadata,
|
||||
$propertyPath,
|
||||
$groups,
|
||||
$cascadedGroups
|
||||
$cascadedGroups,
|
||||
$traversalStrategy
|
||||
);
|
||||
|
||||
$this->object = $object;
|
||||
|
|
|
@ -117,17 +117,17 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
|
||||
private function traverseNode(Node $node, Traversal $traversal)
|
||||
{
|
||||
// Visitors have two possibilities to influence the traversal:
|
||||
//
|
||||
// 1. If a visitor's enterNode() method returns false, the traversal is
|
||||
// skipped entirely.
|
||||
// 2. If a visitor's enterNode() method removes a group from the node,
|
||||
// that group will be skipped in the subtree of that node.
|
||||
|
||||
if (false === $this->visit($node, $traversal->context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Visitors have two possibilities to influence the traversal:
|
||||
//
|
||||
// 1. If a visitor's visit() method returns false, the traversal is
|
||||
// skipped entirely.
|
||||
// 2. If a visitor's visit() method removes a group from the node,
|
||||
// that group will be skipped in the subtree of that node.
|
||||
|
||||
if (null === $node->value) {
|
||||
return;
|
||||
}
|
||||
|
@ -151,9 +151,10 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
// (BC with Symfony < 2.5)
|
||||
$traversal->nodeQueue->enqueue(new CollectionNode(
|
||||
$node->value,
|
||||
new CollectionMetadata($traversalStrategy),
|
||||
$node->propertyPath,
|
||||
$cascadedGroups
|
||||
$cascadedGroups,
|
||||
null,
|
||||
$traversalStrategy
|
||||
));
|
||||
|
||||
return;
|
||||
|
@ -174,38 +175,28 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
return;
|
||||
}
|
||||
|
||||
// Traverse only if the TRAVERSE bit is set
|
||||
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
|
||||
// Traverse only if IMPLICIT or TRAVERSE
|
||||
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$node->value instanceof \Traversable) {
|
||||
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ConstraintDefinitionException(sprintf(
|
||||
'Traversal was enabled for "%s", but this class '.
|
||||
'does not implement "\Traversable".',
|
||||
get_class($node->value)
|
||||
));
|
||||
// If IMPLICIT, stop unless we deal with a Traversable
|
||||
if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If TRAVERSE, the constructor will fail if we have no Traversable
|
||||
$traversal->nodeQueue->enqueue(new CollectionNode(
|
||||
$node->value,
|
||||
new CollectionMetadata($traversalStrategy),
|
||||
$node->propertyPath,
|
||||
$node->groups,
|
||||
$node->cascadedGroups
|
||||
$cascadedGroups,
|
||||
null,
|
||||
$traversalStrategy
|
||||
));
|
||||
}
|
||||
|
||||
private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT)
|
||||
private function traverseClassNode(ClassNode $node, Traversal $traversal)
|
||||
{
|
||||
if (false === $this->visit($node, $traversal->context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Visitors have two possibilities to influence the traversal:
|
||||
//
|
||||
// 1. If a visitor's enterNode() method returns false, the traversal is
|
||||
|
@ -213,6 +204,10 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
// 2. If a visitor's enterNode() method removes a group from the node,
|
||||
// that group will be skipped in the subtree of that node.
|
||||
|
||||
if (false === $this->visit($node, $traversal->context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (0 === count($node->groups)) {
|
||||
return;
|
||||
}
|
||||
|
@ -232,54 +227,58 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
}
|
||||
}
|
||||
|
||||
$traversalStrategy = $node->traversalStrategy;
|
||||
|
||||
// If no specific traversal strategy was requested when this method
|
||||
// was called, use the traversal strategy of the class' metadata
|
||||
if (TraversalStrategy::IMPLICIT === $traversalStrategy) {
|
||||
$traversalStrategy = $node->metadata->getTraversalStrategy();
|
||||
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
|
||||
// Keep the STOP_RECURSION flag, if it was set
|
||||
$traversalStrategy = $node->metadata->getTraversalStrategy()
|
||||
| ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
|
||||
}
|
||||
|
||||
// Traverse only if the TRAVERSE bit is set
|
||||
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
|
||||
// Traverse only if IMPLICIT or TRAVERSE
|
||||
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$node->value instanceof \Traversable) {
|
||||
if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ConstraintDefinitionException(sprintf(
|
||||
'Traversal was enabled for "%s", but this class '.
|
||||
'does not implement "\Traversable".',
|
||||
get_class($node->value)
|
||||
));
|
||||
// If IMPLICIT, stop unless we deal with a Traversable
|
||||
if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If TRAVERSE, the constructor will fail if we have no Traversable
|
||||
$traversal->nodeQueue->enqueue(new CollectionNode(
|
||||
$node->value,
|
||||
new CollectionMetadata($traversalStrategy),
|
||||
$node->propertyPath,
|
||||
$node->groups,
|
||||
$node->cascadedGroups
|
||||
$node->cascadedGroups,
|
||||
$traversalStrategy
|
||||
));
|
||||
}
|
||||
|
||||
private function traverseCollectionNode(CollectionNode $node, Traversal $traversal)
|
||||
{
|
||||
// Visitors have two possibilities to influence the traversal:
|
||||
//
|
||||
// 1. If a visitor's enterNode() method returns false, the traversal is
|
||||
// skipped entirely.
|
||||
// 2. If a visitor's enterNode() method removes a group from the node,
|
||||
// that group will be skipped in the subtree of that node.
|
||||
|
||||
if (false === $this->visit($node, $traversal->context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$traversalStrategy = $node->metadata->getTraversalStrategy();
|
||||
if (0 === count($node->groups)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($traversalStrategy & TraversalStrategy::RECURSIVE) {
|
||||
// Try to traverse nested objects, but ignore if they do not
|
||||
// implement Traversable
|
||||
$traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE;
|
||||
$traversalStrategy = $node->traversalStrategy;
|
||||
|
||||
if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) {
|
||||
$traversalStrategy = TraversalStrategy::NONE;
|
||||
} else {
|
||||
// If the RECURSIVE bit is not set, change the strategy to IMPLICIT
|
||||
// in order to respect the metadata's traversal strategy of each entry
|
||||
// in the collection
|
||||
$traversalStrategy = TraversalStrategy::IMPLICIT;
|
||||
}
|
||||
|
||||
|
@ -290,9 +289,10 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
// (BC with Symfony < 2.5)
|
||||
$traversal->nodeQueue->enqueue(new CollectionNode(
|
||||
$value,
|
||||
new CollectionMetadata($traversalStrategy),
|
||||
$node->propertyPath.'['.$key.']',
|
||||
$node->groups
|
||||
$node->groups,
|
||||
null,
|
||||
$traversalStrategy
|
||||
));
|
||||
|
||||
continue;
|
||||
|
@ -325,24 +325,27 @@ class NodeTraverser implements NodeTraverserInterface
|
|||
$object,
|
||||
$classMetadata,
|
||||
$propertyPath,
|
||||
$groups
|
||||
$groups,
|
||||
null,
|
||||
$traversalStrategy
|
||||
));
|
||||
} catch (NoSuchMetadataException $e) {
|
||||
// Rethrow if the TRAVERSE bit is not set
|
||||
if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) {
|
||||
// Rethrow if not Traversable
|
||||
if (!$object instanceof \Traversable) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Rethrow if the object does not implement Traversable
|
||||
if (!$object instanceof \Traversable) {
|
||||
// Rethrow unless IMPLICIT or TRAVERSE
|
||||
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$traversal->nodeQueue->enqueue(new CollectionNode(
|
||||
$object,
|
||||
new CollectionMetadata($traversalStrategy),
|
||||
$propertyPath,
|
||||
$groups
|
||||
$groups,
|
||||
null,
|
||||
$traversalStrategy
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
|
|||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Group\GroupManagerInterface;
|
||||
use Symfony\Component\Validator\Node\ClassNode;
|
||||
use Symfony\Component\Validator\Node\CollectionNode;
|
||||
use Symfony\Component\Validator\Node\Node;
|
||||
use Symfony\Component\Validator\Node\PropertyNode;
|
||||
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
|
||||
|
@ -56,6 +57,10 @@ class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterf
|
|||
|
||||
public function visit(Node $node, ExecutionContextInterface $context)
|
||||
{
|
||||
if ($node instanceof CollectionNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($node instanceof ClassNode) {
|
||||
$objectHash = spl_object_hash($node->value);
|
||||
} elseif ($node instanceof PropertyNode) {
|
||||
|
|
|
@ -17,6 +17,7 @@ use Symfony\Component\Validator\Constraints\Traverse;
|
|||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Validator\MetadataFactoryInterface;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\Reference;
|
||||
|
@ -354,14 +355,43 @@ abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest
|
|||
$this->assertNull($violations[0]->getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
|
||||
*/
|
||||
public function testExpectTraversableIfTraverse()
|
||||
public function testTraverseTraversableByDefault()
|
||||
{
|
||||
$test = $this;
|
||||
$entity = new Entity();
|
||||
$traversable = new \ArrayIterator(array('key' => $entity));
|
||||
|
||||
$this->validator->validate($entity, new Traverse());
|
||||
$callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) {
|
||||
$test->assertSame($test::ENTITY_CLASS, $context->getClassName());
|
||||
$test->assertNull($context->getPropertyName());
|
||||
$test->assertSame('[key]', $context->getPropertyPath());
|
||||
$test->assertSame('Group', $context->getGroup());
|
||||
$test->assertSame($test->metadata, $context->getMetadata());
|
||||
$test->assertSame($traversable, $context->getRoot());
|
||||
$test->assertSame($entity, $context->getValue());
|
||||
$test->assertSame($entity, $value);
|
||||
|
||||
$context->addViolation('Message %param%', array('%param%' => 'value'));
|
||||
};
|
||||
|
||||
$this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator'));
|
||||
$this->metadata->addConstraint(new Callback(array(
|
||||
'callback' => $callback,
|
||||
'groups' => 'Group',
|
||||
)));
|
||||
|
||||
$violations = $this->validateObject($traversable, 'Group');
|
||||
|
||||
/** @var ConstraintViolationInterface[] $violations */
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('Message value', $violations[0]->getMessage());
|
||||
$this->assertSame('Message %param%', $violations[0]->getMessageTemplate());
|
||||
$this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters());
|
||||
$this->assertSame('[key]', $violations[0]->getPropertyPath());
|
||||
$this->assertSame($traversable, $violations[0]->getRoot());
|
||||
$this->assertSame($entity, $violations[0]->getInvalidValue());
|
||||
$this->assertNull($violations[0]->getMessagePluralization());
|
||||
$this->assertNull($violations[0]->getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -93,6 +93,29 @@ abstract class AbstractLegacyApiTest extends AbstractValidatorTest
|
|||
$this->validator->validate($traversable, 'Group');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
|
||||
*/
|
||||
public function testRecursiveTraversableRecursiveTraversalDisabled()
|
||||
{
|
||||
$test = $this;
|
||||
$entity = new Entity();
|
||||
$traversable = new \ArrayIterator(array(
|
||||
2 => new \ArrayIterator(array('key' => $entity)),
|
||||
));
|
||||
|
||||
$callback = function () use ($test) {
|
||||
$test->fail('Should not be called');
|
||||
};
|
||||
|
||||
$this->metadata->addConstraint(new Callback(array(
|
||||
'callback' => $callback,
|
||||
'groups' => 'Group',
|
||||
)));
|
||||
|
||||
$this->validator->validate($traversable, 'Group');
|
||||
}
|
||||
|
||||
public function testValidateInContext()
|
||||
{
|
||||
$test = $this;
|
||||
|
|
|
@ -339,30 +339,7 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase
|
|||
$this->assertNull($violations[0]->getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException
|
||||
*/
|
||||
public function testRecursiveTraversableRecursiveTraversalDisabled()
|
||||
{
|
||||
$test = $this;
|
||||
$entity = new Entity();
|
||||
$traversable = new \ArrayIterator(array(
|
||||
2 => new \ArrayIterator(array('key' => $entity)),
|
||||
));
|
||||
|
||||
$callback = function () use ($test) {
|
||||
$test->fail('Should not be called');
|
||||
};
|
||||
|
||||
$this->metadata->addConstraint(new Callback(array(
|
||||
'callback' => $callback,
|
||||
'groups' => 'Group',
|
||||
)));
|
||||
|
||||
$this->validateObjects($traversable, 'Group');
|
||||
}
|
||||
|
||||
public function testRecursiveTraversableRecursiveTraversalEnabled()
|
||||
public function testRecursiveTraversable()
|
||||
{
|
||||
$test = $this;
|
||||
$entity = new Entity();
|
||||
|
|
|
@ -14,11 +14,15 @@ namespace Symfony\Component\Validator\Validator;
|
|||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Traverse;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\ValidatorException;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
|
||||
use Symfony\Component\Validator\Mapping\GenericMetadata;
|
||||
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||||
use Symfony\Component\Validator\MetadataFactoryInterface;
|
||||
use Symfony\Component\Validator\Node\ClassNode;
|
||||
use Symfony\Component\Validator\Node\CollectionNode;
|
||||
use Symfony\Component\Validator\Node\GenericNode;
|
||||
use Symfony\Component\Validator\Node\PropertyNode;
|
||||
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
|
||||
|
@ -110,14 +114,26 @@ class ContextualValidator implements ContextualValidatorInterface
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function validateObjects($objects, $groups = null, $deep = false)
|
||||
public function validateObjects($objects, $groups = null)
|
||||
{
|
||||
$constraint = new Traverse(array(
|
||||
'traverse' => true,
|
||||
'deep' => $deep,
|
||||
));
|
||||
if (!is_array($objects) && !$objects instanceof \Traversable) {
|
||||
throw new UnexpectedTypeException($objects, 'array or \Traversable');
|
||||
}
|
||||
|
||||
return $this->validate($objects, $constraint, $groups);
|
||||
$traversalStrategy = TraversalStrategy::TRAVERSE;
|
||||
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
|
||||
|
||||
$node = new CollectionNode(
|
||||
$objects,
|
||||
$this->defaultPropertyPath,
|
||||
$groups,
|
||||
null,
|
||||
$traversalStrategy
|
||||
);
|
||||
|
||||
$this->nodeTraverser->traverse(array($node), $this->context);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function validateProperty($object, $propertyName, $groups = null)
|
||||
|
|
|
@ -51,7 +51,7 @@ interface ContextualValidatorInterface
|
|||
*/
|
||||
public function validateObject($object, $groups = null);
|
||||
|
||||
public function validateObjects($objects, $groups = null, $deep = false);
|
||||
public function validateObjects($objects, $groups = null);
|
||||
|
||||
/**
|
||||
* Validates a property of a value against its current value.
|
||||
|
|
|
@ -29,24 +29,12 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface
|
|||
return parent::validate($value, $groups, $traverse);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$constraint = new Traverse(array(
|
||||
'traverse' => true,
|
||||
'deep' => $deep,
|
||||
));
|
||||
if (is_array($value) || ($traverse && $value instanceof \Traversable)) {
|
||||
$constraint = new Valid(array('deep' => $deep));
|
||||
|
||||
return parent::validate($value, $constraint, $groups);
|
||||
}
|
||||
|
||||
if ($traverse && $value instanceof \Traversable) {
|
||||
$constraints = array(
|
||||
new Valid(),
|
||||
new Traverse(array('traverse' => true, 'deep' => $deep)),
|
||||
);
|
||||
|
||||
return parent::validate($value, $constraints, $groups);
|
||||
}
|
||||
|
||||
return $this->validateObject($value, $groups);
|
||||
}
|
||||
|
||||
|
|
|
@ -98,10 +98,10 @@ class Validator implements ValidatorInterface
|
|||
->getViolations();
|
||||
}
|
||||
|
||||
public function validateObjects($objects, $groups = null, $deep = false)
|
||||
public function validateObjects($objects, $groups = null)
|
||||
{
|
||||
return $this->startContext($objects)
|
||||
->validateObjects($objects, $groups, $deep)
|
||||
->validateObjects($objects, $groups)
|
||||
->getViolations();
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ interface ValidatorInterface
|
|||
*/
|
||||
public function validateObject($object, $groups = null);
|
||||
|
||||
public function validateObjects($objects, $groups = null, $deep = false);
|
||||
public function validateObjects($objects, $groups = null);
|
||||
|
||||
/**
|
||||
* Validates a property of a value against its current value.
|
||||
|
|
Reference in New Issue