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

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