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