838d12b4033fdfd4870ffc41631a252796d33869
[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\Mapping\Factory\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                 $this->context
179             );
180
181             $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
182             $this->context->setGroup($previousGroup);
183
184             return $this;
185         }
186
187         throw new RuntimeException(sprintf(
188             'Cannot validate values of type "%s" automatically. Please '.
189             'provide a constraint.',
190             gettype($value)
191         ));
192     }
193
194     /**
195      * {@inheritdoc}
196      */
197     public function validateProperty($object, $propertyName, $groups = null)
198     {
199         $classMetadata = $this->metadataFactory->getMetadataFor($object);
200
201         if (!$classMetadata instanceof ClassMetadataInterface) {
202             throw new ValidatorException(sprintf(
203                 'The metadata factory should return instances of '.
204                 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
205                 'got: "%s".',
206                 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
207             ));
208         }
209
210         $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
211         $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
212         $cacheKey = spl_object_hash($object);
213         $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
214
215         $previousValue = $this->context->getValue();
216         $previousObject = $this->context->getObject();
217         $previousMetadata = $this->context->getMetadata();
218         $previousPath = $this->context->getPropertyPath();
219         $previousGroup = $this->context->getGroup();
220
221         foreach ($propertyMetadatas as $propertyMetadata) {
222             $propertyValue = $propertyMetadata->getPropertyValue($object);
223
224             $this->validateGenericNode(
225                 $propertyValue,
226                 $object,
227                 $cacheKey.':'.get_class($object).':'.$propertyName,
228                 $propertyMetadata,
229                 $propertyPath,
230                 $groups,
231                 null,
232                 TraversalStrategy::IMPLICIT,
233                 $this->context
234             );
235         }
236
237         $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
238         $this->context->setGroup($previousGroup);
239
240         return $this;
241     }
242
243     /**
244      * {@inheritdoc}
245      */
246     public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
247     {
248         $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
249
250         if (!$classMetadata instanceof ClassMetadataInterface) {
251             throw new ValidatorException(sprintf(
252                 'The metadata factory should return instances of '.
253                 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
254                 'got: "%s".',
255                 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
256             ));
257         }
258
259         $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
260         $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
261
262         if (is_object($objectOrClass)) {
263             $object = $objectOrClass;
264             $cacheKey = spl_object_hash($objectOrClass);
265             $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
266         } else {
267             // $objectOrClass contains a class name
268             $object = null;
269             $cacheKey = null;
270             $propertyPath = $this->defaultPropertyPath;
271         }
272
273         $previousValue = $this->context->getValue();
274         $previousObject = $this->context->getObject();
275         $previousMetadata = $this->context->getMetadata();
276         $previousPath = $this->context->getPropertyPath();
277         $previousGroup = $this->context->getGroup();
278
279         foreach ($propertyMetadatas as $propertyMetadata) {
280             $this->validateGenericNode(
281                 $value,
282                 $object,
283                 $cacheKey.':'.get_class($object).':'.$propertyName,
284                 $propertyMetadata,
285                 $propertyPath,
286                 $groups,
287                 null,
288                 TraversalStrategy::IMPLICIT,
289                 $this->context
290             );
291         }
292
293         $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
294         $this->context->setGroup($previousGroup);
295
296         return $this;
297     }
298
299     /**
300      * {@inheritdoc}
301      */
302     public function getViolations()
303     {
304         return $this->context->getViolations();
305     }
306
307     /**
308      * Normalizes the given group or list of groups to an array.
309      *
310      * @param mixed $groups The groups to normalize
311      *
312      * @return array A group array
313      */
314     protected function normalizeGroups($groups)
315     {
316         if (is_array($groups)) {
317             return $groups;
318         }
319
320         return array($groups);
321     }
322
323     /**
324      * Validates an object against the constraints defined for its class.
325      *
326      * If no metadata is available for the class, but the class is an instance
327      * of {@link \Traversable} and the selected traversal strategy allows
328      * traversal, the object will be iterated and each nested object will be
329      * validated instead.
330      *
331      * @param object                    $object            The object to cascade
332      * @param string                    $propertyPath      The current property path
333      * @param string[]                  $groups            The validated groups
334      * @param int                       $traversalStrategy The strategy for traversing the
335      *                                                     cascaded object
336      * @param ExecutionContextInterface $context           The current execution context
337      *
338      * @throws NoSuchMetadataException      If the object has no associated metadata
339      *                                      and does not implement {@link \Traversable}
340      *                                      or if traversal is disabled via the
341      *                                      $traversalStrategy argument
342      * @throws UnsupportedMetadataException If the metadata returned by the
343      *                                      metadata factory does not implement
344      *                                      {@link ClassMetadataInterface}
345      */
346     private function validateObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context)
347     {
348         try {
349             $classMetadata = $this->metadataFactory->getMetadataFor($object);
350
351             if (!$classMetadata instanceof ClassMetadataInterface) {
352                 throw new UnsupportedMetadataException(sprintf(
353                     'The metadata factory should return instances of '.
354                     '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
355                     'got: "%s".',
356                     is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
357                 ));
358             }
359
360             $this->validateClassNode(
361                 $object,
362                 spl_object_hash($object),
363                 $classMetadata,
364                 $propertyPath,
365                 $groups,
366                 null,
367                 $traversalStrategy,
368                 $context
369             );
370         } catch (NoSuchMetadataException $e) {
371             // Rethrow if not Traversable
372             if (!$object instanceof \Traversable) {
373                 throw $e;
374             }
375
376             // Rethrow unless IMPLICIT or TRAVERSE
377             if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
378                 throw $e;
379             }
380
381             $this->validateEachObjectIn(
382                 $object,
383                 $propertyPath,
384                 $groups,
385                 $context
386             );
387         }
388     }
389
390     /**
391      * Validates each object in a collection against the constraints defined
392      * for their classes.
393      *
394      * If the parameter $recursive is set to true, nested {@link \Traversable}
395      * objects are iterated as well. Nested arrays are always iterated,
396      * regardless of the value of $recursive.
397      *
398      * @param array|\Traversable        $collection   The collection
399      * @param string                    $propertyPath The current property path
400      * @param string[]                  $groups       The validated groups
401      * @param ExecutionContextInterface $context      The current execution context
402      *
403      * @see ClassNode
404      * @see CollectionNode
405      */
406     private function validateEachObjectIn($collection, $propertyPath, array $groups, ExecutionContextInterface $context)
407     {
408         foreach ($collection as $key => $value) {
409             if (is_array($value)) {
410                 // Arrays are always cascaded, independent of the specified
411                 // traversal strategy
412                 $this->validateEachObjectIn(
413                     $value,
414                     $propertyPath.'['.$key.']',
415                     $groups,
416                     $context
417                 );
418
419                 continue;
420             }
421
422             // Scalar and null values in the collection are ignored
423             if (is_object($value)) {
424                 $this->validateObject(
425                     $value,
426                     $propertyPath.'['.$key.']',
427                     $groups,
428                     TraversalStrategy::IMPLICIT,
429                     $context
430                 );
431             }
432         }
433     }
434
435     /**
436      * Validates a class node.
437      *
438      * A class node is a combination of an object with a {@link ClassMetadataInterface}
439      * instance. Each class node (conceptionally) has zero or more succeeding
440      * property nodes:
441      *
442      *     (Article:class node)
443      *                \
444      *        ($title:property node)
445      *
446      * This method validates the passed objects against all constraints defined
447      * at class level. It furthermore triggers the validation of each of the
448      * class' properties against the constraints for that property.
449      *
450      * If the selected traversal strategy allows traversal, the object is
451      * iterated and each nested object is validated against its own constraints.
452      * The object is not traversed if traversal is disabled in the class
453      * metadata.
454      *
455      * If the passed groups contain the group "Default", the validator will
456      * check whether the "Default" group has been replaced by a group sequence
457      * in the class metadata. If this is the case, the group sequence is
458      * validated instead.
459      *
460      * @param object                    $object            The validated object
461      * @param string                    $cacheKey          The key for caching
462      *                                                     the validated object
463      * @param ClassMetadataInterface    $metadata          The class metadata of
464      *                                                     the object
465      * @param string                    $propertyPath      The property path leading
466      *                                                     to the object
467      * @param string[]                  $groups            The groups in which the
468      *                                                     object should be validated
469      * @param string[]|null             $cascadedGroups    The groups in which
470      *                                                     cascaded objects should
471      *                                                     be validated
472      * @param int                       $traversalStrategy The strategy used for
473      *                                                     traversing the object
474      * @param ExecutionContextInterface $context           The current execution context
475      *
476      * @throws UnsupportedMetadataException  If a property metadata does not
477      *                                       implement {@link PropertyMetadataInterface}
478      * @throws ConstraintDefinitionException If traversal was enabled but the
479      *                                       object does not implement
480      *                                       {@link \Traversable}
481      *
482      * @see TraversalStrategy
483      */
484     private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
485     {
486         $context->setNode($object, $object, $metadata, $propertyPath);
487
488         if (!$context->isObjectInitialized($cacheKey)) {
489             foreach ($this->objectInitializers as $initializer) {
490                 $initializer->initialize($object);
491             }
492
493             $context->markObjectAsInitialized($cacheKey);
494         }
495
496         foreach ($groups as $key => $group) {
497             // If the "Default" group is replaced by a group sequence, remember
498             // to cascade the "Default" group when traversing the group
499             // sequence
500             $defaultOverridden = false;
501
502             // Use the object hash for group sequences
503             $groupHash = is_object($group) ? spl_object_hash($group) : $group;
504
505             if ($context->isGroupValidated($cacheKey, $groupHash)) {
506                 // Skip this group when validating the properties and when
507                 // traversing the object
508                 unset($groups[$key]);
509
510                 continue;
511             }
512
513             $context->markGroupAsValidated($cacheKey, $groupHash);
514
515             // Replace the "Default" group by the group sequence defined
516             // for the class, if applicable.
517             // This is done after checking the cache, so that
518             // spl_object_hash() isn't called for this sequence and
519             // "Default" is used instead in the cache. This is useful
520             // if the getters below return different group sequences in
521             // every call.
522             if (Constraint::DEFAULT_GROUP === $group) {
523                 if ($metadata->hasGroupSequence()) {
524                     // The group sequence is statically defined for the class
525                     $group = $metadata->getGroupSequence();
526                     $defaultOverridden = true;
527                 } elseif ($metadata->isGroupSequenceProvider()) {
528                     // The group sequence is dynamically obtained from the validated
529                     // object
530                     /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
531                     $group = $object->getGroupSequence();
532                     $defaultOverridden = true;
533
534                     if (!$group instanceof GroupSequence) {
535                         $group = new GroupSequence($group);
536                     }
537                 }
538             }
539
540             // If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
541             // (=<G1,G2>), then call validateClassNode() with each entry of the
542             // group sequence and abort if necessary (G1, G2)
543             if ($group instanceof GroupSequence) {
544                 $this->stepThroughGroupSequence(
545                      $object,
546                      $object,
547                      $cacheKey,
548                      $metadata,
549                      $propertyPath,
550                      $traversalStrategy,
551                      $group,
552                      $defaultOverridden ? Constraint::DEFAULT_GROUP : null,
553                      $context
554                 );
555
556                 // Skip the group sequence when validating properties, because
557                 // stepThroughGroupSequence() already validates the properties
558                 unset($groups[$key]);
559
560                 continue;
561             }
562
563             $this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
564         }
565
566         // If no more groups should be validated for the property nodes,
567         // we can safely quit
568         if (0 === count($groups)) {
569             return;
570         }
571
572         // Validate all properties against their constraints
573         foreach ($metadata->getConstrainedProperties() as $propertyName) {
574             // If constraints are defined both on the getter of a property as
575             // well as on the property itself, then getPropertyMetadata()
576             // returns two metadata objects, not just one
577             foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
578                 if (!$propertyMetadata instanceof PropertyMetadataInterface) {
579                     throw new UnsupportedMetadataException(sprintf(
580                         'The property metadata instances should implement '.
581                         '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '.
582                         'got: "%s".',
583                         is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata)
584                     ));
585                 }
586
587                 $propertyValue = $propertyMetadata->getPropertyValue($object);
588
589                 $this->validateGenericNode(
590                     $propertyValue,
591                     $object,
592                     $cacheKey.':'.get_class($object).':'.$propertyName,
593                     $propertyMetadata,
594                     PropertyPath::append($propertyPath, $propertyName),
595                     $groups,
596                     $cascadedGroups,
597                     TraversalStrategy::IMPLICIT,
598                     $context
599                 );
600             }
601         }
602
603         // If no specific traversal strategy was requested when this method
604         // was called, use the traversal strategy of the class' metadata
605         if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
606             $traversalStrategy = $metadata->getTraversalStrategy();
607         }
608
609         // Traverse only if IMPLICIT or TRAVERSE
610         if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
611             return;
612         }
613
614         // If IMPLICIT, stop unless we deal with a Traversable
615         if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
616             return;
617         }
618
619         // If TRAVERSE, fail if we have no Traversable
620         if (!$object instanceof \Traversable) {
621             throw new ConstraintDefinitionException(sprintf(
622                 'Traversal was enabled for "%s", but this class '.
623                 'does not implement "\Traversable".',
624                 get_class($object)
625             ));
626         }
627
628         $this->validateEachObjectIn(
629             $object,
630             $propertyPath,
631             $groups,
632             $context
633         );
634     }
635
636     /**
637      * Validates a node that is not a class node.
638      *
639      * Currently, two such node types exist:
640      *
641      *  - property nodes, which consist of the value of an object's
642      *    property together with a {@link PropertyMetadataInterface} instance
643      *  - generic nodes, which consist of a value and some arbitrary
644      *    constraints defined in a {@link MetadataInterface} container
645      *
646      * In both cases, the value is validated against all constraints defined
647      * in the passed metadata object. Then, if the value is an instance of
648      * {@link \Traversable} and the selected traversal strategy permits it,
649      * the value is traversed and each nested object validated against its own
650      * constraints. Arrays are always traversed.
651      *
652      * @param mixed                     $value             The validated value
653      * @param object|null               $object            The current object
654      * @param string                    $cacheKey          The key for caching
655      *                                                     the validated value
656      * @param MetadataInterface         $metadata          The metadata of the
657      *                                                     value
658      * @param string                    $propertyPath      The property path leading
659      *                                                     to the value
660      * @param string[]                  $groups            The groups in which the
661      *                                                     value should be validated
662      * @param string[]|null             $cascadedGroups    The groups in which
663      *                                                     cascaded objects should
664      *                                                     be validated
665      * @param int                       $traversalStrategy The strategy used for
666      *                                                     traversing the value
667      * @param ExecutionContextInterface $context           The current execution context
668      *
669      * @see TraversalStrategy
670      */
671     private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
672     {
673         $context->setNode($value, $object, $metadata, $propertyPath);
674
675         foreach ($groups as $key => $group) {
676             if ($group instanceof GroupSequence) {
677                 $this->stepThroughGroupSequence(
678                      $value,
679                      $object,
680                      $cacheKey,
681                      $metadata,
682                      $propertyPath,
683                      $traversalStrategy,
684                      $group,
685                      null,
686                      $context
687                 );
688
689                 // Skip the group sequence when cascading, as the cascading
690                 // logic is already done in stepThroughGroupSequence()
691                 unset($groups[$key]);
692
693                 continue;
694             }
695
696             $this->validateInGroup($value, $cacheKey, $metadata, $group, $context);
697         }
698
699         if (0 === count($groups)) {
700             return;
701         }
702
703         if (null === $value) {
704             return;
705         }
706
707         $cascadingStrategy = $metadata->getCascadingStrategy();
708
709         // Quit unless we have an array or a cascaded object
710         if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) {
711             return;
712         }
713
714         // If no specific traversal strategy was requested when this method
715         // was called, use the traversal strategy of the node's metadata
716         if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
717             $traversalStrategy = $metadata->getTraversalStrategy();
718         }
719
720         // The $cascadedGroups property is set, if the "Default" group is
721         // overridden by a group sequence
722         // See validateClassNode()
723         $cascadedGroups = null !== $cascadedGroups && count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
724
725         if (is_array($value)) {
726             // Arrays are always traversed, independent of the specified
727             // traversal strategy
728             $this->validateEachObjectIn(
729                 $value,
730                 $propertyPath,
731                 $cascadedGroups,
732                 $context
733             );
734
735             return;
736         }
737
738         // If the value is a scalar, pass it anyway, because we want
739         // a NoSuchMetadataException to be thrown in that case
740         $this->validateObject(
741             $value,
742             $propertyPath,
743             $cascadedGroups,
744             $traversalStrategy,
745             $context
746         );
747
748         // Currently, the traversal strategy can only be TRAVERSE for a
749         // generic node if the cascading strategy is CASCADE. Thus, traversable
750         // objects will always be handled within validateObject() and there's
751         // nothing more to do here.
752
753         // see GenericMetadata::addConstraint()
754     }
755
756     /**
757      * Sequentially validates a node's value in each group of a group sequence.
758      *
759      * If any of the constraints generates a violation, subsequent groups in the
760      * group sequence are skipped.
761      *
762      * @param mixed                     $value             The validated value
763      * @param object|null               $object            The current object
764      * @param string                    $cacheKey          The key for caching
765      *                                                     the validated value
766      * @param MetadataInterface         $metadata          The metadata of the
767      *                                                     value
768      * @param string                    $propertyPath      The property path leading
769      *                                                     to the value
770      * @param int                       $traversalStrategy The strategy used for
771      *                                                     traversing the value
772      * @param GroupSequence             $groupSequence     The group sequence
773      * @param string|null               $cascadedGroup     The group that should
774      *                                                     be passed to cascaded
775      *                                                     objects instead of
776      *                                                     the group sequence
777      * @param ExecutionContextInterface $context           The execution context
778      */
779     private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
780     {
781         $violationCount = count($context->getViolations());
782         $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null;
783
784         foreach ($groupSequence->groups as $groupInSequence) {
785             $groups = (array) $groupInSequence;
786
787             if ($metadata instanceof ClassMetadataInterface) {
788                 $this->validateClassNode(
789                      $value,
790                      $cacheKey,
791                      $metadata,
792                      $propertyPath,
793                      $groups,
794                      $cascadedGroups,
795                      $traversalStrategy,
796                      $context
797                 );
798             } else {
799                 $this->validateGenericNode(
800                      $value,
801                      $object,
802                      $cacheKey,
803                      $metadata,
804                      $propertyPath,
805                      $groups,
806                      $cascadedGroups,
807                      $traversalStrategy,
808                      $context
809                 );
810             }
811
812             // Abort sequence validation if a violation was generated
813             if (count($context->getViolations()) > $violationCount) {
814                 break;
815             }
816         }
817     }
818
819     /**
820      * Validates a node's value against all constraints in the given group.
821      *
822      * @param mixed                     $value    The validated value
823      * @param string                    $cacheKey The key for caching the
824      *                                            validated value
825      * @param MetadataInterface         $metadata The metadata of the value
826      * @param string                    $group    The group to validate
827      * @param ExecutionContextInterface $context  The execution context
828      */
829     private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context)
830     {
831         $context->setGroup($group);
832
833         foreach ($metadata->findConstraints($group) as $constraint) {
834             // Prevent duplicate validation of constraints, in the case
835             // that constraints belong to multiple validated groups
836             if (null !== $cacheKey) {
837                 $constraintHash = spl_object_hash($constraint);
838
839                 if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
840                     continue;
841                 }
842
843                 $context->markConstraintAsValidated($cacheKey, $constraintHash);
844             }
845
846             $context->setConstraint($constraint);
847
848             $validator = $this->validatorFactory->getInstance($constraint);
849             $validator->initialize($context);
850             $validator->validate($value, $constraint);
851         }
852     }
853 }