Version 1
[yaffs-website] / vendor / symfony / validator / Validator / RecursiveContextualValidator.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Validator\Validator;
13
14 use Symfony\Component\Validator\Constraint;
15 use Symfony\Component\Validator\Constraints\GroupSequence;
16 use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
17 use Symfony\Component\Validator\Context\ExecutionContext;
18 use Symfony\Component\Validator\Context\ExecutionContextInterface;
19 use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
20 use Symfony\Component\Validator\Exception\NoSuchMetadataException;
21 use Symfony\Component\Validator\Exception\RuntimeException;
22 use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
23 use Symfony\Component\Validator\Exception\ValidatorException;
24 use Symfony\Component\Validator\Mapping\CascadingStrategy;
25 use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
26 use Symfony\Component\Validator\Mapping\GenericMetadata;
27 use Symfony\Component\Validator\Mapping\MetadataInterface;
28 use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
29 use Symfony\Component\Validator\Mapping\TraversalStrategy;
30 use Symfony\Component\Validator\MetadataFactoryInterface;
31 use Symfony\Component\Validator\ObjectInitializerInterface;
32 use Symfony\Component\Validator\Util\PropertyPath;
33
34 /**
35  * Recursive implementation of {@link ContextualValidatorInterface}.
36  *
37  * @author Bernhard Schussek <bschussek@gmail.com>
38  */
39 class RecursiveContextualValidator implements ContextualValidatorInterface
40 {
41     /**
42      * @var ExecutionContextInterface
43      */
44     private $context;
45
46     /**
47      * @var string
48      */
49     private $defaultPropertyPath;
50
51     /**
52      * @var array
53      */
54     private $defaultGroups;
55
56     /**
57      * @var MetadataFactoryInterface
58      */
59     private $metadataFactory;
60
61     /**
62      * @var ConstraintValidatorFactoryInterface
63      */
64     private $validatorFactory;
65
66     /**
67      * @var ObjectInitializerInterface[]
68      */
69     private $objectInitializers;
70
71     /**
72      * Creates a validator for the given context.
73      *
74      * @param ExecutionContextInterface           $context            The execution context
75      * @param MetadataFactoryInterface            $metadataFactory    The factory for
76      *                                                                fetching the metadata
77      *                                                                of validated objects
78      * @param ConstraintValidatorFactoryInterface $validatorFactory   The factory for creating
79      *                                                                constraint validators
80      * @param ObjectInitializerInterface[]        $objectInitializers The object initializers
81      */
82     public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array())
83     {
84         $this->context = $context;
85         $this->defaultPropertyPath = $context->getPropertyPath();
86         $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
87         $this->metadataFactory = $metadataFactory;
88         $this->validatorFactory = $validatorFactory;
89         $this->objectInitializers = $objectInitializers;
90     }
91
92     /**
93      * {@inheritdoc}
94      */
95     public function atPath($path)
96     {
97         $this->defaultPropertyPath = $this->context->getPropertyPath($path);
98
99         return $this;
100     }
101
102     /**
103      * {@inheritdoc}
104      */
105     public function validate($value, $constraints = null, $groups = null)
106     {
107         $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
108
109         $previousValue = $this->context->getValue();
110         $previousObject = $this->context->getObject();
111         $previousMetadata = $this->context->getMetadata();
112         $previousPath = $this->context->getPropertyPath();
113         $previousGroup = $this->context->getGroup();
114         $previousConstraint = null;
115
116         if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) {
117             $previousConstraint = $this->context->getConstraint();
118         }
119
120         // If explicit constraints are passed, validate the value against
121         // those constraints
122         if (null !== $constraints) {
123             // You can pass a single constraint or an array of constraints
124             // Make sure to deal with an array in the rest of the code
125             if (!is_array($constraints)) {
126                 $constraints = array($constraints);
127             }
128
129             $metadata = new GenericMetadata();
130             $metadata->addConstraints($constraints);
131
132             $this->validateGenericNode(
133                 $value,
134                 null,
135                 is_object($value) ? spl_object_hash($value) : null,
136                 $metadata,
137                 $this->defaultPropertyPath,
138                 $groups,
139                 null,
140                 TraversalStrategy::IMPLICIT,
141                 $this->context
142             );
143
144             $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
145             $this->context->setGroup($previousGroup);
146
147             if (null !== $previousConstraint) {
148                 $this->context->setConstraint($previousConstraint);
149             }
150
151             return $this;
152         }
153
154         // If an object is passed without explicit constraints, validate that
155         // object against the constraints defined for the object's class
156         if (is_object($value)) {
157             $this->validateObject(
158                 $value,
159                 $this->defaultPropertyPath,
160                 $groups,
161                 TraversalStrategy::IMPLICIT,
162                 $this->context
163             );
164
165             $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
166             $this->context->setGroup($previousGroup);
167
168             return $this;
169         }
170
171         // If an array is passed without explicit constraints, validate each
172         // object in the array
173         if (is_array($value)) {
174             $this->validateEachObjectIn(
175                 $value,
176                 $this->defaultPropertyPath,
177                 $groups,
178                 true,
179                 $this->context
180             );
181
182             $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
183             $this->context->setGroup($previousGroup);
184
185             return $this;
186         }
187
188         throw new RuntimeException(sprintf(
189             'Cannot validate values of type "%s" automatically. Please '.
190             'provide a constraint.',
191             gettype($value)
192         ));
193     }
194
195     /**
196      * {@inheritdoc}
197      */
198     public function validateProperty($object, $propertyName, $groups = null)
199     {
200         $classMetadata = $this->metadataFactory->getMetadataFor($object);
201
202         if (!$classMetadata instanceof ClassMetadataInterface) {
203             // Cannot be UnsupportedMetadataException because of BC with
204             // Symfony < 2.5
205             throw new ValidatorException(sprintf(
206                 'The metadata factory should return instances of '.
207                 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
208                 'got: "%s".',
209                 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
210             ));
211         }
212
213         $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
214         $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
215         $cacheKey = spl_object_hash($object);
216         $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
217
218         $previousValue = $this->context->getValue();
219         $previousObject = $this->context->getObject();
220         $previousMetadata = $this->context->getMetadata();
221         $previousPath = $this->context->getPropertyPath();
222         $previousGroup = $this->context->getGroup();
223
224         foreach ($propertyMetadatas as $propertyMetadata) {
225             $propertyValue = $propertyMetadata->getPropertyValue($object);
226
227             $this->validateGenericNode(
228                 $propertyValue,
229                 $object,
230                 $cacheKey.':'.$propertyName,
231                 $propertyMetadata,
232                 $propertyPath,
233                 $groups,
234                 null,
235                 TraversalStrategy::IMPLICIT,
236                 $this->context
237             );
238         }
239
240         $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
241         $this->context->setGroup($previousGroup);
242
243         return $this;
244     }
245
246     /**
247      * {@inheritdoc}
248      */
249     public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
250     {
251         $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
252
253         if (!$classMetadata instanceof ClassMetadataInterface) {
254             // Cannot be UnsupportedMetadataException because of BC with
255             // Symfony < 2.5
256             throw new ValidatorException(sprintf(
257                 'The metadata factory should return instances of '.
258                 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
259                 'got: "%s".',
260                 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
261             ));
262         }
263
264         $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
265         $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
266
267         if (is_object($objectOrClass)) {
268             $object = $objectOrClass;
269             $cacheKey = spl_object_hash($objectOrClass);
270             $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
271         } else {
272             // $objectOrClass contains a class name
273             $object = null;
274             $cacheKey = null;
275             $propertyPath = $this->defaultPropertyPath;
276         }
277
278         $previousValue = $this->context->getValue();
279         $previousObject = $this->context->getObject();
280         $previousMetadata = $this->context->getMetadata();
281         $previousPath = $this->context->getPropertyPath();
282         $previousGroup = $this->context->getGroup();
283
284         foreach ($propertyMetadatas as $propertyMetadata) {
285             $this->validateGenericNode(
286                 $value,
287                 $object,
288                 $cacheKey.':'.$propertyName,
289                 $propertyMetadata,
290                 $propertyPath,
291                 $groups,
292                 null,
293                 TraversalStrategy::IMPLICIT,
294                 $this->context
295             );
296         }
297
298         $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
299         $this->context->setGroup($previousGroup);
300
301         return $this;
302     }
303
304     /**
305      * {@inheritdoc}
306      */
307     public function getViolations()
308     {
309         return $this->context->getViolations();
310     }
311
312     /**
313      * Normalizes the given group or list of groups to an array.
314      *
315      * @param mixed $groups The groups to normalize
316      *
317      * @return array A group array
318      */
319     protected function normalizeGroups($groups)
320     {
321         if (is_array($groups)) {
322             return $groups;
323         }
324
325         return array($groups);
326     }
327
328     /**
329      * Validates an object against the constraints defined for its class.
330      *
331      * If no metadata is available for the class, but the class is an instance
332      * of {@link \Traversable} and the selected traversal strategy allows
333      * traversal, the object will be iterated and each nested object will be
334      * validated instead.
335      *
336      * @param object                    $object            The object to cascade
337      * @param string                    $propertyPath      The current property path
338      * @param string[]                  $groups            The validated groups
339      * @param int                       $traversalStrategy The strategy for traversing the
340      *                                                     cascaded object
341      * @param ExecutionContextInterface $context           The current execution context
342      *
343      * @throws NoSuchMetadataException      If the object has no associated metadata
344      *                                      and does not implement {@link \Traversable}
345      *                                      or if traversal is disabled via the
346      *                                      $traversalStrategy argument
347      * @throws UnsupportedMetadataException If the metadata returned by the
348      *                                      metadata factory does not implement
349      *                                      {@link ClassMetadataInterface}
350      */
351     private function validateObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context)
352     {
353         try {
354             $classMetadata = $this->metadataFactory->getMetadataFor($object);
355
356             if (!$classMetadata instanceof ClassMetadataInterface) {
357                 throw new UnsupportedMetadataException(sprintf(
358                     'The metadata factory should return instances of '.
359                     '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
360                     'got: "%s".',
361                     is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
362                 ));
363             }
364
365             $this->validateClassNode(
366                 $object,
367                 spl_object_hash($object),
368                 $classMetadata,
369                 $propertyPath,
370                 $groups,
371                 null,
372                 $traversalStrategy,
373                 $context
374             );
375         } catch (NoSuchMetadataException $e) {
376             // Rethrow if not Traversable
377             if (!$object instanceof \Traversable) {
378                 throw $e;
379             }
380
381             // Rethrow unless IMPLICIT or TRAVERSE
382             if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
383                 throw $e;
384             }
385
386             $this->validateEachObjectIn(
387                 $object,
388                 $propertyPath,
389                 $groups,
390                 $traversalStrategy & TraversalStrategy::STOP_RECURSION,
391                 $context
392             );
393         }
394     }
395
396     /**
397      * Validates each object in a collection against the constraints defined
398      * for their classes.
399      *
400      * If the parameter $recursive is set to true, nested {@link \Traversable}
401      * objects are iterated as well. Nested arrays are always iterated,
402      * regardless of the value of $recursive.
403      *
404      * @param array|\Traversable        $collection    The collection
405      * @param string                    $propertyPath  The current property path
406      * @param string[]                  $groups        The validated groups
407      * @param bool                      $stopRecursion Whether to disable
408      *                                                 recursive iteration. For
409      *                                                 backwards compatibility
410      *                                                 with Symfony < 2.5.
411      * @param ExecutionContextInterface $context       The current execution context
412      *
413      * @see ClassNode
414      * @see CollectionNode
415      */
416     private function validateEachObjectIn($collection, $propertyPath, array $groups, $stopRecursion, ExecutionContextInterface $context)
417     {
418         if ($stopRecursion) {
419             $traversalStrategy = TraversalStrategy::NONE;
420         } else {
421             $traversalStrategy = TraversalStrategy::IMPLICIT;
422         }
423
424         foreach ($collection as $key => $value) {
425             if (is_array($value)) {
426                 // Arrays are always cascaded, independent of the specified
427                 // traversal strategy
428                 // (BC with Symfony < 2.5)
429                 $this->validateEachObjectIn(
430                     $value,
431                     $propertyPath.'['.$key.']',
432                     $groups,
433                     $stopRecursion,
434                     $context
435                 );
436
437                 continue;
438             }
439
440             // Scalar and null values in the collection are ignored
441             // (BC with Symfony < 2.5)
442             if (is_object($value)) {
443                 $this->validateObject(
444                     $value,
445                     $propertyPath.'['.$key.']',
446                     $groups,
447                     $traversalStrategy,
448                     $context
449                 );
450             }
451         }
452     }
453
454     /**
455      * Validates a class node.
456      *
457      * A class node is a combination of an object with a {@link ClassMetadataInterface}
458      * instance. Each class node (conceptionally) has zero or more succeeding
459      * property nodes:
460      *
461      *     (Article:class node)
462      *                \
463      *        ($title:property node)
464      *
465      * This method validates the passed objects against all constraints defined
466      * at class level. It furthermore triggers the validation of each of the
467      * class' properties against the constraints for that property.
468      *
469      * If the selected traversal strategy allows traversal, the object is
470      * iterated and each nested object is validated against its own constraints.
471      * The object is not traversed if traversal is disabled in the class
472      * metadata.
473      *
474      * If the passed groups contain the group "Default", the validator will
475      * check whether the "Default" group has been replaced by a group sequence
476      * in the class metadata. If this is the case, the group sequence is
477      * validated instead.
478      *
479      * @param object                    $object            The validated object
480      * @param string                    $cacheKey          The key for caching
481      *                                                     the validated object
482      * @param ClassMetadataInterface    $metadata          The class metadata of
483      *                                                     the object
484      * @param string                    $propertyPath      The property path leading
485      *                                                     to the object
486      * @param string[]                  $groups            The groups in which the
487      *                                                     object should be validated
488      * @param string[]|null             $cascadedGroups    The groups in which
489      *                                                     cascaded objects should
490      *                                                     be validated
491      * @param int                       $traversalStrategy The strategy used for
492      *                                                     traversing the object
493      * @param ExecutionContextInterface $context           The current execution context
494      *
495      * @throws UnsupportedMetadataException  If a property metadata does not
496      *                                       implement {@link PropertyMetadataInterface}
497      * @throws ConstraintDefinitionException If traversal was enabled but the
498      *                                       object does not implement
499      *                                       {@link \Traversable}
500      *
501      * @see TraversalStrategy
502      */
503     private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
504     {
505         $context->setNode($object, $object, $metadata, $propertyPath);
506
507         if (!$context->isObjectInitialized($cacheKey)) {
508             foreach ($this->objectInitializers as $initializer) {
509                 $initializer->initialize($object);
510             }
511
512             $context->markObjectAsInitialized($cacheKey);
513         }
514
515         foreach ($groups as $key => $group) {
516             // If the "Default" group is replaced by a group sequence, remember
517             // to cascade the "Default" group when traversing the group
518             // sequence
519             $defaultOverridden = false;
520
521             // Use the object hash for group sequences
522             $groupHash = is_object($group) ? spl_object_hash($group) : $group;
523
524             if ($context->isGroupValidated($cacheKey, $groupHash)) {
525                 // Skip this group when validating the properties and when
526                 // traversing the object
527                 unset($groups[$key]);
528
529                 continue;
530             }
531
532             $context->markGroupAsValidated($cacheKey, $groupHash);
533
534             // Replace the "Default" group by the group sequence defined
535             // for the class, if applicable.
536             // This is done after checking the cache, so that
537             // spl_object_hash() isn't called for this sequence and
538             // "Default" is used instead in the cache. This is useful
539             // if the getters below return different group sequences in
540             // every call.
541             if (Constraint::DEFAULT_GROUP === $group) {
542                 if ($metadata->hasGroupSequence()) {
543                     // The group sequence is statically defined for the class
544                     $group = $metadata->getGroupSequence();
545                     $defaultOverridden = true;
546                 } elseif ($metadata->isGroupSequenceProvider()) {
547                     // The group sequence is dynamically obtained from the validated
548                     // object
549                     /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
550                     $group = $object->getGroupSequence();
551                     $defaultOverridden = true;
552
553                     if (!$group instanceof GroupSequence) {
554                         $group = new GroupSequence($group);
555                     }
556                 }
557             }
558
559             // If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
560             // (=<G1,G2>), then call validateClassNode() with each entry of the
561             // group sequence and abort if necessary (G1, G2)
562             if ($group instanceof GroupSequence) {
563                 $this->stepThroughGroupSequence(
564                      $object,
565                      $object,
566                      $cacheKey,
567                      $metadata,
568                      $propertyPath,
569                      $traversalStrategy,
570                      $group,
571                      $defaultOverridden ? Constraint::DEFAULT_GROUP : null,
572                      $context
573                 );
574
575                 // Skip the group sequence when validating properties, because
576                 // stepThroughGroupSequence() already validates the properties
577                 unset($groups[$key]);
578
579                 continue;
580             }
581
582             $this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
583         }
584
585         // If no more groups should be validated for the property nodes,
586         // we can safely quit
587         if (0 === count($groups)) {
588             return;
589         }
590
591         // Validate all properties against their constraints
592         foreach ($metadata->getConstrainedProperties() as $propertyName) {
593             // If constraints are defined both on the getter of a property as
594             // well as on the property itself, then getPropertyMetadata()
595             // returns two metadata objects, not just one
596             foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
597                 if (!$propertyMetadata instanceof PropertyMetadataInterface) {
598                     throw new UnsupportedMetadataException(sprintf(
599                         'The property metadata instances should implement '.
600                         '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '.
601                         'got: "%s".',
602                         is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata)
603                     ));
604                 }
605
606                 $propertyValue = $propertyMetadata->getPropertyValue($object);
607
608                 $this->validateGenericNode(
609                     $propertyValue,
610                     $object,
611                     $cacheKey.':'.$propertyName,
612                     $propertyMetadata,
613                     PropertyPath::append($propertyPath, $propertyName),
614                     $groups,
615                     $cascadedGroups,
616                     TraversalStrategy::IMPLICIT,
617                     $context
618                 );
619             }
620         }
621
622         // If no specific traversal strategy was requested when this method
623         // was called, use the traversal strategy of the class' metadata
624         if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
625             // Keep the STOP_RECURSION flag, if it was set
626             $traversalStrategy = $metadata->getTraversalStrategy()
627                 | ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
628         }
629
630         // Traverse only if IMPLICIT or TRAVERSE
631         if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
632             return;
633         }
634
635         // If IMPLICIT, stop unless we deal with a Traversable
636         if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
637             return;
638         }
639
640         // If TRAVERSE, fail if we have no Traversable
641         if (!$object instanceof \Traversable) {
642             // Must throw a ConstraintDefinitionException for backwards
643             // compatibility reasons with Symfony < 2.5
644             throw new ConstraintDefinitionException(sprintf(
645                 'Traversal was enabled for "%s", but this class '.
646                 'does not implement "\Traversable".',
647                 get_class($object)
648             ));
649         }
650
651         $this->validateEachObjectIn(
652             $object,
653             $propertyPath,
654             $groups,
655             $traversalStrategy & TraversalStrategy::STOP_RECURSION,
656             $context
657         );
658     }
659
660     /**
661      * Validates a node that is not a class node.
662      *
663      * Currently, two such node types exist:
664      *
665      *  - property nodes, which consist of the value of an object's
666      *    property together with a {@link PropertyMetadataInterface} instance
667      *  - generic nodes, which consist of a value and some arbitrary
668      *    constraints defined in a {@link MetadataInterface} container
669      *
670      * In both cases, the value is validated against all constraints defined
671      * in the passed metadata object. Then, if the value is an instance of
672      * {@link \Traversable} and the selected traversal strategy permits it,
673      * the value is traversed and each nested object validated against its own
674      * constraints. Arrays are always traversed.
675      *
676      * @param mixed                     $value             The validated value
677      * @param object|null               $object            The current object
678      * @param string                    $cacheKey          The key for caching
679      *                                                     the validated value
680      * @param MetadataInterface         $metadata          The metadata of the
681      *                                                     value
682      * @param string                    $propertyPath      The property path leading
683      *                                                     to the value
684      * @param string[]                  $groups            The groups in which the
685      *                                                     value should be validated
686      * @param string[]|null             $cascadedGroups    The groups in which
687      *                                                     cascaded objects should
688      *                                                     be validated
689      * @param int                       $traversalStrategy The strategy used for
690      *                                                     traversing the value
691      * @param ExecutionContextInterface $context           The current execution context
692      *
693      * @see TraversalStrategy
694      */
695     private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
696     {
697         $context->setNode($value, $object, $metadata, $propertyPath);
698
699         foreach ($groups as $key => $group) {
700             if ($group instanceof GroupSequence) {
701                 $this->stepThroughGroupSequence(
702                      $value,
703                      $object,
704                      $cacheKey,
705                      $metadata,
706                      $propertyPath,
707                      $traversalStrategy,
708                      $group,
709                      null,
710                      $context
711                 );
712
713                 // Skip the group sequence when cascading, as the cascading
714                 // logic is already done in stepThroughGroupSequence()
715                 unset($groups[$key]);
716
717                 continue;
718             }
719
720             $this->validateInGroup($value, $cacheKey, $metadata, $group, $context);
721         }
722
723         if (0 === count($groups)) {
724             return;
725         }
726
727         if (null === $value) {
728             return;
729         }
730
731         $cascadingStrategy = $metadata->getCascadingStrategy();
732
733         // Quit unless we have an array or a cascaded object
734         if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) {
735             return;
736         }
737
738         // If no specific traversal strategy was requested when this method
739         // was called, use the traversal strategy of the node's metadata
740         if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
741             // Keep the STOP_RECURSION flag, if it was set
742             $traversalStrategy = $metadata->getTraversalStrategy()
743                 | ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
744         }
745
746         // The $cascadedGroups property is set, if the "Default" group is
747         // overridden by a group sequence
748         // See validateClassNode()
749         $cascadedGroups = null !== $cascadedGroups && count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
750
751         if (is_array($value)) {
752             // Arrays are always traversed, independent of the specified
753             // traversal strategy
754             // (BC with Symfony < 2.5)
755             $this->validateEachObjectIn(
756                 $value,
757                 $propertyPath,
758                 $cascadedGroups,
759                 $traversalStrategy & TraversalStrategy::STOP_RECURSION,
760                 $context
761             );
762
763             return;
764         }
765
766         // If the value is a scalar, pass it anyway, because we want
767         // a NoSuchMetadataException to be thrown in that case
768         // (BC with Symfony < 2.5)
769         $this->validateObject(
770             $value,
771             $propertyPath,
772             $cascadedGroups,
773             $traversalStrategy,
774             $context
775         );
776
777         // Currently, the traversal strategy can only be TRAVERSE for a
778         // generic node if the cascading strategy is CASCADE. Thus, traversable
779         // objects will always be handled within validateObject() and there's
780         // nothing more to do here.
781
782         // see GenericMetadata::addConstraint()
783     }
784
785     /**
786      * Sequentially validates a node's value in each group of a group sequence.
787      *
788      * If any of the constraints generates a violation, subsequent groups in the
789      * group sequence are skipped.
790      *
791      * @param mixed                     $value             The validated value
792      * @param object|null               $object            The current object
793      * @param string                    $cacheKey          The key for caching
794      *                                                     the validated value
795      * @param MetadataInterface         $metadata          The metadata of the
796      *                                                     value
797      * @param string                    $propertyPath      The property path leading
798      *                                                     to the value
799      * @param int                       $traversalStrategy The strategy used for
800      *                                                     traversing the value
801      * @param GroupSequence             $groupSequence     The group sequence
802      * @param string|null               $cascadedGroup     The group that should
803      *                                                     be passed to cascaded
804      *                                                     objects instead of
805      *                                                     the group sequence
806      * @param ExecutionContextInterface $context           The execution context
807      */
808     private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
809     {
810         $violationCount = count($context->getViolations());
811         $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null;
812
813         foreach ($groupSequence->groups as $groupInSequence) {
814             $groups = array($groupInSequence);
815
816             if ($metadata instanceof ClassMetadataInterface) {
817                 $this->validateClassNode(
818                      $value,
819                      $cacheKey,
820                      $metadata,
821                      $propertyPath,
822                      $groups,
823                      $cascadedGroups,
824                      $traversalStrategy,
825                      $context
826                 );
827             } else {
828                 $this->validateGenericNode(
829                      $value,
830                      $object,
831                      $cacheKey,
832                      $metadata,
833                      $propertyPath,
834                      $groups,
835                      $cascadedGroups,
836                      $traversalStrategy,
837                      $context
838                 );
839             }
840
841             // Abort sequence validation if a violation was generated
842             if (count($context->getViolations()) > $violationCount) {
843                 break;
844             }
845         }
846     }
847
848     /**
849      * Validates a node's value against all constraints in the given group.
850      *
851      * @param mixed                     $value    The validated value
852      * @param string                    $cacheKey The key for caching the
853      *                                            validated value
854      * @param MetadataInterface         $metadata The metadata of the value
855      * @param string                    $group    The group to validate
856      * @param ExecutionContextInterface $context  The execution context
857      */
858     private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context)
859     {
860         $context->setGroup($group);
861
862         foreach ($metadata->findConstraints($group) as $constraint) {
863             // Prevent duplicate validation of constraints, in the case
864             // that constraints belong to multiple validated groups
865             if (null !== $cacheKey) {
866                 $constraintHash = spl_object_hash($constraint);
867
868                 if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
869                     continue;
870                 }
871
872                 $context->markConstraintAsValidated($cacheKey, $constraintHash);
873             }
874
875             $context->setConstraint($constraint);
876
877             $validator = $this->validatorFactory->getInstance($constraint);
878             $validator->initialize($context);
879             $validator->validate($value, $constraint);
880         }
881     }
882 }