--- /dev/null
+<?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\GroupSequence;
+use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
+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\UnsupportedMetadataException;
+use Symfony\Component\Validator\Exception\ValidatorException;
+use Symfony\Component\Validator\Mapping\CascadingStrategy;
+use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
+use Symfony\Component\Validator\Mapping\GenericMetadata;
+use Symfony\Component\Validator\Mapping\MetadataInterface;
+use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
+use Symfony\Component\Validator\Mapping\TraversalStrategy;
+use Symfony\Component\Validator\MetadataFactoryInterface;
+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
+{
+ /**
+ * @var ExecutionContextInterface
+ */
+ private $context;
+
+ /**
+ * @var string
+ */
+ private $defaultPropertyPath;
+
+ /**
+ * @var array
+ */
+ private $defaultGroups;
+
+ /**
+ * @var MetadataFactoryInterface
+ */
+ private $metadataFactory;
+
+ /**
+ * @var ConstraintValidatorFactoryInterface
+ */
+ private $validatorFactory;
+
+ /**
+ * @var ObjectInitializerInterface[]
+ */
+ private $objectInitializers;
+
+ /**
+ * Creates a validator for the given context.
+ *
+ * @param ExecutionContextInterface $context The execution context
+ * @param MetadataFactoryInterface $metadataFactory The factory for
+ * fetching the metadata
+ * of validated objects
+ * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating
+ * constraint validators
+ * @param ObjectInitializerInterface[] $objectInitializers The object initializers
+ */
+ public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array())
+ {
+ $this->context = $context;
+ $this->defaultPropertyPath = $context->getPropertyPath();
+ $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
+ $this->metadataFactory = $metadataFactory;
+ $this->validatorFactory = $validatorFactory;
+ $this->objectInitializers = $objectInitializers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function atPath($path)
+ {
+ $this->defaultPropertyPath = $this->context->getPropertyPath($path);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($value, $constraints = null, $groups = null)
+ {
+ $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 constraints
+ if (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 code
+ if (!is_array($constraints)) {
+ $constraints = array($constraints);
+ }
+
+ $metadata = new GenericMetadata();
+ $metadata->addConstraints($constraints);
+
+ $this->validateGenericNode(
+ $value,
+ null,
+ is_object($value) ? spl_object_hash($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 class
+ if (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 array
+ if (is_array($value)) {
+ $this->validateEachObjectIn(
+ $value,
+ $this->defaultPropertyPath,
+ $groups,
+ true,
+ $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.',
+ gettype($value)
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateProperty($object, $propertyName, $groups = null)
+ {
+ $classMetadata = $this->metadataFactory->getMetadataFor($object);
+
+ if (!$classMetadata instanceof ClassMetadataInterface) {
+ // Cannot be UnsupportedMetadataException because of BC with
+ // Symfony < 2.5
+ 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)
+ ));
+ }
+
+ $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
+ $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
+ $cacheKey = spl_object_hash($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.':'.$propertyName,
+ $propertyMetadata,
+ $propertyPath,
+ $groups,
+ null,
+ TraversalStrategy::IMPLICIT,
+ $this->context
+ );
+ }
+
+ $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
+ $this->context->setGroup($previousGroup);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
+ {
+ $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
+
+ if (!$classMetadata instanceof ClassMetadataInterface) {
+ // Cannot be UnsupportedMetadataException because of BC with
+ // Symfony < 2.5
+ 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)
+ ));
+ }
+
+ $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
+ $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
+
+ if (is_object($objectOrClass)) {
+ $object = $objectOrClass;
+ $cacheKey = spl_object_hash($objectOrClass);
+ $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
+ } else {
+ // $objectOrClass contains a class name
+ $object = null;
+ $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.':'.$propertyName,
+ $propertyMetadata,
+ $propertyPath,
+ $groups,
+ null,
+ TraversalStrategy::IMPLICIT,
+ $this->context
+ );
+ }
+
+ $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
+ $this->context->setGroup($previousGroup);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getViolations()
+ {
+ return $this->context->getViolations();
+ }
+
+ /**
+ * Normalizes the given group or list of groups to an array.
+ *
+ * @param mixed $groups The groups to normalize
+ *
+ * @return array A group array
+ */
+ protected function normalizeGroups($groups)
+ {
+ if (is_array($groups)) {
+ return $groups;
+ }
+
+ return array($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.
+ *
+ * @param object $object The object to cascade
+ * @param string $propertyPath The current property path
+ * @param string[] $groups The validated groups
+ * @param int $traversalStrategy The strategy for traversing the
+ * cascaded object
+ * @param ExecutionContextInterface $context The current execution context
+ *
+ * @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, $propertyPath, array $groups, $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".',
+ is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
+ ));
+ }
+
+ $this->validateClassNode(
+ $object,
+ spl_object_hash($object),
+ $classMetadata,
+ $propertyPath,
+ $groups,
+ null,
+ $traversalStrategy,
+ $context
+ );
+ } catch (NoSuchMetadataException $e) {
+ // Rethrow if not Traversable
+ if (!$object instanceof \Traversable) {
+ throw $e;
+ }
+
+ // Rethrow unless IMPLICIT or TRAVERSE
+ if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
+ throw $e;
+ }
+
+ $this->validateEachObjectIn(
+ $object,
+ $propertyPath,
+ $groups,
+ $traversalStrategy & TraversalStrategy::STOP_RECURSION,
+ $context
+ );
+ }
+ }
+
+ /**
+ * Validates each object in a collection against the constraints defined
+ * for their classes.
+ *
+ * If the parameter $recursive is set to true, nested {@link \Traversable}
+ * objects are iterated as well. Nested arrays are always iterated,
+ * regardless of the value of $recursive.
+ *
+ * @param array|\Traversable $collection The collection
+ * @param string $propertyPath The current property path
+ * @param string[] $groups The validated groups
+ * @param bool $stopRecursion Whether to disable
+ * recursive iteration. For
+ * backwards compatibility
+ * with Symfony < 2.5.
+ * @param ExecutionContextInterface $context The current execution context
+ *
+ * @see ClassNode
+ * @see CollectionNode
+ */
+ private function validateEachObjectIn($collection, $propertyPath, array $groups, $stopRecursion, ExecutionContextInterface $context)
+ {
+ if ($stopRecursion) {
+ $traversalStrategy = TraversalStrategy::NONE;
+ } else {
+ $traversalStrategy = TraversalStrategy::IMPLICIT;
+ }
+
+ foreach ($collection as $key => $value) {
+ if (is_array($value)) {
+ // Arrays are always cascaded, independent of the specified
+ // traversal strategy
+ // (BC with Symfony < 2.5)
+ $this->validateEachObjectIn(
+ $value,
+ $propertyPath.'['.$key.']',
+ $groups,
+ $stopRecursion,
+ $context
+ );
+
+ continue;
+ }
+
+ // Scalar and null values in the collection are ignored
+ // (BC with Symfony < 2.5)
+ if (is_object($value)) {
+ $this->validateObject(
+ $value,
+ $propertyPath.'['.$key.']',
+ $groups,
+ $traversalStrategy,
+ $context
+ );
+ }
+ }
+ }
+
+ /**
+ * Validates a class node.
+ *
+ * A class node is a combination of an object with a {@link ClassMetadataInterface}
+ * instance. Each class node (conceptionally) 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.
+ *
+ * @param object $object The validated object
+ * @param string $cacheKey The key for caching
+ * the validated object
+ * @param ClassMetadataInterface $metadata The class metadata of
+ * the object
+ * @param string $propertyPath The property path leading
+ * to the object
+ * @param string[] $groups The groups in which the
+ * object should be validated
+ * @param string[]|null $cascadedGroups The groups in which
+ * cascaded objects should
+ * be validated
+ * @param int $traversalStrategy The strategy used for
+ * traversing the object
+ * @param ExecutionContextInterface $context The current execution context
+ *
+ * @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, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $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) ? spl_object_hash($group) : $group;
+
+ if ($context->isGroupValidated($cacheKey, $groupHash)) {
+ // Skip this group when validating the properties and when
+ // traversing the object
+ unset($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 properties
+ unset($groups[$key]);
+
+ continue;
+ }
+
+ $this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
+ }
+
+ // If no more groups should be validated for the property nodes,
+ // we can safely quit
+ if (0 === count($groups)) {
+ return;
+ }
+
+ // Validate all properties against their constraints
+ foreach ($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 one
+ foreach ($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".',
+ is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata)
+ ));
+ }
+
+ $propertyValue = $propertyMetadata->getPropertyValue($object);
+
+ $this->validateGenericNode(
+ $propertyValue,
+ $object,
+ $cacheKey.':'.$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' metadata
+ if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
+ // Keep the STOP_RECURSION flag, if it was set
+ $traversalStrategy = $metadata->getTraversalStrategy()
+ | ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
+ }
+
+ // Traverse only if IMPLICIT or TRAVERSE
+ if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
+ return;
+ }
+
+ // If IMPLICIT, stop unless we deal with a Traversable
+ if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
+ return;
+ }
+
+ // If TRAVERSE, fail if we have no Traversable
+ if (!$object instanceof \Traversable) {
+ // Must throw a ConstraintDefinitionException for backwards
+ // compatibility reasons with Symfony < 2.5
+ throw new ConstraintDefinitionException(sprintf(
+ 'Traversal was enabled for "%s", but this class '.
+ 'does not implement "\Traversable".',
+ get_class($object)
+ ));
+ }
+
+ $this->validateEachObjectIn(
+ $object,
+ $propertyPath,
+ $groups,
+ $traversalStrategy & TraversalStrategy::STOP_RECURSION,
+ $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. Arrays are always traversed.
+ *
+ * @param mixed $value The validated value
+ * @param object|null $object The current object
+ * @param string $cacheKey The key for caching
+ * the validated value
+ * @param MetadataInterface $metadata The metadata of the
+ * value
+ * @param string $propertyPath The property path leading
+ * to the value
+ * @param string[] $groups The groups in which the
+ * value should be validated
+ * @param string[]|null $cascadedGroups The groups in which
+ * cascaded objects should
+ * be validated
+ * @param int $traversalStrategy The strategy used for
+ * traversing the value
+ * @param ExecutionContextInterface $context The current execution context
+ *
+ * @see TraversalStrategy
+ */
+ private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $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 have an array or a cascaded object
+ if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) {
+ return;
+ }
+
+ // If no specific traversal strategy was requested when this method
+ // was called, use the traversal strategy of the node's metadata
+ if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
+ // Keep the STOP_RECURSION flag, if it was set
+ $traversalStrategy = $metadata->getTraversalStrategy()
+ | ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
+ }
+
+ // 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 (is_array($value)) {
+ // Arrays are always traversed, independent of the specified
+ // traversal strategy
+ // (BC with Symfony < 2.5)
+ $this->validateEachObjectIn(
+ $value,
+ $propertyPath,
+ $cascadedGroups,
+ $traversalStrategy & TraversalStrategy::STOP_RECURSION,
+ $context
+ );
+
+ return;
+ }
+
+ // If the value is a scalar, pass it anyway, because we want
+ // a NoSuchMetadataException to be thrown in that case
+ // (BC with Symfony < 2.5)
+ $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.
+ *
+ * @param mixed $value The validated value
+ * @param object|null $object The current object
+ * @param string $cacheKey The key for caching
+ * the validated value
+ * @param MetadataInterface $metadata The metadata of the
+ * value
+ * @param string $propertyPath The property path leading
+ * to the value
+ * @param int $traversalStrategy The strategy used for
+ * traversing the value
+ * @param GroupSequence $groupSequence The group sequence
+ * @param string|null $cascadedGroup The group that should
+ * be passed to cascaded
+ * objects instead of
+ * the group sequence
+ * @param ExecutionContextInterface $context The execution context
+ */
+ private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
+ {
+ $violationCount = count($context->getViolations());
+ $cascadedGroups = $cascadedGroup ? array($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 generated
+ if (count($context->getViolations()) > $violationCount) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Validates a node's value against all constraints in the given group.
+ *
+ * @param mixed $value The validated value
+ * @param string $cacheKey The key for caching the
+ * validated value
+ * @param MetadataInterface $metadata The metadata of the value
+ * @param string $group The group to validate
+ * @param ExecutionContextInterface $context The execution context
+ */
+ private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context)
+ {
+ $context->setGroup($group);
+
+ foreach ($metadata->findConstraints($group) as $constraint) {
+ // Prevent duplicate validation of constraints, in the case
+ // that constraints belong to multiple validated groups
+ if (null !== $cacheKey) {
+ $constraintHash = spl_object_hash($constraint);
+
+ if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
+ continue;
+ }
+
+ $context->markConstraintAsValidated($cacheKey, $constraintHash);
+ }
+
+ $context->setConstraint($constraint);
+
+ $validator = $this->validatorFactory->getInstance($constraint);
+ $validator->initialize($context);
+ $validator->validate($value, $constraint);
+ }
+ }
+}