vendor/symfony/validator/Validator/RecursiveContextualValidator.php line 306

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Validator\Validator;
  11. use Symfony\Component\Validator\Constraint;
  12. use Symfony\Component\Validator\Constraints\Composite;
  13. use Symfony\Component\Validator\Constraints\Existence;
  14. use Symfony\Component\Validator\Constraints\GroupSequence;
  15. use Symfony\Component\Validator\Constraints\Valid;
  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\UnexpectedValueException;
  23. use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
  24. use Symfony\Component\Validator\Exception\ValidatorException;
  25. use Symfony\Component\Validator\Mapping\CascadingStrategy;
  26. use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
  27. use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
  28. use Symfony\Component\Validator\Mapping\GenericMetadata;
  29. use Symfony\Component\Validator\Mapping\GetterMetadata;
  30. use Symfony\Component\Validator\Mapping\MetadataInterface;
  31. use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
  32. use Symfony\Component\Validator\Mapping\TraversalStrategy;
  33. use Symfony\Component\Validator\ObjectInitializerInterface;
  34. use Symfony\Component\Validator\Util\PropertyPath;
  35. /**
  36.  * Recursive implementation of {@link ContextualValidatorInterface}.
  37.  *
  38.  * @author Bernhard Schussek <bschussek@gmail.com>
  39.  */
  40. class RecursiveContextualValidator implements ContextualValidatorInterface
  41. {
  42.     private $context;
  43.     private $defaultPropertyPath;
  44.     private $defaultGroups;
  45.     private $metadataFactory;
  46.     private $validatorFactory;
  47.     private $objectInitializers;
  48.     /**
  49.      * Creates a validator for the given context.
  50.      *
  51.      * @param ObjectInitializerInterface[] $objectInitializers The object initializers
  52.      */
  53.     public function __construct(ExecutionContextInterface $contextMetadataFactoryInterface $metadataFactoryConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = [])
  54.     {
  55.         $this->context $context;
  56.         $this->defaultPropertyPath $context->getPropertyPath();
  57.         $this->defaultGroups = [$context->getGroup() ?: Constraint::DEFAULT_GROUP];
  58.         $this->metadataFactory $metadataFactory;
  59.         $this->validatorFactory $validatorFactory;
  60.         $this->objectInitializers $objectInitializers;
  61.     }
  62.     /**
  63.      * {@inheritdoc}
  64.      */
  65.     public function atPath(string $path)
  66.     {
  67.         $this->defaultPropertyPath $this->context->getPropertyPath($path);
  68.         return $this;
  69.     }
  70.     /**
  71.      * {@inheritdoc}
  72.      */
  73.     public function validate($value$constraints null$groups null)
  74.     {
  75.         $groups $groups $this->normalizeGroups($groups) : $this->defaultGroups;
  76.         $previousValue $this->context->getValue();
  77.         $previousObject $this->context->getObject();
  78.         $previousMetadata $this->context->getMetadata();
  79.         $previousPath $this->context->getPropertyPath();
  80.         $previousGroup $this->context->getGroup();
  81.         $previousConstraint null;
  82.         if ($this->context instanceof ExecutionContext || method_exists($this->context'getConstraint')) {
  83.             $previousConstraint $this->context->getConstraint();
  84.         }
  85.         // If explicit constraints are passed, validate the value against
  86.         // those constraints
  87.         if (null !== $constraints) {
  88.             // You can pass a single constraint or an array of constraints
  89.             // Make sure to deal with an array in the rest of the code
  90.             if (!\is_array($constraints)) {
  91.                 $constraints = [$constraints];
  92.             }
  93.             $metadata = new GenericMetadata();
  94.             $metadata->addConstraints($constraints);
  95.             $this->validateGenericNode(
  96.                 $value,
  97.                 $previousObject,
  98.                 \is_object($value) ? $this->generateCacheKey($value) : null,
  99.                 $metadata,
  100.                 $this->defaultPropertyPath,
  101.                 $groups,
  102.                 null,
  103.                 TraversalStrategy::IMPLICIT,
  104.                 $this->context
  105.             );
  106.             $this->context->setNode($previousValue$previousObject$previousMetadata$previousPath);
  107.             $this->context->setGroup($previousGroup);
  108.             if (null !== $previousConstraint) {
  109.                 $this->context->setConstraint($previousConstraint);
  110.             }
  111.             return $this;
  112.         }
  113.         // If an object is passed without explicit constraints, validate that
  114.         // object against the constraints defined for the object's class
  115.         if (\is_object($value)) {
  116.             $this->validateObject(
  117.                 $value,
  118.                 $this->defaultPropertyPath,
  119.                 $groups,
  120.                 TraversalStrategy::IMPLICIT,
  121.                 $this->context
  122.             );
  123.             $this->context->setNode($previousValue$previousObject$previousMetadata$previousPath);
  124.             $this->context->setGroup($previousGroup);
  125.             return $this;
  126.         }
  127.         // If an array is passed without explicit constraints, validate each
  128.         // object in the array
  129.         if (\is_array($value)) {
  130.             $this->validateEachObjectIn(
  131.                 $value,
  132.                 $this->defaultPropertyPath,
  133.                 $groups,
  134.                 $this->context
  135.             );
  136.             $this->context->setNode($previousValue$previousObject$previousMetadata$previousPath);
  137.             $this->context->setGroup($previousGroup);
  138.             return $this;
  139.         }
  140.         throw new RuntimeException(sprintf('Cannot validate values of type "%s" automatically. Please provide a constraint.'get_debug_type($value)));
  141.     }
  142.     /**
  143.      * {@inheritdoc}
  144.      */
  145.     public function validateProperty(object $objectstring $propertyName$groups null)
  146.     {
  147.         $classMetadata $this->metadataFactory->getMetadataFor($object);
  148.         if (!$classMetadata instanceof ClassMetadataInterface) {
  149.             throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".'get_debug_type($classMetadata)));
  150.         }
  151.         $propertyMetadatas $classMetadata->getPropertyMetadata($propertyName);
  152.         $groups $groups $this->normalizeGroups($groups) : $this->defaultGroups;
  153.         $cacheKey $this->generateCacheKey($object);
  154.         $propertyPath PropertyPath::append($this->defaultPropertyPath$propertyName);
  155.         $previousValue $this->context->getValue();
  156.         $previousObject $this->context->getObject();
  157.         $previousMetadata $this->context->getMetadata();
  158.         $previousPath $this->context->getPropertyPath();
  159.         $previousGroup $this->context->getGroup();
  160.         foreach ($propertyMetadatas as $propertyMetadata) {
  161.             $propertyValue $propertyMetadata->getPropertyValue($object);
  162.             $this->validateGenericNode(
  163.                 $propertyValue,
  164.                 $object,
  165.                 $cacheKey.':'.\get_class($object).':'.$propertyName,
  166.                 $propertyMetadata,
  167.                 $propertyPath,
  168.                 $groups,
  169.                 null,
  170.                 TraversalStrategy::IMPLICIT,
  171.                 $this->context
  172.             );
  173.         }
  174.         $this->context->setNode($previousValue$previousObject$previousMetadata$previousPath);
  175.         $this->context->setGroup($previousGroup);
  176.         return $this;
  177.     }
  178.     /**
  179.      * {@inheritdoc}
  180.      */
  181.     public function validatePropertyValue($objectOrClassstring $propertyName$value$groups null)
  182.     {
  183.         $classMetadata $this->metadataFactory->getMetadataFor($objectOrClass);
  184.         if (!$classMetadata instanceof ClassMetadataInterface) {
  185.             throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".'get_debug_type($classMetadata)));
  186.         }
  187.         $propertyMetadatas $classMetadata->getPropertyMetadata($propertyName);
  188.         $groups $groups $this->normalizeGroups($groups) : $this->defaultGroups;
  189.         if (\is_object($objectOrClass)) {
  190.             $object $objectOrClass;
  191.             $class \get_class($object);
  192.             $cacheKey $this->generateCacheKey($objectOrClass);
  193.             $propertyPath PropertyPath::append($this->defaultPropertyPath$propertyName);
  194.         } else {
  195.             // $objectOrClass contains a class name
  196.             $object null;
  197.             $class $objectOrClass;
  198.             $cacheKey null;
  199.             $propertyPath $this->defaultPropertyPath;
  200.         }
  201.         $previousValue $this->context->getValue();
  202.         $previousObject $this->context->getObject();
  203.         $previousMetadata $this->context->getMetadata();
  204.         $previousPath $this->context->getPropertyPath();
  205.         $previousGroup $this->context->getGroup();
  206.         foreach ($propertyMetadatas as $propertyMetadata) {
  207.             $this->validateGenericNode(
  208.                 $value,
  209.                 $object,
  210.                 $cacheKey.':'.$class.':'.$propertyName,
  211.                 $propertyMetadata,
  212.                 $propertyPath,
  213.                 $groups,
  214.                 null,
  215.                 TraversalStrategy::IMPLICIT,
  216.                 $this->context
  217.             );
  218.         }
  219.         $this->context->setNode($previousValue$previousObject$previousMetadata$previousPath);
  220.         $this->context->setGroup($previousGroup);
  221.         return $this;
  222.     }
  223.     /**
  224.      * {@inheritdoc}
  225.      */
  226.     public function getViolations()
  227.     {
  228.         return $this->context->getViolations();
  229.     }
  230.     /**
  231.      * Normalizes the given group or list of groups to an array.
  232.      *
  233.      * @param string|GroupSequence|array<string|GroupSequence> $groups The groups to normalize
  234.      *
  235.      * @return array<string|GroupSequence>
  236.      */
  237.     protected function normalizeGroups($groups)
  238.     {
  239.         if (\is_array($groups)) {
  240.             return $groups;
  241.         }
  242.         return [$groups];
  243.     }
  244.     /**
  245.      * Validates an object against the constraints defined for its class.
  246.      *
  247.      * If no metadata is available for the class, but the class is an instance
  248.      * of {@link \Traversable} and the selected traversal strategy allows
  249.      * traversal, the object will be iterated and each nested object will be
  250.      * validated instead.
  251.      *
  252.      * @throws NoSuchMetadataException      If the object has no associated metadata
  253.      *                                      and does not implement {@link \Traversable}
  254.      *                                      or if traversal is disabled via the
  255.      *                                      $traversalStrategy argument
  256.      * @throws UnsupportedMetadataException If the metadata returned by the
  257.      *                                      metadata factory does not implement
  258.      *                                      {@link ClassMetadataInterface}
  259.      */
  260.     private function validateObject(object $objectstring $propertyPath, array $groupsint $traversalStrategyExecutionContextInterface $context)
  261.     {
  262.         try {
  263.             $classMetadata $this->metadataFactory->getMetadataFor($object);
  264.             if (!$classMetadata instanceof ClassMetadataInterface) {
  265.                 throw new UnsupportedMetadataException(sprintf('The metadata factory should return instances of "Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".'get_debug_type($classMetadata)));
  266.             }
  267.             $this->validateClassNode(
  268.                 $object,
  269.                 $this->generateCacheKey($object),
  270.                 $classMetadata,
  271.                 $propertyPath,
  272.                 $groups,
  273.                 null,
  274.                 $traversalStrategy,
  275.                 $context
  276.             );
  277.         } catch (NoSuchMetadataException $e) {
  278.             // Rethrow if not Traversable
  279.             if (!$object instanceof \Traversable) {
  280.                 throw $e;
  281.             }
  282.             // Rethrow unless IMPLICIT or TRAVERSE
  283.             if (!($traversalStrategy & (TraversalStrategy::IMPLICIT TraversalStrategy::TRAVERSE))) {
  284.                 throw $e;
  285.             }
  286.             $this->validateEachObjectIn(
  287.                 $object,
  288.                 $propertyPath,
  289.                 $groups,
  290.                 $context
  291.             );
  292.         }
  293.     }
  294.     /**
  295.      * Validates each object in a collection against the constraints defined
  296.      * for their classes.
  297.      *
  298.      * Nested arrays are also iterated.
  299.      */
  300.     private function validateEachObjectIn(iterable $collectionstring $propertyPath, array $groupsExecutionContextInterface $context)
  301.     {
  302.         foreach ($collection as $key => $value) {
  303.             if (\is_array($value)) {
  304.                 // Also traverse nested arrays
  305.                 $this->validateEachObjectIn(
  306.                     $value,
  307.                     $propertyPath.'['.$key.']',
  308.                     $groups,
  309.                     $context
  310.                 );
  311.                 continue;
  312.             }
  313.             // Scalar and null values in the collection are ignored
  314.             if (\is_object($value)) {
  315.                 $this->validateObject(
  316.                     $value,
  317.                     $propertyPath.'['.$key.']',
  318.                     $groups,
  319.                     TraversalStrategy::IMPLICIT,
  320.                     $context
  321.                 );
  322.             }
  323.         }
  324.     }
  325.     /**
  326.      * Validates a class node.
  327.      *
  328.      * A class node is a combination of an object with a {@link ClassMetadataInterface}
  329.      * instance. Each class node (conceptually) has zero or more succeeding
  330.      * property nodes:
  331.      *
  332.      *     (Article:class node)
  333.      *                \
  334.      *        ($title:property node)
  335.      *
  336.      * This method validates the passed objects against all constraints defined
  337.      * at class level. It furthermore triggers the validation of each of the
  338.      * class' properties against the constraints for that property.
  339.      *
  340.      * If the selected traversal strategy allows traversal, the object is
  341.      * iterated and each nested object is validated against its own constraints.
  342.      * The object is not traversed if traversal is disabled in the class
  343.      * metadata.
  344.      *
  345.      * If the passed groups contain the group "Default", the validator will
  346.      * check whether the "Default" group has been replaced by a group sequence
  347.      * in the class metadata. If this is the case, the group sequence is
  348.      * validated instead.
  349.      *
  350.      * @throws UnsupportedMetadataException  If a property metadata does not
  351.      *                                       implement {@link PropertyMetadataInterface}
  352.      * @throws ConstraintDefinitionException If traversal was enabled but the
  353.      *                                       object does not implement
  354.      *                                       {@link \Traversable}
  355.      *
  356.      * @see TraversalStrategy
  357.      */
  358.     private function validateClassNode(object $object, ?string $cacheKeyClassMetadataInterface $metadatastring $propertyPath, array $groups, ?array $cascadedGroupsint $traversalStrategyExecutionContextInterface $context)
  359.     {
  360.         $context->setNode($object$object$metadata$propertyPath);
  361.         if (!$context->isObjectInitialized($cacheKey)) {
  362.             foreach ($this->objectInitializers as $initializer) {
  363.                 $initializer->initialize($object);
  364.             }
  365.             $context->markObjectAsInitialized($cacheKey);
  366.         }
  367.         foreach ($groups as $key => $group) {
  368.             // If the "Default" group is replaced by a group sequence, remember
  369.             // to cascade the "Default" group when traversing the group
  370.             // sequence
  371.             $defaultOverridden false;
  372.             // Use the object hash for group sequences
  373.             $groupHash \is_object($group) ? $this->generateCacheKey($grouptrue) : $group;
  374.             if ($context->isGroupValidated($cacheKey$groupHash)) {
  375.                 // Skip this group when validating the properties and when
  376.                 // traversing the object
  377.                 unset($groups[$key]);
  378.                 continue;
  379.             }
  380.             $context->markGroupAsValidated($cacheKey$groupHash);
  381.             // Replace the "Default" group by the group sequence defined
  382.             // for the class, if applicable.
  383.             // This is done after checking the cache, so that
  384.             // spl_object_hash() isn't called for this sequence and
  385.             // "Default" is used instead in the cache. This is useful
  386.             // if the getters below return different group sequences in
  387.             // every call.
  388.             if (Constraint::DEFAULT_GROUP === $group) {
  389.                 if ($metadata->hasGroupSequence()) {
  390.                     // The group sequence is statically defined for the class
  391.                     $group $metadata->getGroupSequence();
  392.                     $defaultOverridden true;
  393.                 } elseif ($metadata->isGroupSequenceProvider()) {
  394.                     // The group sequence is dynamically obtained from the validated
  395.                     // object
  396.                     /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
  397.                     $group $object->getGroupSequence();
  398.                     $defaultOverridden true;
  399.                     if (!$group instanceof GroupSequence) {
  400.                         $group = new GroupSequence($group);
  401.                     }
  402.                 }
  403.             }
  404.             // If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
  405.             // (=<G1,G2>), then call validateClassNode() with each entry of the
  406.             // group sequence and abort if necessary (G1, G2)
  407.             if ($group instanceof GroupSequence) {
  408.                 $this->stepThroughGroupSequence(
  409.                     $object,
  410.                     $object,
  411.                     $cacheKey,
  412.                     $metadata,
  413.                     $propertyPath,
  414.                     $traversalStrategy,
  415.                     $group,
  416.                     $defaultOverridden Constraint::DEFAULT_GROUP null,
  417.                     $context
  418.                 );
  419.                 // Skip the group sequence when validating properties, because
  420.                 // stepThroughGroupSequence() already validates the properties
  421.                 unset($groups[$key]);
  422.                 continue;
  423.             }
  424.             $this->validateInGroup($object$cacheKey$metadata$group$context);
  425.         }
  426.         // If no more groups should be validated for the property nodes,
  427.         // we can safely quit
  428.         if (=== \count($groups)) {
  429.             return;
  430.         }
  431.         // Validate all properties against their constraints
  432.         foreach ($metadata->getConstrainedProperties() as $propertyName) {
  433.             // If constraints are defined both on the getter of a property as
  434.             // well as on the property itself, then getPropertyMetadata()
  435.             // returns two metadata objects, not just one
  436.             foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
  437.                 if (!$propertyMetadata instanceof PropertyMetadataInterface) {
  438.                     throw new UnsupportedMetadataException(sprintf('The property metadata instances should implement "Symfony\Component\Validator\Mapping\PropertyMetadataInterface", got: "%s".'get_debug_type($propertyMetadata)));
  439.                 }
  440.                 if ($propertyMetadata instanceof GetterMetadata) {
  441.                     $propertyValue = new LazyProperty(static function () use ($propertyMetadata$object) {
  442.                         return $propertyMetadata->getPropertyValue($object);
  443.                     });
  444.                 } else {
  445.                     $propertyValue $propertyMetadata->getPropertyValue($object);
  446.                 }
  447.                 $this->validateGenericNode(
  448.                     $propertyValue,
  449.                     $object,
  450.                     $cacheKey.':'.\get_class($object).':'.$propertyName,
  451.                     $propertyMetadata,
  452.                     PropertyPath::append($propertyPath$propertyName),
  453.                     $groups,
  454.                     $cascadedGroups,
  455.                     TraversalStrategy::IMPLICIT,
  456.                     $context
  457.                 );
  458.             }
  459.         }
  460.         // If no specific traversal strategy was requested when this method
  461.         // was called, use the traversal strategy of the class' metadata
  462.         if ($traversalStrategy TraversalStrategy::IMPLICIT) {
  463.             $traversalStrategy $metadata->getTraversalStrategy();
  464.         }
  465.         // Traverse only if IMPLICIT or TRAVERSE
  466.         if (!($traversalStrategy & (TraversalStrategy::IMPLICIT TraversalStrategy::TRAVERSE))) {
  467.             return;
  468.         }
  469.         // If IMPLICIT, stop unless we deal with a Traversable
  470.         if ($traversalStrategy TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
  471.             return;
  472.         }
  473.         // If TRAVERSE, fail if we have no Traversable
  474.         if (!$object instanceof \Traversable) {
  475.             throw new ConstraintDefinitionException(sprintf('Traversal was enabled for "%s", but this class does not implement "\Traversable".'get_debug_type($object)));
  476.         }
  477.         $this->validateEachObjectIn(
  478.             $object,
  479.             $propertyPath,
  480.             $groups,
  481.             $context
  482.         );
  483.     }
  484.     /**
  485.      * Validates a node that is not a class node.
  486.      *
  487.      * Currently, two such node types exist:
  488.      *
  489.      *  - property nodes, which consist of the value of an object's
  490.      *    property together with a {@link PropertyMetadataInterface} instance
  491.      *  - generic nodes, which consist of a value and some arbitrary
  492.      *    constraints defined in a {@link MetadataInterface} container
  493.      *
  494.      * In both cases, the value is validated against all constraints defined
  495.      * in the passed metadata object. Then, if the value is an instance of
  496.      * {@link \Traversable} and the selected traversal strategy permits it,
  497.      * the value is traversed and each nested object validated against its own
  498.      * constraints. If the value is an array, it is traversed regardless of
  499.      * the given strategy.
  500.      *
  501.      * @see TraversalStrategy
  502.      */
  503.     private function validateGenericNode($value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadatastring $propertyPath, array $groups, ?array $cascadedGroupsint $traversalStrategyExecutionContextInterface $context)
  504.     {
  505.         $context->setNode($value$object$metadata$propertyPath);
  506.         foreach ($groups as $key => $group) {
  507.             if ($group instanceof GroupSequence) {
  508.                 $this->stepThroughGroupSequence(
  509.                     $value,
  510.                     $object,
  511.                     $cacheKey,
  512.                     $metadata,
  513.                     $propertyPath,
  514.                     $traversalStrategy,
  515.                     $group,
  516.                     null,
  517.                     $context
  518.                 );
  519.                 // Skip the group sequence when cascading, as the cascading
  520.                 // logic is already done in stepThroughGroupSequence()
  521.                 unset($groups[$key]);
  522.                 continue;
  523.             }
  524.             $this->validateInGroup($value$cacheKey$metadata$group$context);
  525.         }
  526.         if (=== \count($groups)) {
  527.             return;
  528.         }
  529.         if (null === $value) {
  530.             return;
  531.         }
  532.         $cascadingStrategy $metadata->getCascadingStrategy();
  533.         // Quit unless we cascade
  534.         if (!($cascadingStrategy CascadingStrategy::CASCADE)) {
  535.             return;
  536.         }
  537.         // If no specific traversal strategy was requested when this method
  538.         // was called, use the traversal strategy of the node's metadata
  539.         if ($traversalStrategy TraversalStrategy::IMPLICIT) {
  540.             $traversalStrategy $metadata->getTraversalStrategy();
  541.         }
  542.         // The $cascadedGroups property is set, if the "Default" group is
  543.         // overridden by a group sequence
  544.         // See validateClassNode()
  545.         $cascadedGroups null !== $cascadedGroups && \count($cascadedGroups) > $cascadedGroups $groups;
  546.         if ($value instanceof LazyProperty) {
  547.             $value $value->getPropertyValue();
  548.             if (null === $value) {
  549.                 return;
  550.             }
  551.         }
  552.         if (\is_array($value)) {
  553.             // Arrays are always traversed, independent of the specified
  554.             // traversal strategy
  555.             $this->validateEachObjectIn(
  556.                 $value,
  557.                 $propertyPath,
  558.                 $cascadedGroups,
  559.                 $context
  560.             );
  561.             return;
  562.         }
  563.         if (!\is_object($value)) {
  564.             throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: "%s".'\gettype($value)));
  565.         }
  566.         $this->validateObject(
  567.             $value,
  568.             $propertyPath,
  569.             $cascadedGroups,
  570.             $traversalStrategy,
  571.             $context
  572.         );
  573.         // Currently, the traversal strategy can only be TRAVERSE for a
  574.         // generic node if the cascading strategy is CASCADE. Thus, traversable
  575.         // objects will always be handled within validateObject() and there's
  576.         // nothing more to do here.
  577.         // see GenericMetadata::addConstraint()
  578.     }
  579.     /**
  580.      * Sequentially validates a node's value in each group of a group sequence.
  581.      *
  582.      * If any of the constraints generates a violation, subsequent groups in the
  583.      * group sequence are skipped.
  584.      */
  585.     private function stepThroughGroupSequence($value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadatastring $propertyPathint $traversalStrategyGroupSequence $groupSequence, ?string $cascadedGroupExecutionContextInterface $context)
  586.     {
  587.         $violationCount \count($context->getViolations());
  588.         $cascadedGroups $cascadedGroup ? [$cascadedGroup] : null;
  589.         foreach ($groupSequence->groups as $groupInSequence) {
  590.             $groups = (array) $groupInSequence;
  591.             if ($metadata instanceof ClassMetadataInterface) {
  592.                 $this->validateClassNode(
  593.                     $value,
  594.                     $cacheKey,
  595.                     $metadata,
  596.                     $propertyPath,
  597.                     $groups,
  598.                     $cascadedGroups,
  599.                     $traversalStrategy,
  600.                     $context
  601.                 );
  602.             } else {
  603.                 $this->validateGenericNode(
  604.                     $value,
  605.                     $object,
  606.                     $cacheKey,
  607.                     $metadata,
  608.                     $propertyPath,
  609.                     $groups,
  610.                     $cascadedGroups,
  611.                     $traversalStrategy,
  612.                     $context
  613.                 );
  614.             }
  615.             // Abort sequence validation if a violation was generated
  616.             if (\count($context->getViolations()) > $violationCount) {
  617.                 break;
  618.             }
  619.         }
  620.     }
  621.     /**
  622.      * Validates a node's value against all constraints in the given group.
  623.      *
  624.      * @param mixed $value The validated value
  625.      */
  626.     private function validateInGroup($value, ?string $cacheKeyMetadataInterface $metadatastring $groupExecutionContextInterface $context)
  627.     {
  628.         $context->setGroup($group);
  629.         foreach ($metadata->findConstraints($group) as $constraint) {
  630.             if ($constraint instanceof Existence) {
  631.                 continue;
  632.             }
  633.             // Prevent duplicate validation of constraints, in the case
  634.             // that constraints belong to multiple validated groups
  635.             if (null !== $cacheKey) {
  636.                 $constraintHash $this->generateCacheKey($constrainttrue);
  637.                 // instanceof Valid: In case of using a Valid constraint with many groups
  638.                 // it makes a reference object get validated by each group
  639.                 if ($constraint instanceof Composite || $constraint instanceof Valid) {
  640.                     $constraintHash .= $group;
  641.                 }
  642.                 if ($context->isConstraintValidated($cacheKey$constraintHash)) {
  643.                     continue;
  644.                 }
  645.                 $context->markConstraintAsValidated($cacheKey$constraintHash);
  646.             }
  647.             $context->setConstraint($constraint);
  648.             $validator $this->validatorFactory->getInstance($constraint);
  649.             $validator->initialize($context);
  650.             if ($value instanceof LazyProperty) {
  651.                 $value $value->getPropertyValue();
  652.             }
  653.             try {
  654.                 $validator->validate($value$constraint);
  655.             } catch (UnexpectedValueException $e) {
  656.                 $context->buildViolation('This value should be of type {{ type }}.')
  657.                     ->setParameter('{{ type }}'$e->getExpectedType())
  658.                     ->addViolation();
  659.             }
  660.         }
  661.     }
  662.     private function generateCacheKey(object $objectbool $dependsOnPropertyPath false): string
  663.     {
  664.         if ($this->context instanceof ExecutionContext) {
  665.             $cacheKey $this->context->generateCacheKey($object);
  666.         } else {
  667.             $cacheKey spl_object_hash($object);
  668.         }
  669.         if ($dependsOnPropertyPath) {
  670.             $cacheKey .= $this->context->getPropertyPath();
  671.         }
  672.         return $cacheKey;
  673.     }
  674. }