From 117b1b9a17edba7b0724bc6ce52e3957e1229887 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 17:12:00 +0100 Subject: [PATCH] [Validator] Wrapped collections into CollectionNode instances --- .../Validator/Mapping/CollectionMetadata.php | 48 +++++++ .../Validator/Node/CollectionNode.php | 56 ++++++++ .../Validator/NodeTraverser/NodeTraverser.php | 136 +++++++++--------- 3 files changed, 175 insertions(+), 65 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/CollectionMetadata.php create mode 100644 src/Symfony/Component/Validator/Node/CollectionNode.php diff --git a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php new file mode 100644 index 0000000000..f1235af899 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php @@ -0,0 +1,48 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php new file mode 100644 index 0000000000..848c7a6c92 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * Represents an traversable collection in the validation graph. + * + * @since 2.5 + * @author Bernhard Schussek + */ +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 + * 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 + * + * @throws UnexpectedTypeException If the given value is not an array or + * an instance of {@link \Traversable} + */ + public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + { + if (!is_array($collection) && !$collection instanceof \Traversable) { + throw new UnexpectedTypeException($collection, 'object'); + } + + parent::__construct( + $collection, + $metadata, + $propertyPath, + $groups, + $cascadedGroups + ); + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index b2c4355b76..7be878d713 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -16,9 +16,11 @@ 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\CollectionMetadata; 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\Node; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; @@ -84,6 +86,8 @@ class NodeTraverser implements NodeTraverserInterface if ($node instanceof ClassNode) { $this->traverseClassNode($node, $traversal); + } elseif ($node instanceof CollectionNode) { + $this->traverseCollectionNode($node, $traversal); } else { $this->traverseNode($node, $traversal); } @@ -145,13 +149,12 @@ class NodeTraverser implements NodeTraverserInterface // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $traversal - ); + $cascadedGroups + )); return; } @@ -188,13 +191,13 @@ class NodeTraverser implements NodeTraverserInterface )); } - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $traversal - ); + $node->groups, + $node->cascadedGroups + )); } private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) @@ -252,13 +255,61 @@ class NodeTraverser implements NodeTraverserInterface )); } - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, $node->groups, - $traversalStrategy, - $traversal - ); + $node->cascadedGroups + )); + } + + private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) + { + if (false === $this->visit($node, $traversal->context)) { + return; + } + + $traversalStrategy = $node->metadata->getTraversalStrategy(); + + 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 ($node->value as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $traversal->nodeQueue->enqueue(new CollectionNode( + $value, + new CollectionMetadata($traversalStrategy), + $node->propertyPath.'['.$key.']', + $node->groups + )); + + continue; + } + + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->cascadeObject( + $value, + $node->propertyPath.'['.$key.']', + $node->groups, + $traversalStrategy, + $traversal + ); + } + } } private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) @@ -287,57 +338,12 @@ class NodeTraverser implements NodeTraverserInterface throw $e; } - // In that case, iterate the object and cascade each entry - $this->cascadeEachObjectIn( - $object, - $propertyPath, - $groups, - $traversalStrategy, - $traversal - ); - } - } - - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) - { - 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)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $this->cascadeEachObjectIn( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $traversal - ); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $traversal - ); - } + $traversal->nodeQueue->enqueue(new CollectionNode( + $object, + new CollectionMetadata($traversalStrategy), + $propertyPath, + $groups + )); } } }