diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 3ee840a06b..d8314e023c 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -593,6 +593,28 @@ * Core translation messages are changed. Dot is added at the end of each message. Overwritten core translations should be fixed if any. More info here. + * Collections (arrays or instances of `\Traversable`) in properties + annotated with `Valid` are not traversed recursively by default anymore. + + This means that if a collection contains an entry which is again a + collection, the inner collection won't be traversed anymore as it + happened before. You can set the BC behavior by setting the new property + `deep` of `Valid` to `true`. + + Before: + + ``` + /** @Assert\Valid */ + private $recursiveCollection; + ``` + + After: + + ``` + /** @Assert\Valid(deep = true) */ + private $recursiveCollection; + ``` + ### Session * Flash messages now return an array based on their type. The old method is diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 34cc3061c1..43d895d0b6 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -20,3 +20,6 @@ CHANGELOG * [BC BREAK] ConstraintValidatorInterface method `isValid` has been renamed to `validate`, its return value was dropped. ConstraintValidator still contains `isValid` for BC + * [BC BREAK] collections in fields annotated with `Valid` are not traversed + recursively anymore by default. `Valid` contains a new property `deep` + which enables the BC behavior. diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 3f1a20cb42..10c8b0d8d6 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -21,4 +21,6 @@ use Symfony\Component\Validator\Constraint; class Valid extends Constraint { public $traverse = true; + + public $deep = false; } diff --git a/src/Symfony/Component/Validator/GraphWalker.php b/src/Symfony/Component/Validator/GraphWalker.php index 887d82cf88..54fd92e050 100644 --- a/src/Symfony/Component/Validator/GraphWalker.php +++ b/src/Symfony/Component/Validator/GraphWalker.php @@ -137,11 +137,11 @@ class GraphWalker } if ($metadata->isCascaded()) { - $this->walkReference($value, $propagatedGroup ?: $group, $propertyPath, $metadata->isCollectionCascaded()); + $this->walkReference($value, $propagatedGroup ?: $group, $propertyPath, $metadata->isCollectionCascaded(), $metadata->isCollectionCascadedDeeply()); } } - public function walkReference($value, $group, $propertyPath, $traverse) + public function walkReference($value, $group, $propertyPath, $traverse, $deep = false) { if (null !== $value) { if (!is_object($value) && !is_array($value)) { @@ -152,7 +152,8 @@ class GraphWalker foreach ($value as $key => $element) { // Ignore any scalar values in the collection if (is_object($element) || is_array($element)) { - $this->walkReference($element, $group, $propertyPath.'['.$key.']', $traverse); + // Only repeat the traversal if $deep is set + $this->walkReference($element, $group, $propertyPath.'['.$key.']', $deep, $deep); } } } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 949adf4607..30d022d75f 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -22,6 +22,7 @@ abstract class MemberMetadata extends ElementMetadata public $property; public $cascaded = false; public $collectionCascaded = false; + public $collectionCascadedDeeply = false; private $reflMember; /** @@ -52,7 +53,9 @@ abstract class MemberMetadata extends ElementMetadata if ($constraint instanceof Valid) { $this->cascaded = true; + /* @var Valid $constraint */ $this->collectionCascaded = $constraint->traverse; + $this->collectionCascadedDeeply = $constraint->deep; } else { parent::addConstraint($constraint); } @@ -156,6 +159,17 @@ abstract class MemberMetadata extends ElementMetadata return $this->collectionCascaded; } + /** + * Returns whether arrays or traversable objects stored in this member + * should be traversed recursively for inner arrays/traversable objects + * + * @return Boolean + */ + public function isCollectionCascadedDeeply() + { + return $this->collectionCascadedDeeply; + } + /** * Returns the value of this property in the given object * diff --git a/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php b/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php index 3a69e4c31b..5fd8b186c7 100644 --- a/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php +++ b/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php @@ -357,6 +357,77 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($violations, $this->walker->getViolations()); } + public function testWalkCascadedPropertyDoesNotRecurseByDefault() + { + $entity = new Entity(); + $entityMetadata = new ClassMetadata(get_class($entity)); + $this->factory->addClassMetadata($entityMetadata); + $this->factory->addClassMetadata(new ClassMetadata('ArrayIterator')); + + // add a constraint for the entity that always fails + $entityMetadata->addConstraint(new FailingConstraint()); + + // validate iterator when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->walker->walkPropertyValue( + $this->metadata, + 'reference', + new \ArrayIterator(array( + // The inner iterator should not be traversed by default + 'key' => new \ArrayIterator(array( + 'nested' => $entity, + )), + )), + 'Default', + 'path' + ); + + $violations = new ConstraintViolationList(); + + $this->assertEquals($violations, $this->walker->getViolations()); + } + + public function testWalkCascadedPropertyRecursesIfDeepIsSet() + { + $entity = new Entity(); + $entityMetadata = new ClassMetadata(get_class($entity)); + $this->factory->addClassMetadata($entityMetadata); + $this->factory->addClassMetadata(new ClassMetadata('ArrayIterator')); + + // add a constraint for the entity that always fails + $entityMetadata->addConstraint(new FailingConstraint()); + + // validate iterator when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => true, + ))); + + $this->walker->walkPropertyValue( + $this->metadata, + 'reference', + new \ArrayIterator(array( + // The inner iterator should now be traversed + 'key' => new \ArrayIterator(array( + 'nested' => $entity, + )), + )), + 'Default', + 'path' + ); + + $violations = new ConstraintViolationList(); + $violations->add(new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'path[key][nested]', + $entity + )); + + $this->assertEquals($violations, $this->walker->getViolations()); + } + public function testWalkCascadedPropertyDoesNotValidateNestedScalarValues() { // validate array when validating the property "reference"