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\GenericMetadata;
27 use Symfony\Component\Validator\Mapping\MetadataInterface;
28 use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
29 use Symfony\Component\Validator\Mapping\TraversalStrategy;
30 use Symfony\Component\Validator\MetadataFactoryInterface;
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 * @var ExecutionContextInterface
49 private $defaultPropertyPath;
54 private $defaultGroups;
57 * @var MetadataFactoryInterface
59 private $metadataFactory;
62 * @var ConstraintValidatorFactoryInterface
64 private $validatorFactory;
67 * @var ObjectInitializerInterface[]
69 private $objectInitializers;
72 * Creates a validator for the given context.
74 * @param ExecutionContextInterface $context The execution context
75 * @param MetadataFactoryInterface $metadataFactory The factory for
76 * fetching the metadata
77 * of validated objects
78 * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating
79 * constraint validators
80 * @param ObjectInitializerInterface[] $objectInitializers The object initializers
82 public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array())
84 $this->context = $context;
85 $this->defaultPropertyPath = $context->getPropertyPath();
86 $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
87 $this->metadataFactory = $metadataFactory;
88 $this->validatorFactory = $validatorFactory;
89 $this->objectInitializers = $objectInitializers;
95 public function atPath($path)
97 $this->defaultPropertyPath = $this->context->getPropertyPath($path);
105 public function validate($value, $constraints = null, $groups = null)
107 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
109 $previousValue = $this->context->getValue();
110 $previousObject = $this->context->getObject();
111 $previousMetadata = $this->context->getMetadata();
112 $previousPath = $this->context->getPropertyPath();
113 $previousGroup = $this->context->getGroup();
114 $previousConstraint = null;
116 if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) {
117 $previousConstraint = $this->context->getConstraint();
120 // If explicit constraints are passed, validate the value against
122 if (null !== $constraints) {
123 // You can pass a single constraint or an array of constraints
124 // Make sure to deal with an array in the rest of the code
125 if (!is_array($constraints)) {
126 $constraints = array($constraints);
129 $metadata = new GenericMetadata();
130 $metadata->addConstraints($constraints);
132 $this->validateGenericNode(
135 is_object($value) ? spl_object_hash($value) : null,
137 $this->defaultPropertyPath,
140 TraversalStrategy::IMPLICIT,
144 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
145 $this->context->setGroup($previousGroup);
147 if (null !== $previousConstraint) {
148 $this->context->setConstraint($previousConstraint);
154 // If an object is passed without explicit constraints, validate that
155 // object against the constraints defined for the object's class
156 if (is_object($value)) {
157 $this->validateObject(
159 $this->defaultPropertyPath,
161 TraversalStrategy::IMPLICIT,
165 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
166 $this->context->setGroup($previousGroup);
171 // If an array is passed without explicit constraints, validate each
172 // object in the array
173 if (is_array($value)) {
174 $this->validateEachObjectIn(
176 $this->defaultPropertyPath,
182 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
183 $this->context->setGroup($previousGroup);
188 throw new RuntimeException(sprintf(
189 'Cannot validate values of type "%s" automatically. Please '.
190 'provide a constraint.',
198 public function validateProperty($object, $propertyName, $groups = null)
200 $classMetadata = $this->metadataFactory->getMetadataFor($object);
202 if (!$classMetadata instanceof ClassMetadataInterface) {
203 // Cannot be UnsupportedMetadataException because of BC with
205 throw new ValidatorException(sprintf(
206 'The metadata factory should return instances of '.
207 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
209 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
213 $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
214 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
215 $cacheKey = spl_object_hash($object);
216 $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
218 $previousValue = $this->context->getValue();
219 $previousObject = $this->context->getObject();
220 $previousMetadata = $this->context->getMetadata();
221 $previousPath = $this->context->getPropertyPath();
222 $previousGroup = $this->context->getGroup();
224 foreach ($propertyMetadatas as $propertyMetadata) {
225 $propertyValue = $propertyMetadata->getPropertyValue($object);
227 $this->validateGenericNode(
230 $cacheKey.':'.$propertyName,
235 TraversalStrategy::IMPLICIT,
240 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
241 $this->context->setGroup($previousGroup);
249 public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
251 $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
253 if (!$classMetadata instanceof ClassMetadataInterface) {
254 // Cannot be UnsupportedMetadataException because of BC with
256 throw new ValidatorException(sprintf(
257 'The metadata factory should return instances of '.
258 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
260 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
264 $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
265 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
267 if (is_object($objectOrClass)) {
268 $object = $objectOrClass;
269 $cacheKey = spl_object_hash($objectOrClass);
270 $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
272 // $objectOrClass contains a class name
275 $propertyPath = $this->defaultPropertyPath;
278 $previousValue = $this->context->getValue();
279 $previousObject = $this->context->getObject();
280 $previousMetadata = $this->context->getMetadata();
281 $previousPath = $this->context->getPropertyPath();
282 $previousGroup = $this->context->getGroup();
284 foreach ($propertyMetadatas as $propertyMetadata) {
285 $this->validateGenericNode(
288 $cacheKey.':'.$propertyName,
293 TraversalStrategy::IMPLICIT,
298 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
299 $this->context->setGroup($previousGroup);
307 public function getViolations()
309 return $this->context->getViolations();
313 * Normalizes the given group or list of groups to an array.
315 * @param mixed $groups The groups to normalize
317 * @return array A group array
319 protected function normalizeGroups($groups)
321 if (is_array($groups)) {
325 return array($groups);
329 * Validates an object against the constraints defined for its class.
331 * If no metadata is available for the class, but the class is an instance
332 * of {@link \Traversable} and the selected traversal strategy allows
333 * traversal, the object will be iterated and each nested object will be
336 * @param object $object The object to cascade
337 * @param string $propertyPath The current property path
338 * @param string[] $groups The validated groups
339 * @param int $traversalStrategy The strategy for traversing the
341 * @param ExecutionContextInterface $context The current execution context
343 * @throws NoSuchMetadataException If the object has no associated metadata
344 * and does not implement {@link \Traversable}
345 * or if traversal is disabled via the
346 * $traversalStrategy argument
347 * @throws UnsupportedMetadataException If the metadata returned by the
348 * metadata factory does not implement
349 * {@link ClassMetadataInterface}
351 private function validateObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context)
354 $classMetadata = $this->metadataFactory->getMetadataFor($object);
356 if (!$classMetadata instanceof ClassMetadataInterface) {
357 throw new UnsupportedMetadataException(sprintf(
358 'The metadata factory should return instances of '.
359 '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
361 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
365 $this->validateClassNode(
367 spl_object_hash($object),
375 } catch (NoSuchMetadataException $e) {
376 // Rethrow if not Traversable
377 if (!$object instanceof \Traversable) {
381 // Rethrow unless IMPLICIT or TRAVERSE
382 if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
386 $this->validateEachObjectIn(
390 $traversalStrategy & TraversalStrategy::STOP_RECURSION,
397 * Validates each object in a collection against the constraints defined
400 * If the parameter $recursive is set to true, nested {@link \Traversable}
401 * objects are iterated as well. Nested arrays are always iterated,
402 * regardless of the value of $recursive.
404 * @param array|\Traversable $collection The collection
405 * @param string $propertyPath The current property path
406 * @param string[] $groups The validated groups
407 * @param bool $stopRecursion Whether to disable
408 * recursive iteration. For
409 * backwards compatibility
410 * with Symfony < 2.5.
411 * @param ExecutionContextInterface $context The current execution context
414 * @see CollectionNode
416 private function validateEachObjectIn($collection, $propertyPath, array $groups, $stopRecursion, ExecutionContextInterface $context)
418 if ($stopRecursion) {
419 $traversalStrategy = TraversalStrategy::NONE;
421 $traversalStrategy = TraversalStrategy::IMPLICIT;
424 foreach ($collection as $key => $value) {
425 if (is_array($value)) {
426 // Arrays are always cascaded, independent of the specified
427 // traversal strategy
428 // (BC with Symfony < 2.5)
429 $this->validateEachObjectIn(
431 $propertyPath.'['.$key.']',
440 // Scalar and null values in the collection are ignored
441 // (BC with Symfony < 2.5)
442 if (is_object($value)) {
443 $this->validateObject(
445 $propertyPath.'['.$key.']',
455 * Validates a class node.
457 * A class node is a combination of an object with a {@link ClassMetadataInterface}
458 * instance. Each class node (conceptionally) has zero or more succeeding
461 * (Article:class node)
463 * ($title:property node)
465 * This method validates the passed objects against all constraints defined
466 * at class level. It furthermore triggers the validation of each of the
467 * class' properties against the constraints for that property.
469 * If the selected traversal strategy allows traversal, the object is
470 * iterated and each nested object is validated against its own constraints.
471 * The object is not traversed if traversal is disabled in the class
474 * If the passed groups contain the group "Default", the validator will
475 * check whether the "Default" group has been replaced by a group sequence
476 * in the class metadata. If this is the case, the group sequence is
479 * @param object $object The validated object
480 * @param string $cacheKey The key for caching
481 * the validated object
482 * @param ClassMetadataInterface $metadata The class metadata of
484 * @param string $propertyPath The property path leading
486 * @param string[] $groups The groups in which the
487 * object should be validated
488 * @param string[]|null $cascadedGroups The groups in which
489 * cascaded objects should
491 * @param int $traversalStrategy The strategy used for
492 * traversing the object
493 * @param ExecutionContextInterface $context The current execution context
495 * @throws UnsupportedMetadataException If a property metadata does not
496 * implement {@link PropertyMetadataInterface}
497 * @throws ConstraintDefinitionException If traversal was enabled but the
498 * object does not implement
499 * {@link \Traversable}
501 * @see TraversalStrategy
503 private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
505 $context->setNode($object, $object, $metadata, $propertyPath);
507 if (!$context->isObjectInitialized($cacheKey)) {
508 foreach ($this->objectInitializers as $initializer) {
509 $initializer->initialize($object);
512 $context->markObjectAsInitialized($cacheKey);
515 foreach ($groups as $key => $group) {
516 // If the "Default" group is replaced by a group sequence, remember
517 // to cascade the "Default" group when traversing the group
519 $defaultOverridden = false;
521 // Use the object hash for group sequences
522 $groupHash = is_object($group) ? spl_object_hash($group) : $group;
524 if ($context->isGroupValidated($cacheKey, $groupHash)) {
525 // Skip this group when validating the properties and when
526 // traversing the object
527 unset($groups[$key]);
532 $context->markGroupAsValidated($cacheKey, $groupHash);
534 // Replace the "Default" group by the group sequence defined
535 // for the class, if applicable.
536 // This is done after checking the cache, so that
537 // spl_object_hash() isn't called for this sequence and
538 // "Default" is used instead in the cache. This is useful
539 // if the getters below return different group sequences in
541 if (Constraint::DEFAULT_GROUP === $group) {
542 if ($metadata->hasGroupSequence()) {
543 // The group sequence is statically defined for the class
544 $group = $metadata->getGroupSequence();
545 $defaultOverridden = true;
546 } elseif ($metadata->isGroupSequenceProvider()) {
547 // The group sequence is dynamically obtained from the validated
549 /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
550 $group = $object->getGroupSequence();
551 $defaultOverridden = true;
553 if (!$group instanceof GroupSequence) {
554 $group = new GroupSequence($group);
559 // If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
560 // (=<G1,G2>), then call validateClassNode() with each entry of the
561 // group sequence and abort if necessary (G1, G2)
562 if ($group instanceof GroupSequence) {
563 $this->stepThroughGroupSequence(
571 $defaultOverridden ? Constraint::DEFAULT_GROUP : null,
575 // Skip the group sequence when validating properties, because
576 // stepThroughGroupSequence() already validates the properties
577 unset($groups[$key]);
582 $this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
585 // If no more groups should be validated for the property nodes,
586 // we can safely quit
587 if (0 === count($groups)) {
591 // Validate all properties against their constraints
592 foreach ($metadata->getConstrainedProperties() as $propertyName) {
593 // If constraints are defined both on the getter of a property as
594 // well as on the property itself, then getPropertyMetadata()
595 // returns two metadata objects, not just one
596 foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
597 if (!$propertyMetadata instanceof PropertyMetadataInterface) {
598 throw new UnsupportedMetadataException(sprintf(
599 'The property metadata instances should implement '.
600 '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '.
602 is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata)
606 $propertyValue = $propertyMetadata->getPropertyValue($object);
608 $this->validateGenericNode(
611 $cacheKey.':'.$propertyName,
613 PropertyPath::append($propertyPath, $propertyName),
616 TraversalStrategy::IMPLICIT,
622 // If no specific traversal strategy was requested when this method
623 // was called, use the traversal strategy of the class' metadata
624 if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
625 // Keep the STOP_RECURSION flag, if it was set
626 $traversalStrategy = $metadata->getTraversalStrategy()
627 | ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
630 // Traverse only if IMPLICIT or TRAVERSE
631 if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
635 // If IMPLICIT, stop unless we deal with a Traversable
636 if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
640 // If TRAVERSE, fail if we have no Traversable
641 if (!$object instanceof \Traversable) {
642 // Must throw a ConstraintDefinitionException for backwards
643 // compatibility reasons with Symfony < 2.5
644 throw new ConstraintDefinitionException(sprintf(
645 'Traversal was enabled for "%s", but this class '.
646 'does not implement "\Traversable".',
651 $this->validateEachObjectIn(
655 $traversalStrategy & TraversalStrategy::STOP_RECURSION,
661 * Validates a node that is not a class node.
663 * Currently, two such node types exist:
665 * - property nodes, which consist of the value of an object's
666 * property together with a {@link PropertyMetadataInterface} instance
667 * - generic nodes, which consist of a value and some arbitrary
668 * constraints defined in a {@link MetadataInterface} container
670 * In both cases, the value is validated against all constraints defined
671 * in the passed metadata object. Then, if the value is an instance of
672 * {@link \Traversable} and the selected traversal strategy permits it,
673 * the value is traversed and each nested object validated against its own
674 * constraints. Arrays are always traversed.
676 * @param mixed $value The validated value
677 * @param object|null $object The current object
678 * @param string $cacheKey The key for caching
679 * the validated value
680 * @param MetadataInterface $metadata The metadata of the
682 * @param string $propertyPath The property path leading
684 * @param string[] $groups The groups in which the
685 * value should be validated
686 * @param string[]|null $cascadedGroups The groups in which
687 * cascaded objects should
689 * @param int $traversalStrategy The strategy used for
690 * traversing the value
691 * @param ExecutionContextInterface $context The current execution context
693 * @see TraversalStrategy
695 private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
697 $context->setNode($value, $object, $metadata, $propertyPath);
699 foreach ($groups as $key => $group) {
700 if ($group instanceof GroupSequence) {
701 $this->stepThroughGroupSequence(
713 // Skip the group sequence when cascading, as the cascading
714 // logic is already done in stepThroughGroupSequence()
715 unset($groups[$key]);
720 $this->validateInGroup($value, $cacheKey, $metadata, $group, $context);
723 if (0 === count($groups)) {
727 if (null === $value) {
731 $cascadingStrategy = $metadata->getCascadingStrategy();
733 // Quit unless we have an array or a cascaded object
734 if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) {
738 // If no specific traversal strategy was requested when this method
739 // was called, use the traversal strategy of the node's metadata
740 if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
741 // Keep the STOP_RECURSION flag, if it was set
742 $traversalStrategy = $metadata->getTraversalStrategy()
743 | ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
746 // The $cascadedGroups property is set, if the "Default" group is
747 // overridden by a group sequence
748 // See validateClassNode()
749 $cascadedGroups = null !== $cascadedGroups && count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
751 if (is_array($value)) {
752 // Arrays are always traversed, independent of the specified
753 // traversal strategy
754 // (BC with Symfony < 2.5)
755 $this->validateEachObjectIn(
759 $traversalStrategy & TraversalStrategy::STOP_RECURSION,
766 // If the value is a scalar, pass it anyway, because we want
767 // a NoSuchMetadataException to be thrown in that case
768 // (BC with Symfony < 2.5)
769 $this->validateObject(
777 // Currently, the traversal strategy can only be TRAVERSE for a
778 // generic node if the cascading strategy is CASCADE. Thus, traversable
779 // objects will always be handled within validateObject() and there's
780 // nothing more to do here.
782 // see GenericMetadata::addConstraint()
786 * Sequentially validates a node's value in each group of a group sequence.
788 * If any of the constraints generates a violation, subsequent groups in the
789 * group sequence are skipped.
791 * @param mixed $value The validated value
792 * @param object|null $object The current object
793 * @param string $cacheKey The key for caching
794 * the validated value
795 * @param MetadataInterface $metadata The metadata of the
797 * @param string $propertyPath The property path leading
799 * @param int $traversalStrategy The strategy used for
800 * traversing the value
801 * @param GroupSequence $groupSequence The group sequence
802 * @param string|null $cascadedGroup The group that should
803 * be passed to cascaded
806 * @param ExecutionContextInterface $context The execution context
808 private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
810 $violationCount = count($context->getViolations());
811 $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null;
813 foreach ($groupSequence->groups as $groupInSequence) {
814 $groups = array($groupInSequence);
816 if ($metadata instanceof ClassMetadataInterface) {
817 $this->validateClassNode(
828 $this->validateGenericNode(
841 // Abort sequence validation if a violation was generated
842 if (count($context->getViolations()) > $violationCount) {
849 * Validates a node's value against all constraints in the given group.
851 * @param mixed $value The validated value
852 * @param string $cacheKey The key for caching the
854 * @param MetadataInterface $metadata The metadata of the value
855 * @param string $group The group to validate
856 * @param ExecutionContextInterface $context The execution context
858 private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context)
860 $context->setGroup($group);
862 foreach ($metadata->findConstraints($group) as $constraint) {
863 // Prevent duplicate validation of constraints, in the case
864 // that constraints belong to multiple validated groups
865 if (null !== $cacheKey) {
866 $constraintHash = spl_object_hash($constraint);
868 if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
872 $context->markConstraintAsValidated($cacheKey, $constraintHash);
875 $context->setConstraint($constraint);
877 $validator = $this->validatorFactory->getInstance($constraint);
878 $validator->initialize($context);
879 $validator->validate($value, $constraint);