diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index d6e818c44a..d3d7937ad9 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -72,8 +72,6 @@ class NodeValidationVisitor extends AbstractVisitor $context->setNode($node->value, $node->metadata, $node->propertyPath); if ($node instanceof ClassNode) { - $this->replaceDefaultGroup($node); - $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { $objectHash = spl_object_hash($node->object); @@ -88,6 +86,8 @@ class NodeValidationVisitor extends AbstractVisitor // simply continue traversal (if possible) foreach ($node->groups as $key => $group) { + $cascadedGroup = null; + // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). @@ -106,11 +106,36 @@ class NodeValidationVisitor extends AbstractVisitor } $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $node->metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $node->value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $context); + $this->traverseGroupSequence($node, $group, $cascadedGroup, $context); // Skip the group sequence when validating successor nodes unset($node->groups[$key]); @@ -135,17 +160,15 @@ class NodeValidationVisitor extends AbstractVisitor * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); + $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; foreach ($groupSequence->groups as $groupInSequence) { $node = clone $node; $node->groups = array($groupInSequence); - - if (null !== $groupSequence->cascadedGroup) { - $node->cascadedGroups = array($groupSequence->cascadedGroup); - } + $node->cascadedGroups = $cascadedGroups; $this->nodeTraverser->traverse(array($node), $context); @@ -199,44 +222,4 @@ class NodeValidationVisitor extends AbstractVisitor $validator->validate($node->value, $constraint); } } - - /** - * Checks class nodes whether their "Default" group is replaced by a group - * sequence and adjusts the validation groups accordingly. - * - * If the "Default" group is replaced for a class node, and if the validated - * groups of the node contain the group "Default", that group is replaced by - * the group sequence specified in the class' metadata. - * - * @param ClassNode $node The node - */ - private function replaceDefaultGroup(ClassNode $node) - { - if ($node->metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $node->metadata->getGroupSequence(); - } elseif ($node->metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $node->value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } else { - // The "Default" group is not overridden. Quit. - return; - } - - $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); - - if (false !== $key) { - // Replace the "Default" group by the group sequence - $node->groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } } diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 563fe74288..bb5a0fff5e 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -277,9 +277,6 @@ class RecursiveContextualValidator implements ContextualValidatorInterface */ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - // Replace "Default" group by group sequence, if appropriate - $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { @@ -444,14 +441,13 @@ class RecursiveContextualValidator implements ContextualValidatorInterface return; } - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = count($cascadedGroups) > 0 - ? $cascadedGroups - : $groups; - $cascadingStrategy = $metadata->getCascadingStrategy(); + // Quit unless we have an array or a cascaded object + if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) { + return; + } + // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the node's metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { @@ -460,6 +456,12 @@ class RecursiveContextualValidator implements ContextualValidatorInterface | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = count($cascadedGroups) > 0 + ? $cascadedGroups + : $groups; + if (is_array($value)) { // Arrays are always traversed, independent of the specified // traversal strategy @@ -475,21 +477,17 @@ class RecursiveContextualValidator implements ContextualValidatorInterface 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( - $value, - $valueHash, - $propertyPath, - $cascadedGroups, - $traversalStrategy, - $context - ); - - return; - } + // 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( + $value, + $valueHash, + $propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); // Currently, the traversal strategy can only be TRAVERSE for a // generic node if the cascading strategy is CASCADE. Thus, traversable @@ -590,6 +588,8 @@ class RecursiveContextualValidator implements ContextualValidatorInterface // simply continue traversal (if possible) foreach ($groups as $key => $group) { + $cascadedGroup = null; + // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). @@ -608,11 +608,36 @@ class RecursiveContextualValidator implements ContextualValidatorInterface } $context->markObjectAsValidatedForGroup($valueHash, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $context); + $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); // Skip the group sequence when validating successor nodes unset($groups[$key]); @@ -637,17 +662,13 @@ class RecursiveContextualValidator implements ContextualValidatorInterface * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); + $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; foreach ($groupSequence->groups as $groupInSequence) { $groups = array($groupInSequence); - $cascadedGroups = null; - - if (null !== $groupSequence->cascadedGroup) { - $cascadedGroups = array($groupSequence->cascadedGroup); - } if ($metadata instanceof ClassMetadataInterface) { $this->traverseClassNode( @@ -727,44 +748,4 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $validator->validate($value, $constraint); } } - - /** - * @param $value - * @param ClassMetadataInterface $metadata - * @param array $groups - * - * @return array - */ - private function replaceDefaultGroup($value, ClassMetadataInterface $metadata, array $groups) - { - $groupSequence = null; - - if ($metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $metadata->getGroupSequence(); - } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } - - if (null !== $groupSequence) { - $key = array_search(Constraint::DEFAULT_GROUP, $groups); - - if (false !== $key) { - // Replace the "Default" group by the group sequence - $groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } - - return $groups; - } }