vendor/symfony/validator/Validator/RecursiveContextualValidator.php line 298
<?php/** This file is part of the Symfony package.** (c) Fabien Potencier <fabien@symfony.com>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace Symfony\Component\Validator\Validator;use Symfony\Component\Validator\Constraint;use Symfony\Component\Validator\Constraints\Composite;use Symfony\Component\Validator\Constraints\Existence;use Symfony\Component\Validator\Constraints\GroupSequence;use Symfony\Component\Validator\Constraints\Valid;use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;use Symfony\Component\Validator\ConstraintViolationListInterface;use Symfony\Component\Validator\Context\ExecutionContext;use Symfony\Component\Validator\Context\ExecutionContextInterface;use Symfony\Component\Validator\Exception\ConstraintDefinitionException;use Symfony\Component\Validator\Exception\NoSuchMetadataException;use Symfony\Component\Validator\Exception\RuntimeException;use Symfony\Component\Validator\Exception\UnexpectedValueException;use Symfony\Component\Validator\Exception\UnsupportedMetadataException;use Symfony\Component\Validator\Exception\ValidatorException;use Symfony\Component\Validator\Mapping\CascadingStrategy;use Symfony\Component\Validator\Mapping\ClassMetadataInterface;use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;use Symfony\Component\Validator\Mapping\GenericMetadata;use Symfony\Component\Validator\Mapping\GetterMetadata;use Symfony\Component\Validator\Mapping\MetadataInterface;use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;use Symfony\Component\Validator\Mapping\TraversalStrategy;use Symfony\Component\Validator\ObjectInitializerInterface;use Symfony\Component\Validator\Util\PropertyPath;/*** Recursive implementation of {@link ContextualValidatorInterface}.** @author Bernhard Schussek <bschussek@gmail.com>*/class RecursiveContextualValidator implements ContextualValidatorInterface{private ExecutionContextInterface $context;private string $defaultPropertyPath;private array $defaultGroups;private MetadataFactoryInterface $metadataFactory;private ConstraintValidatorFactoryInterface $validatorFactory;private array $objectInitializers;/*** Creates a validator for the given context.** @param ObjectInitializerInterface[] $objectInitializers The object initializers*/public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = []){$this->context = $context;$this->defaultPropertyPath = $context->getPropertyPath();$this->defaultGroups = [$context->getGroup() ?: Constraint::DEFAULT_GROUP];$this->metadataFactory = $metadataFactory;$this->validatorFactory = $validatorFactory;$this->objectInitializers = $objectInitializers;}public function atPath(string $path): static{$this->defaultPropertyPath = $this->context->getPropertyPath($path);return $this;}public function validate(mixed $value, Constraint|array $constraints = null, string|GroupSequence|array $groups = null): static{$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;$previousValue = $this->context->getValue();$previousObject = $this->context->getObject();$previousMetadata = $this->context->getMetadata();$previousPath = $this->context->getPropertyPath();$previousGroup = $this->context->getGroup();$previousConstraint = null;if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) {$previousConstraint = $this->context->getConstraint();}// If explicit constraints are passed, validate the value against// those constraintsif (null !== $constraints) {// You can pass a single constraint or an array of constraints// Make sure to deal with an array in the rest of the codeif (!\is_array($constraints)) {$constraints = [$constraints];}$metadata = new GenericMetadata();$metadata->addConstraints($constraints);$this->validateGenericNode($value,$previousObject,\is_object($value) ? $this->generateCacheKey($value) : null,$metadata,$this->defaultPropertyPath,$groups,null,TraversalStrategy::IMPLICIT,$this->context);$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);$this->context->setGroup($previousGroup);if (null !== $previousConstraint) {$this->context->setConstraint($previousConstraint);}return $this;}// If an object is passed without explicit constraints, validate that// object against the constraints defined for the object's classif (\is_object($value)) {$this->validateObject($value,$this->defaultPropertyPath,$groups,TraversalStrategy::IMPLICIT,$this->context);$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);$this->context->setGroup($previousGroup);return $this;}// If an array is passed without explicit constraints, validate each// object in the arrayif (\is_array($value)) {$this->validateEachObjectIn($value,$this->defaultPropertyPath,$groups,$this->context);$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);$this->context->setGroup($previousGroup);return $this;}throw new RuntimeException(sprintf('Cannot validate values of type "%s" automatically. Please provide a constraint.', get_debug_type($value)));}public function validateProperty(object $object, string $propertyName, string|GroupSequence|array $groups = null): static{$classMetadata = $this->metadataFactory->getMetadataFor($object);if (!$classMetadata instanceof ClassMetadataInterface) {throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata)));}$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;$cacheKey = $this->generateCacheKey($object);$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);$previousValue = $this->context->getValue();$previousObject = $this->context->getObject();$previousMetadata = $this->context->getMetadata();$previousPath = $this->context->getPropertyPath();$previousGroup = $this->context->getGroup();foreach ($propertyMetadatas as $propertyMetadata) {$propertyValue = $propertyMetadata->getPropertyValue($object);$this->validateGenericNode($propertyValue,$object,$cacheKey.':'.$object::class.':'.$propertyName,$propertyMetadata,$propertyPath,$groups,null,TraversalStrategy::IMPLICIT,$this->context);}$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);$this->context->setGroup($previousGroup);return $this;}public function validatePropertyValue(object|string $objectOrClass, string $propertyName, mixed $value, string|GroupSequence|array $groups = null): static{$classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);if (!$classMetadata instanceof ClassMetadataInterface) {throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata)));}$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;if (\is_object($objectOrClass)) {$object = $objectOrClass;$class = $object::class;$cacheKey = $this->generateCacheKey($objectOrClass);$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);} else {// $objectOrClass contains a class name$object = null;$class = $objectOrClass;$cacheKey = null;$propertyPath = $this->defaultPropertyPath;}$previousValue = $this->context->getValue();$previousObject = $this->context->getObject();$previousMetadata = $this->context->getMetadata();$previousPath = $this->context->getPropertyPath();$previousGroup = $this->context->getGroup();foreach ($propertyMetadatas as $propertyMetadata) {$this->validateGenericNode($value,$object,$cacheKey.':'.$class.':'.$propertyName,$propertyMetadata,$propertyPath,$groups,null,TraversalStrategy::IMPLICIT,$this->context);}$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);$this->context->setGroup($previousGroup);return $this;}public function getViolations(): ConstraintViolationListInterface{return $this->context->getViolations();}/*** Normalizes the given group or list of groups to an array.** @param string|GroupSequence|array<string|GroupSequence> $groups The groups to normalize** @return array<string|GroupSequence>*/protected function normalizeGroups(string|GroupSequence|array $groups): array{if (\is_array($groups)) {return $groups;}return [$groups];}/*** Validates an object against the constraints defined for its class.** If no metadata is available for the class, but the class is an instance* of {@link \Traversable} and the selected traversal strategy allows* traversal, the object will be iterated and each nested object will be* validated instead.** @throws NoSuchMetadataException If the object has no associated metadata* and does not implement {@link \Traversable}* or if traversal is disabled via the* $traversalStrategy argument* @throws UnsupportedMetadataException If the metadata returned by the* metadata factory does not implement* {@link ClassMetadataInterface}*/private function validateObject(object $object, string $propertyPath, array $groups, int $traversalStrategy, ExecutionContextInterface $context){try {$classMetadata = $this->metadataFactory->getMetadataFor($object);if (!$classMetadata instanceof ClassMetadataInterface) {throw new UnsupportedMetadataException(sprintf('The metadata factory should return instances of "Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata)));}$this->validateClassNode($object,$this->generateCacheKey($object),$classMetadata,$propertyPath,$groups,null,$traversalStrategy,$context);} catch (NoSuchMetadataException $e) {// Rethrow if not Traversableif (!$object instanceof \Traversable) {throw $e;}// Rethrow unless IMPLICIT or TRAVERSEif (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {throw $e;}$this->validateEachObjectIn($object,$propertyPath,$groups,$context);}}/*** Validates each object in a collection against the constraints defined* for their classes.** Nested arrays are also iterated.*/private function validateEachObjectIn(iterable $collection, string $propertyPath, array $groups, ExecutionContextInterface $context){foreach ($collection as $key => $value) {if (\is_array($value)) {// Also traverse nested arrays$this->validateEachObjectIn($value,$propertyPath.'['.$key.']',$groups,$context);continue;}// Scalar and null values in the collection are ignoredif (\is_object($value)) {$this->validateObject($value,$propertyPath.'['.$key.']',$groups,TraversalStrategy::IMPLICIT,$context);}}}/*** Validates a class node.** A class node is a combination of an object with a {@link ClassMetadataInterface}* instance. Each class node (conceptually) has zero or more succeeding* property nodes:** (Article:class node)* \* ($title:property node)** This method validates the passed objects against all constraints defined* at class level. It furthermore triggers the validation of each of the* class' properties against the constraints for that property.** If the selected traversal strategy allows traversal, the object is* iterated and each nested object is validated against its own constraints.* The object is not traversed if traversal is disabled in the class* metadata.** If the passed groups contain the group "Default", the validator will* check whether the "Default" group has been replaced by a group sequence* in the class metadata. If this is the case, the group sequence is* validated instead.** @throws UnsupportedMetadataException If a property metadata does not* implement {@link PropertyMetadataInterface}* @throws ConstraintDefinitionException If traversal was enabled but the* object does not implement* {@link \Traversable}** @see TraversalStrategy*/private function validateClassNode(object $object, ?string $cacheKey, ClassMetadataInterface $metadata, string $propertyPath, array $groups, ?array $cascadedGroups, int $traversalStrategy, ExecutionContextInterface $context){$context->setNode($object, $object, $metadata, $propertyPath);if (!$context->isObjectInitialized($cacheKey)) {foreach ($this->objectInitializers as $initializer) {$initializer->initialize($object);}$context->markObjectAsInitialized($cacheKey);}foreach ($groups as $key => $group) {// If the "Default" group is replaced by a group sequence, remember// to cascade the "Default" group when traversing the group// sequence$defaultOverridden = false;// Use the object hash for group sequences$groupHash = \is_object($group) ? $this->generateCacheKey($group, true) : $group;if ($context->isGroupValidated($cacheKey, $groupHash)) {// Skip this group when validating the properties and when// traversing the objectunset($groups[$key]);continue;}$context->markGroupAsValidated($cacheKey, $groupHash);// Replace the "Default" group by the group sequence defined// for the class, if applicable.// This is done after checking the cache, so that// spl_object_hash() isn't called for this sequence and// "Default" is used instead in the cache. This is useful// if the getters below return different group sequences in// every call.if (Constraint::DEFAULT_GROUP === $group) {if ($metadata->hasGroupSequence()) {// The group sequence is statically defined for the class$group = $metadata->getGroupSequence();$defaultOverridden = true;} elseif ($metadata->isGroupSequenceProvider()) {// The group sequence is dynamically obtained from the validated// object/* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */$group = $object->getGroupSequence();$defaultOverridden = true;if (!$group instanceof GroupSequence) {$group = new GroupSequence($group);}}}// If the groups (=[<G1,G2>,G3,G4]) contain a group sequence// (=<G1,G2>), then call validateClassNode() with each entry of the// group sequence and abort if necessary (G1, G2)if ($group instanceof GroupSequence) {$this->stepThroughGroupSequence($object,$object,$cacheKey,$metadata,$propertyPath,$traversalStrategy,$group,$defaultOverridden ? Constraint::DEFAULT_GROUP : null,$context);// Skip the group sequence when validating properties, because// stepThroughGroupSequence() already validates the propertiesunset($groups[$key]);continue;}$this->validateInGroup($object, $cacheKey, $metadata, $group, $context);}// If no more groups should be validated for the property nodes,// we can safely quitif (0 === \count($groups)) {return;}// Validate all properties against their constraintsforeach ($metadata->getConstrainedProperties() as $propertyName) {// If constraints are defined both on the getter of a property as// well as on the property itself, then getPropertyMetadata()// returns two metadata objects, not just oneforeach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {if (!$propertyMetadata instanceof PropertyMetadataInterface) {throw new UnsupportedMetadataException(sprintf('The property metadata instances should implement "Symfony\Component\Validator\Mapping\PropertyMetadataInterface", got: "%s".', get_debug_type($propertyMetadata)));}if ($propertyMetadata instanceof GetterMetadata) {$propertyValue = new LazyProperty(static function () use ($propertyMetadata, $object) {return $propertyMetadata->getPropertyValue($object);});} else {$propertyValue = $propertyMetadata->getPropertyValue($object);}$this->validateGenericNode($propertyValue,$object,$cacheKey.':'.$object::class.':'.$propertyName,$propertyMetadata,PropertyPath::append($propertyPath, $propertyName),$groups,$cascadedGroups,TraversalStrategy::IMPLICIT,$context);}}// If no specific traversal strategy was requested when this method// was called, use the traversal strategy of the class' metadataif ($traversalStrategy & TraversalStrategy::IMPLICIT) {$traversalStrategy = $metadata->getTraversalStrategy();}// Traverse only if IMPLICIT or TRAVERSEif (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {return;}// If IMPLICIT, stop unless we deal with a Traversableif ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {return;}// If TRAVERSE, fail if we have no Traversableif (!$object instanceof \Traversable) {throw new ConstraintDefinitionException(sprintf('Traversal was enabled for "%s", but this class does not implement "\Traversable".', get_debug_type($object)));}$this->validateEachObjectIn($object,$propertyPath,$groups,$context);}/*** Validates a node that is not a class node.** Currently, two such node types exist:** - property nodes, which consist of the value of an object's* property together with a {@link PropertyMetadataInterface} instance* - generic nodes, which consist of a value and some arbitrary* constraints defined in a {@link MetadataInterface} container** In both cases, the value is validated against all constraints defined* in the passed metadata object. Then, if the value is an instance of* {@link \Traversable} and the selected traversal strategy permits it,* the value is traversed and each nested object validated against its own* constraints. If the value is an array, it is traversed regardless of* the given strategy.** @see TraversalStrategy*/private function validateGenericNode(mixed $value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadata, string $propertyPath, array $groups, ?array $cascadedGroups, int $traversalStrategy, ExecutionContextInterface $context){$context->setNode($value, $object, $metadata, $propertyPath);foreach ($groups as $key => $group) {if ($group instanceof GroupSequence) {$this->stepThroughGroupSequence($value,$object,$cacheKey,$metadata,$propertyPath,$traversalStrategy,$group,null,$context);// Skip the group sequence when cascading, as the cascading// logic is already done in stepThroughGroupSequence()unset($groups[$key]);continue;}$this->validateInGroup($value, $cacheKey, $metadata, $group, $context);}if (0 === \count($groups)) {return;}if (null === $value) {return;}$cascadingStrategy = $metadata->getCascadingStrategy();// Quit unless we cascadeif (!($cascadingStrategy & CascadingStrategy::CASCADE)) {return;}// If no specific traversal strategy was requested when this method// was called, use the traversal strategy of the node's metadataif ($traversalStrategy & TraversalStrategy::IMPLICIT) {$traversalStrategy = $metadata->getTraversalStrategy();}// The $cascadedGroups property is set, if the "Default" group is// overridden by a group sequence// See validateClassNode()$cascadedGroups = null !== $cascadedGroups && \count($cascadedGroups) > 0 ? $cascadedGroups : $groups;if ($value instanceof LazyProperty) {$value = $value->getPropertyValue();if (null === $value) {return;}}if (\is_array($value)) {// Arrays are always traversed, independent of the specified// traversal strategy$this->validateEachObjectIn($value,$propertyPath,$cascadedGroups,$context);return;}if (!\is_object($value)) {throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: "%s".', \gettype($value)));}$this->validateObject($value,$propertyPath,$cascadedGroups,$traversalStrategy,$context);// Currently, the traversal strategy can only be TRAVERSE for a// generic node if the cascading strategy is CASCADE. Thus, traversable// objects will always be handled within validateObject() and there's// nothing more to do here.// see GenericMetadata::addConstraint()}/*** Sequentially validates a node's value in each group of a group sequence.** If any of the constraints generates a violation, subsequent groups in the* group sequence are skipped.*/private function stepThroughGroupSequence(mixed $value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadata, string $propertyPath, int $traversalStrategy, GroupSequence $groupSequence, ?string $cascadedGroup, ExecutionContextInterface $context){$violationCount = \count($context->getViolations());$cascadedGroups = $cascadedGroup ? [$cascadedGroup] : null;foreach ($groupSequence->groups as $groupInSequence) {$groups = (array) $groupInSequence;if ($metadata instanceof ClassMetadataInterface) {$this->validateClassNode($value,$cacheKey,$metadata,$propertyPath,$groups,$cascadedGroups,$traversalStrategy,$context);} else {$this->validateGenericNode($value,$object,$cacheKey,$metadata,$propertyPath,$groups,$cascadedGroups,$traversalStrategy,$context);}// Abort sequence validation if a violation was generatedif (\count($context->getViolations()) > $violationCount) {break;}}}/*** Validates a node's value against all constraints in the given group.*/private function validateInGroup(mixed $value, ?string $cacheKey, MetadataInterface $metadata, string $group, ExecutionContextInterface $context){$context->setGroup($group);foreach ($metadata->findConstraints($group) as $constraint) {if ($constraint instanceof Existence) {continue;}// Prevent duplicate validation of constraints, in the case// that constraints belong to multiple validated groupsif (null !== $cacheKey) {$constraintHash = $this->generateCacheKey($constraint, true);// instanceof Valid: In case of using a Valid constraint with many groups// it makes a reference object get validated by each groupif ($constraint instanceof Composite || $constraint instanceof Valid) {$constraintHash .= $group;}if ($context->isConstraintValidated($cacheKey, $constraintHash)) {continue;}$context->markConstraintAsValidated($cacheKey, $constraintHash);}$context->setConstraint($constraint);$validator = $this->validatorFactory->getInstance($constraint);$validator->initialize($context);if ($value instanceof LazyProperty) {$value = $value->getPropertyValue();}try {$validator->validate($value, $constraint);} catch (UnexpectedValueException $e) {$context->buildViolation('This value should be of type {{ type }}.')->setParameter('{{ type }}', $e->getExpectedType())->addViolation();}}}private function generateCacheKey(object $object, bool $dependsOnPropertyPath = false): string{if ($this->context instanceof ExecutionContext) {$cacheKey = $this->context->generateCacheKey($object);} else {$cacheKey = spl_object_hash($object);}if ($dependsOnPropertyPath) {$cacheKey .= $this->context->getPropertyPath();}return $cacheKey;}}