4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Validator\Validator;
14 use Symfony\Component\Validator\Constraint;
15 use Symfony\Component\Validator\Constraints\GroupSequence;
16 use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
17 use Symfony\Component\Validator\Context\ExecutionContext;
18 use Symfony\Component\Validator\Context\ExecutionContextInterface;
19 use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
20 use Symfony\Component\Validator\Exception\NoSuchMetadataException;
21 use Symfony\Component\Validator\Exception\RuntimeException;
22 use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
23 use Symfony\Component\Validator\Exception\ValidatorException;
24 use Symfony\Component\Validator\Mapping\CascadingStrategy;
25 use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
26 use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
27 use Symfony\Component\Validator\Mapping\GenericMetadata;
28 use Symfony\Component\Validator\Mapping\MetadataInterface;
29 use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
30 use Symfony\Component\Validator\Mapping\TraversalStrategy;
31 use Symfony\Component\Validator\ObjectInitializerInterface;
32 use Symfony\Component\Validator\Util\PropertyPath;
35 * Recursive implementation of {@link ContextualValidatorInterface}.
37 * @author Bernhard Schussek <bschussek@gmail.com>
39 class RecursiveContextualValidator implements ContextualValidatorInterface
42 private $defaultPropertyPath;
43 private $defaultGroups;
44 private $metadataFactory;
45 private $validatorFactory;
46 private $objectInitializers;
49 * Creates a validator for the given context.
51 * @param ExecutionContextInterface $context The execution context
52 * @param MetadataFactoryInterface $metadataFactory The factory for
53 * fetching the metadata
54 * of validated objects
55 * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating
56 * constraint validators
57 * @param ObjectInitializerInterface[] $objectInitializers The object initializers
59 public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array())
61 $this->context = $context;
62 $this->defaultPropertyPath = $context->getPropertyPath();
63 $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
64 $this->metadataFactory = $metadataFactory;
65 $this->validatorFactory = $validatorFactory;
66 $this->objectInitializers = $objectInitializers;
72 public function atPath($path)
74 $this->defaultPropertyPath = $this->context->getPropertyPath($path);
82 public function validate($value, $constraints = null, $groups = null)
84 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
86 $previousValue = $this->context->getValue();
87 $previousObject = $this->context->getObject();
88 $previousMetadata = $this->context->getMetadata();
89 $previousPath = $this->context->getPropertyPath();
90 $previousGroup = $this->context->getGroup();
91 $previousConstraint = null;
93 if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) {
94 $previousConstraint = $this->context->getConstraint();
97 // If explicit constraints are passed, validate the value against
99 if (null !== $constraints) {
100 // You can pass a single constraint or an array of constraints
101 // Make sure to deal with an array in the rest of the code
102 if (!\is_array($constraints)) {
103 $constraints = array($constraints);
106 $metadata = new GenericMetadata();
107 $metadata->addConstraints($constraints);
109 $this->validateGenericNode(
112 \is_object($value) ? spl_object_hash($value) : null,
114 $this->defaultPropertyPath,
117 TraversalStrategy::IMPLICIT,
121 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
122 $this->context->setGroup($previousGroup);
124 if (null !== $previousConstraint) {
125 $this->context->setConstraint($previousConstraint);
131 // If an object is passed without explicit constraints, validate that
132 // object against the constraints defined for the object's class
133 if (\is_object($value)) {
134 $this->validateObject(
136 $this->defaultPropertyPath,
138 TraversalStrategy::IMPLICIT,
142 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
143 $this->context->setGroup($previousGroup);
148 // If an array is passed without explicit constraints, validate each
149 // object in the array
150 if (\is_array($value)) {
151 $this->validateEachObjectIn(
153 $this->defaultPropertyPath,
158 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
159 $this->context->setGroup($previousGroup);
164 throw new RuntimeException(sprintf('Cannot validate values of type "%s" automatically. Please provide a constraint.', \gettype($value)));
170 public function validateProperty($object, $propertyName, $groups = null)
172 $classMetadata = $this->metadataFactory->getMetadataFor($object);
174 if (!$classMetadata instanceof ClassMetadataInterface) {
175 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)));
178 $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
179 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
180 $cacheKey = spl_object_hash($object);
181 $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
183 $previousValue = $this->context->getValue();
184 $previousObject = $this->context->getObject();
185 $previousMetadata = $this->context->getMetadata();
186 $previousPath = $this->context->getPropertyPath();
187 $previousGroup = $this->context->getGroup();
189 foreach ($propertyMetadatas as $propertyMetadata) {
190 $propertyValue = $propertyMetadata->getPropertyValue($object);
192 $this->validateGenericNode(
195 $cacheKey.':'.\get_class($object).':'.$propertyName,
200 TraversalStrategy::IMPLICIT,
205 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
206 $this->context->setGroup($previousGroup);
214 public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
216 $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
218 if (!$classMetadata instanceof ClassMetadataInterface) {
219 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)));
222 $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
223 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
225 if (\is_object($objectOrClass)) {
226 $object = $objectOrClass;
227 $class = \get_class($object);
228 $cacheKey = spl_object_hash($objectOrClass);
229 $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
231 // $objectOrClass contains a class name
233 $class = $objectOrClass;
235 $propertyPath = $this->defaultPropertyPath;
238 $previousValue = $this->context->getValue();
239 $previousObject = $this->context->getObject();
240 $previousMetadata = $this->context->getMetadata();
241 $previousPath = $this->context->getPropertyPath();
242 $previousGroup = $this->context->getGroup();
244 foreach ($propertyMetadatas as $propertyMetadata) {
245 $this->validateGenericNode(
248 $cacheKey.':'.$class.':'.$propertyName,
253 TraversalStrategy::IMPLICIT,
258 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
259 $this->context->setGroup($previousGroup);
267 public function getViolations()
269 return $this->context->getViolations();
273 * Normalizes the given group or list of groups to an array.
275 * @param string|GroupSequence|(string|GroupSequence)[] $groups The groups to normalize
277 * @return (string|GroupSequence)[] A group array
279 protected function normalizeGroups($groups)
281 if (\is_array($groups)) {
285 return array($groups);
289 * Validates an object against the constraints defined for its class.
291 * If no metadata is available for the class, but the class is an instance
292 * of {@link \Traversable} and the selected traversal strategy allows
293 * traversal, the object will be iterated and each nested object will be
296 * @param object $object The object to cascade
297 * @param string $propertyPath The current property path
298 * @param (string|GroupSequence)[] $groups The validated groups
299 * @param int $traversalStrategy The strategy for traversing the
301 * @param ExecutionContextInterface $context The current execution context
303 * @throws NoSuchMetadataException If the object has no associated metadata
304 * and does not implement {@link \Traversable}
305 * or if traversal is disabled via the
306 * $traversalStrategy argument
307 * @throws UnsupportedMetadataException If the metadata returned by the
308 * metadata factory does not implement
309 * {@link ClassMetadataInterface}
311 private function validateObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context)
314 $classMetadata = $this->metadataFactory->getMetadataFor($object);
316 if (!$classMetadata instanceof ClassMetadataInterface) {
317 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)));
320 $this->validateClassNode(
322 spl_object_hash($object),
330 } catch (NoSuchMetadataException $e) {
331 // Rethrow if not Traversable
332 if (!$object instanceof \Traversable) {
336 // Rethrow unless IMPLICIT or TRAVERSE
337 if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
341 $this->validateEachObjectIn(
351 * Validates each object in a collection against the constraints defined
354 * If the parameter $recursive is set to true, nested {@link \Traversable}
355 * objects are iterated as well. Nested arrays are always iterated,
356 * regardless of the value of $recursive.
358 * @param iterable $collection The collection
359 * @param string $propertyPath The current property path
360 * @param (string|GroupSequence)[] $groups The validated groups
361 * @param ExecutionContextInterface $context The current execution context
364 * @see CollectionNode
366 private function validateEachObjectIn($collection, $propertyPath, array $groups, ExecutionContextInterface $context)
368 foreach ($collection as $key => $value) {
369 if (\is_array($value)) {
370 // Arrays are always cascaded, independent of the specified
371 // traversal strategy
372 $this->validateEachObjectIn(
374 $propertyPath.'['.$key.']',
382 // Scalar and null values in the collection are ignored
383 if (\is_object($value)) {
384 $this->validateObject(
386 $propertyPath.'['.$key.']',
388 TraversalStrategy::IMPLICIT,
396 * Validates a class node.
398 * A class node is a combination of an object with a {@link ClassMetadataInterface}
399 * instance. Each class node (conceptionally) has zero or more succeeding
402 * (Article:class node)
404 * ($title:property node)
406 * This method validates the passed objects against all constraints defined
407 * at class level. It furthermore triggers the validation of each of the
408 * class' properties against the constraints for that property.
410 * If the selected traversal strategy allows traversal, the object is
411 * iterated and each nested object is validated against its own constraints.
412 * The object is not traversed if traversal is disabled in the class
415 * If the passed groups contain the group "Default", the validator will
416 * check whether the "Default" group has been replaced by a group sequence
417 * in the class metadata. If this is the case, the group sequence is
420 * @param object $object The validated object
421 * @param string $cacheKey The key for caching
422 * the validated object
423 * @param ClassMetadataInterface $metadata The class metadata of
425 * @param string $propertyPath The property path leading
427 * @param (string|GroupSequence)[] $groups The groups in which the
428 * object should be validated
429 * @param string[]|null $cascadedGroups The groups in which
430 * cascaded objects should
432 * @param int $traversalStrategy The strategy used for
433 * traversing the object
434 * @param ExecutionContextInterface $context The current execution context
436 * @throws UnsupportedMetadataException If a property metadata does not
437 * implement {@link PropertyMetadataInterface}
438 * @throws ConstraintDefinitionException If traversal was enabled but the
439 * object does not implement
440 * {@link \Traversable}
442 * @see TraversalStrategy
444 private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
446 $context->setNode($object, $object, $metadata, $propertyPath);
448 if (!$context->isObjectInitialized($cacheKey)) {
449 foreach ($this->objectInitializers as $initializer) {
450 $initializer->initialize($object);
453 $context->markObjectAsInitialized($cacheKey);
456 foreach ($groups as $key => $group) {
457 // If the "Default" group is replaced by a group sequence, remember
458 // to cascade the "Default" group when traversing the group
460 $defaultOverridden = false;
462 // Use the object hash for group sequences
463 $groupHash = \is_object($group) ? spl_object_hash($group) : $group;
465 if ($context->isGroupValidated($cacheKey, $groupHash)) {
466 // Skip this group when validating the properties and when
467 // traversing the object
468 unset($groups[$key]);
473 $context->markGroupAsValidated($cacheKey, $groupHash);
475 // Replace the "Default" group by the group sequence defined
476 // for the class, if applicable.
477 // This is done after checking the cache, so that
478 // spl_object_hash() isn't called for this sequence and
479 // "Default" is used instead in the cache. This is useful
480 // if the getters below return different group sequences in
482 if (Constraint::DEFAULT_GROUP === $group) {
483 if ($metadata->hasGroupSequence()) {
484 // The group sequence is statically defined for the class
485 $group = $metadata->getGroupSequence();
486 $defaultOverridden = true;
487 } elseif ($metadata->isGroupSequenceProvider()) {
488 // The group sequence is dynamically obtained from the validated
490 /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
491 $group = $object->getGroupSequence();
492 $defaultOverridden = true;
494 if (!$group instanceof GroupSequence) {
495 $group = new GroupSequence($group);
500 // If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
501 // (=<G1,G2>), then call validateClassNode() with each entry of the
502 // group sequence and abort if necessary (G1, G2)
503 if ($group instanceof GroupSequence) {
504 $this->stepThroughGroupSequence(
512 $defaultOverridden ? Constraint::DEFAULT_GROUP : null,
516 // Skip the group sequence when validating properties, because
517 // stepThroughGroupSequence() already validates the properties
518 unset($groups[$key]);
523 $this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
526 // If no more groups should be validated for the property nodes,
527 // we can safely quit
528 if (0 === \count($groups)) {
532 // Validate all properties against their constraints
533 foreach ($metadata->getConstrainedProperties() as $propertyName) {
534 // If constraints are defined both on the getter of a property as
535 // well as on the property itself, then getPropertyMetadata()
536 // returns two metadata objects, not just one
537 foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
538 if (!$propertyMetadata instanceof PropertyMetadataInterface) {
539 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)));
542 $propertyValue = $propertyMetadata->getPropertyValue($object);
544 $this->validateGenericNode(
547 $cacheKey.':'.\get_class($object).':'.$propertyName,
549 PropertyPath::append($propertyPath, $propertyName),
552 TraversalStrategy::IMPLICIT,
558 // If no specific traversal strategy was requested when this method
559 // was called, use the traversal strategy of the class' metadata
560 if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
561 $traversalStrategy = $metadata->getTraversalStrategy();
564 // Traverse only if IMPLICIT or TRAVERSE
565 if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
569 // If IMPLICIT, stop unless we deal with a Traversable
570 if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
574 // If TRAVERSE, fail if we have no Traversable
575 if (!$object instanceof \Traversable) {
576 throw new ConstraintDefinitionException(sprintf('Traversal was enabled for "%s", but this class does not implement "\Traversable".', \get_class($object)));
579 $this->validateEachObjectIn(
588 * Validates a node that is not a class node.
590 * Currently, two such node types exist:
592 * - property nodes, which consist of the value of an object's
593 * property together with a {@link PropertyMetadataInterface} instance
594 * - generic nodes, which consist of a value and some arbitrary
595 * constraints defined in a {@link MetadataInterface} container
597 * In both cases, the value is validated against all constraints defined
598 * in the passed metadata object. Then, if the value is an instance of
599 * {@link \Traversable} and the selected traversal strategy permits it,
600 * the value is traversed and each nested object validated against its own
601 * constraints. Arrays are always traversed.
603 * @param mixed $value The validated value
604 * @param object|null $object The current object
605 * @param string $cacheKey The key for caching
606 * the validated value
607 * @param MetadataInterface $metadata The metadata of the
609 * @param string $propertyPath The property path leading
611 * @param (string|GroupSequence)[] $groups The groups in which the
612 * value should be validated
613 * @param string[]|null $cascadedGroups The groups in which
614 * cascaded objects should
616 * @param int $traversalStrategy The strategy used for
617 * traversing the value
618 * @param ExecutionContextInterface $context The current execution context
620 * @see TraversalStrategy
622 private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
624 $context->setNode($value, $object, $metadata, $propertyPath);
626 foreach ($groups as $key => $group) {
627 if ($group instanceof GroupSequence) {
628 $this->stepThroughGroupSequence(
640 // Skip the group sequence when cascading, as the cascading
641 // logic is already done in stepThroughGroupSequence()
642 unset($groups[$key]);
647 $this->validateInGroup($value, $cacheKey, $metadata, $group, $context);
650 if (0 === \count($groups)) {
654 if (null === $value) {
658 $cascadingStrategy = $metadata->getCascadingStrategy();
660 // Quit unless we have an array or a cascaded object
661 if (!\is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) {
665 // If no specific traversal strategy was requested when this method
666 // was called, use the traversal strategy of the node's metadata
667 if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
668 $traversalStrategy = $metadata->getTraversalStrategy();
671 // The $cascadedGroups property is set, if the "Default" group is
672 // overridden by a group sequence
673 // See validateClassNode()
674 $cascadedGroups = null !== $cascadedGroups && \count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
676 if (\is_array($value)) {
677 // Arrays are always traversed, independent of the specified
678 // traversal strategy
679 $this->validateEachObjectIn(
689 // If the value is a scalar, pass it anyway, because we want
690 // a NoSuchMetadataException to be thrown in that case
691 $this->validateObject(
699 // Currently, the traversal strategy can only be TRAVERSE for a
700 // generic node if the cascading strategy is CASCADE. Thus, traversable
701 // objects will always be handled within validateObject() and there's
702 // nothing more to do here.
704 // see GenericMetadata::addConstraint()
708 * Sequentially validates a node's value in each group of a group sequence.
710 * If any of the constraints generates a violation, subsequent groups in the
711 * group sequence are skipped.
713 * @param mixed $value The validated value
714 * @param object|null $object The current object
715 * @param string $cacheKey The key for caching
716 * the validated value
717 * @param MetadataInterface $metadata The metadata of the
719 * @param string $propertyPath The property path leading
721 * @param int $traversalStrategy The strategy used for
722 * traversing the value
723 * @param GroupSequence $groupSequence The group sequence
724 * @param string|null $cascadedGroup The group that should
725 * be passed to cascaded
728 * @param ExecutionContextInterface $context The execution context
730 private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
732 $violationCount = \count($context->getViolations());
733 $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null;
735 foreach ($groupSequence->groups as $groupInSequence) {
736 $groups = (array) $groupInSequence;
738 if ($metadata instanceof ClassMetadataInterface) {
739 $this->validateClassNode(
750 $this->validateGenericNode(
763 // Abort sequence validation if a violation was generated
764 if (\count($context->getViolations()) > $violationCount) {
771 * Validates a node's value against all constraints in the given group.
773 * @param mixed $value The validated value
774 * @param string $cacheKey The key for caching the
776 * @param MetadataInterface $metadata The metadata of the value
777 * @param string $group The group to validate
778 * @param ExecutionContextInterface $context The execution context
780 private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context)
782 $context->setGroup($group);
784 foreach ($metadata->findConstraints($group) as $constraint) {
785 // Prevent duplicate validation of constraints, in the case
786 // that constraints belong to multiple validated groups
787 if (null !== $cacheKey) {
788 $constraintHash = spl_object_hash($constraint);
790 if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
794 $context->markConstraintAsValidated($cacheKey, $constraintHash);
797 $context->setConstraint($constraint);
799 $validator = $this->validatorFactory->getInstance($constraint);
800 $validator->initialize($context);
801 $validator->validate($value, $constraint);