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