annotate vendor/symfony/validator/Validator/RecursiveContextualValidator.php @ 8:50b0d041100e

Further files for download
author Chris Cannam
date Mon, 05 Feb 2018 10:56:40 +0000
parents 4c8ae668cc8c
children 1fec387a4317
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /*
Chris@0 4 * This file is part of the Symfony package.
Chris@0 5 *
Chris@0 6 * (c) Fabien Potencier <fabien@symfony.com>
Chris@0 7 *
Chris@0 8 * For the full copyright and license information, please view the LICENSE
Chris@0 9 * file that was distributed with this source code.
Chris@0 10 */
Chris@0 11
Chris@0 12 namespace Symfony\Component\Validator\Validator;
Chris@0 13
Chris@0 14 use Symfony\Component\Validator\Constraint;
Chris@0 15 use Symfony\Component\Validator\Constraints\GroupSequence;
Chris@0 16 use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
Chris@0 17 use Symfony\Component\Validator\Context\ExecutionContext;
Chris@0 18 use Symfony\Component\Validator\Context\ExecutionContextInterface;
Chris@0 19 use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
Chris@0 20 use Symfony\Component\Validator\Exception\NoSuchMetadataException;
Chris@0 21 use Symfony\Component\Validator\Exception\RuntimeException;
Chris@0 22 use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
Chris@0 23 use Symfony\Component\Validator\Exception\ValidatorException;
Chris@0 24 use Symfony\Component\Validator\Mapping\CascadingStrategy;
Chris@0 25 use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
Chris@0 26 use Symfony\Component\Validator\Mapping\GenericMetadata;
Chris@0 27 use Symfony\Component\Validator\Mapping\MetadataInterface;
Chris@0 28 use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
Chris@0 29 use Symfony\Component\Validator\Mapping\TraversalStrategy;
Chris@0 30 use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
Chris@0 31 use Symfony\Component\Validator\ObjectInitializerInterface;
Chris@0 32 use Symfony\Component\Validator\Util\PropertyPath;
Chris@0 33
Chris@0 34 /**
Chris@0 35 * Recursive implementation of {@link ContextualValidatorInterface}.
Chris@0 36 *
Chris@0 37 * @author Bernhard Schussek <bschussek@gmail.com>
Chris@0 38 */
Chris@0 39 class RecursiveContextualValidator implements ContextualValidatorInterface
Chris@0 40 {
Chris@0 41 /**
Chris@0 42 * @var ExecutionContextInterface
Chris@0 43 */
Chris@0 44 private $context;
Chris@0 45
Chris@0 46 /**
Chris@0 47 * @var string
Chris@0 48 */
Chris@0 49 private $defaultPropertyPath;
Chris@0 50
Chris@0 51 /**
Chris@0 52 * @var array
Chris@0 53 */
Chris@0 54 private $defaultGroups;
Chris@0 55
Chris@0 56 /**
Chris@0 57 * @var MetadataFactoryInterface
Chris@0 58 */
Chris@0 59 private $metadataFactory;
Chris@0 60
Chris@0 61 /**
Chris@0 62 * @var ConstraintValidatorFactoryInterface
Chris@0 63 */
Chris@0 64 private $validatorFactory;
Chris@0 65
Chris@0 66 /**
Chris@0 67 * @var ObjectInitializerInterface[]
Chris@0 68 */
Chris@0 69 private $objectInitializers;
Chris@0 70
Chris@0 71 /**
Chris@0 72 * Creates a validator for the given context.
Chris@0 73 *
Chris@0 74 * @param ExecutionContextInterface $context The execution context
Chris@0 75 * @param MetadataFactoryInterface $metadataFactory The factory for
Chris@0 76 * fetching the metadata
Chris@0 77 * of validated objects
Chris@0 78 * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating
Chris@0 79 * constraint validators
Chris@0 80 * @param ObjectInitializerInterface[] $objectInitializers The object initializers
Chris@0 81 */
Chris@0 82 public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array())
Chris@0 83 {
Chris@0 84 $this->context = $context;
Chris@0 85 $this->defaultPropertyPath = $context->getPropertyPath();
Chris@0 86 $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
Chris@0 87 $this->metadataFactory = $metadataFactory;
Chris@0 88 $this->validatorFactory = $validatorFactory;
Chris@0 89 $this->objectInitializers = $objectInitializers;
Chris@0 90 }
Chris@0 91
Chris@0 92 /**
Chris@0 93 * {@inheritdoc}
Chris@0 94 */
Chris@0 95 public function atPath($path)
Chris@0 96 {
Chris@0 97 $this->defaultPropertyPath = $this->context->getPropertyPath($path);
Chris@0 98
Chris@0 99 return $this;
Chris@0 100 }
Chris@0 101
Chris@0 102 /**
Chris@0 103 * {@inheritdoc}
Chris@0 104 */
Chris@0 105 public function validate($value, $constraints = null, $groups = null)
Chris@0 106 {
Chris@0 107 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
Chris@0 108
Chris@0 109 $previousValue = $this->context->getValue();
Chris@0 110 $previousObject = $this->context->getObject();
Chris@0 111 $previousMetadata = $this->context->getMetadata();
Chris@0 112 $previousPath = $this->context->getPropertyPath();
Chris@0 113 $previousGroup = $this->context->getGroup();
Chris@0 114 $previousConstraint = null;
Chris@0 115
Chris@0 116 if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) {
Chris@0 117 $previousConstraint = $this->context->getConstraint();
Chris@0 118 }
Chris@0 119
Chris@0 120 // If explicit constraints are passed, validate the value against
Chris@0 121 // those constraints
Chris@0 122 if (null !== $constraints) {
Chris@0 123 // You can pass a single constraint or an array of constraints
Chris@0 124 // Make sure to deal with an array in the rest of the code
Chris@0 125 if (!is_array($constraints)) {
Chris@0 126 $constraints = array($constraints);
Chris@0 127 }
Chris@0 128
Chris@0 129 $metadata = new GenericMetadata();
Chris@0 130 $metadata->addConstraints($constraints);
Chris@0 131
Chris@0 132 $this->validateGenericNode(
Chris@0 133 $value,
Chris@0 134 null,
Chris@0 135 is_object($value) ? spl_object_hash($value) : null,
Chris@0 136 $metadata,
Chris@0 137 $this->defaultPropertyPath,
Chris@0 138 $groups,
Chris@0 139 null,
Chris@0 140 TraversalStrategy::IMPLICIT,
Chris@0 141 $this->context
Chris@0 142 );
Chris@0 143
Chris@0 144 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
Chris@0 145 $this->context->setGroup($previousGroup);
Chris@0 146
Chris@0 147 if (null !== $previousConstraint) {
Chris@0 148 $this->context->setConstraint($previousConstraint);
Chris@0 149 }
Chris@0 150
Chris@0 151 return $this;
Chris@0 152 }
Chris@0 153
Chris@0 154 // If an object is passed without explicit constraints, validate that
Chris@0 155 // object against the constraints defined for the object's class
Chris@0 156 if (is_object($value)) {
Chris@0 157 $this->validateObject(
Chris@0 158 $value,
Chris@0 159 $this->defaultPropertyPath,
Chris@0 160 $groups,
Chris@0 161 TraversalStrategy::IMPLICIT,
Chris@0 162 $this->context
Chris@0 163 );
Chris@0 164
Chris@0 165 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
Chris@0 166 $this->context->setGroup($previousGroup);
Chris@0 167
Chris@0 168 return $this;
Chris@0 169 }
Chris@0 170
Chris@0 171 // If an array is passed without explicit constraints, validate each
Chris@0 172 // object in the array
Chris@0 173 if (is_array($value)) {
Chris@0 174 $this->validateEachObjectIn(
Chris@0 175 $value,
Chris@0 176 $this->defaultPropertyPath,
Chris@0 177 $groups,
Chris@0 178 $this->context
Chris@0 179 );
Chris@0 180
Chris@0 181 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
Chris@0 182 $this->context->setGroup($previousGroup);
Chris@0 183
Chris@0 184 return $this;
Chris@0 185 }
Chris@0 186
Chris@0 187 throw new RuntimeException(sprintf(
Chris@0 188 'Cannot validate values of type "%s" automatically. Please '.
Chris@0 189 'provide a constraint.',
Chris@0 190 gettype($value)
Chris@0 191 ));
Chris@0 192 }
Chris@0 193
Chris@0 194 /**
Chris@0 195 * {@inheritdoc}
Chris@0 196 */
Chris@0 197 public function validateProperty($object, $propertyName, $groups = null)
Chris@0 198 {
Chris@0 199 $classMetadata = $this->metadataFactory->getMetadataFor($object);
Chris@0 200
Chris@0 201 if (!$classMetadata instanceof ClassMetadataInterface) {
Chris@0 202 throw new ValidatorException(sprintf(
Chris@0 203 'The metadata factory should return instances of '.
Chris@0 204 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
Chris@0 205 'got: "%s".',
Chris@0 206 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
Chris@0 207 ));
Chris@0 208 }
Chris@0 209
Chris@0 210 $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
Chris@0 211 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
Chris@0 212 $cacheKey = spl_object_hash($object);
Chris@0 213 $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
Chris@0 214
Chris@0 215 $previousValue = $this->context->getValue();
Chris@0 216 $previousObject = $this->context->getObject();
Chris@0 217 $previousMetadata = $this->context->getMetadata();
Chris@0 218 $previousPath = $this->context->getPropertyPath();
Chris@0 219 $previousGroup = $this->context->getGroup();
Chris@0 220
Chris@0 221 foreach ($propertyMetadatas as $propertyMetadata) {
Chris@0 222 $propertyValue = $propertyMetadata->getPropertyValue($object);
Chris@0 223
Chris@0 224 $this->validateGenericNode(
Chris@0 225 $propertyValue,
Chris@0 226 $object,
Chris@0 227 $cacheKey.':'.get_class($object).':'.$propertyName,
Chris@0 228 $propertyMetadata,
Chris@0 229 $propertyPath,
Chris@0 230 $groups,
Chris@0 231 null,
Chris@0 232 TraversalStrategy::IMPLICIT,
Chris@0 233 $this->context
Chris@0 234 );
Chris@0 235 }
Chris@0 236
Chris@0 237 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
Chris@0 238 $this->context->setGroup($previousGroup);
Chris@0 239
Chris@0 240 return $this;
Chris@0 241 }
Chris@0 242
Chris@0 243 /**
Chris@0 244 * {@inheritdoc}
Chris@0 245 */
Chris@0 246 public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
Chris@0 247 {
Chris@0 248 $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
Chris@0 249
Chris@0 250 if (!$classMetadata instanceof ClassMetadataInterface) {
Chris@0 251 throw new ValidatorException(sprintf(
Chris@0 252 'The metadata factory should return instances of '.
Chris@0 253 '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
Chris@0 254 'got: "%s".',
Chris@0 255 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
Chris@0 256 ));
Chris@0 257 }
Chris@0 258
Chris@0 259 $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
Chris@0 260 $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
Chris@0 261
Chris@0 262 if (is_object($objectOrClass)) {
Chris@0 263 $object = $objectOrClass;
Chris@0 264 $cacheKey = spl_object_hash($objectOrClass);
Chris@0 265 $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
Chris@0 266 } else {
Chris@0 267 // $objectOrClass contains a class name
Chris@0 268 $object = null;
Chris@0 269 $cacheKey = null;
Chris@0 270 $propertyPath = $this->defaultPropertyPath;
Chris@0 271 }
Chris@0 272
Chris@0 273 $previousValue = $this->context->getValue();
Chris@0 274 $previousObject = $this->context->getObject();
Chris@0 275 $previousMetadata = $this->context->getMetadata();
Chris@0 276 $previousPath = $this->context->getPropertyPath();
Chris@0 277 $previousGroup = $this->context->getGroup();
Chris@0 278
Chris@0 279 foreach ($propertyMetadatas as $propertyMetadata) {
Chris@0 280 $this->validateGenericNode(
Chris@0 281 $value,
Chris@0 282 $object,
Chris@0 283 $cacheKey.':'.get_class($object).':'.$propertyName,
Chris@0 284 $propertyMetadata,
Chris@0 285 $propertyPath,
Chris@0 286 $groups,
Chris@0 287 null,
Chris@0 288 TraversalStrategy::IMPLICIT,
Chris@0 289 $this->context
Chris@0 290 );
Chris@0 291 }
Chris@0 292
Chris@0 293 $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
Chris@0 294 $this->context->setGroup($previousGroup);
Chris@0 295
Chris@0 296 return $this;
Chris@0 297 }
Chris@0 298
Chris@0 299 /**
Chris@0 300 * {@inheritdoc}
Chris@0 301 */
Chris@0 302 public function getViolations()
Chris@0 303 {
Chris@0 304 return $this->context->getViolations();
Chris@0 305 }
Chris@0 306
Chris@0 307 /**
Chris@0 308 * Normalizes the given group or list of groups to an array.
Chris@0 309 *
Chris@0 310 * @param mixed $groups The groups to normalize
Chris@0 311 *
Chris@0 312 * @return array A group array
Chris@0 313 */
Chris@0 314 protected function normalizeGroups($groups)
Chris@0 315 {
Chris@0 316 if (is_array($groups)) {
Chris@0 317 return $groups;
Chris@0 318 }
Chris@0 319
Chris@0 320 return array($groups);
Chris@0 321 }
Chris@0 322
Chris@0 323 /**
Chris@0 324 * Validates an object against the constraints defined for its class.
Chris@0 325 *
Chris@0 326 * If no metadata is available for the class, but the class is an instance
Chris@0 327 * of {@link \Traversable} and the selected traversal strategy allows
Chris@0 328 * traversal, the object will be iterated and each nested object will be
Chris@0 329 * validated instead.
Chris@0 330 *
Chris@0 331 * @param object $object The object to cascade
Chris@0 332 * @param string $propertyPath The current property path
Chris@0 333 * @param string[] $groups The validated groups
Chris@0 334 * @param int $traversalStrategy The strategy for traversing the
Chris@0 335 * cascaded object
Chris@0 336 * @param ExecutionContextInterface $context The current execution context
Chris@0 337 *
Chris@0 338 * @throws NoSuchMetadataException If the object has no associated metadata
Chris@0 339 * and does not implement {@link \Traversable}
Chris@0 340 * or if traversal is disabled via the
Chris@0 341 * $traversalStrategy argument
Chris@0 342 * @throws UnsupportedMetadataException If the metadata returned by the
Chris@0 343 * metadata factory does not implement
Chris@0 344 * {@link ClassMetadataInterface}
Chris@0 345 */
Chris@0 346 private function validateObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context)
Chris@0 347 {
Chris@0 348 try {
Chris@0 349 $classMetadata = $this->metadataFactory->getMetadataFor($object);
Chris@0 350
Chris@0 351 if (!$classMetadata instanceof ClassMetadataInterface) {
Chris@0 352 throw new UnsupportedMetadataException(sprintf(
Chris@0 353 'The metadata factory should return instances of '.
Chris@0 354 '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
Chris@0 355 'got: "%s".',
Chris@0 356 is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
Chris@0 357 ));
Chris@0 358 }
Chris@0 359
Chris@0 360 $this->validateClassNode(
Chris@0 361 $object,
Chris@0 362 spl_object_hash($object),
Chris@0 363 $classMetadata,
Chris@0 364 $propertyPath,
Chris@0 365 $groups,
Chris@0 366 null,
Chris@0 367 $traversalStrategy,
Chris@0 368 $context
Chris@0 369 );
Chris@0 370 } catch (NoSuchMetadataException $e) {
Chris@0 371 // Rethrow if not Traversable
Chris@0 372 if (!$object instanceof \Traversable) {
Chris@0 373 throw $e;
Chris@0 374 }
Chris@0 375
Chris@0 376 // Rethrow unless IMPLICIT or TRAVERSE
Chris@0 377 if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
Chris@0 378 throw $e;
Chris@0 379 }
Chris@0 380
Chris@0 381 $this->validateEachObjectIn(
Chris@0 382 $object,
Chris@0 383 $propertyPath,
Chris@0 384 $groups,
Chris@0 385 $context
Chris@0 386 );
Chris@0 387 }
Chris@0 388 }
Chris@0 389
Chris@0 390 /**
Chris@0 391 * Validates each object in a collection against the constraints defined
Chris@0 392 * for their classes.
Chris@0 393 *
Chris@0 394 * If the parameter $recursive is set to true, nested {@link \Traversable}
Chris@0 395 * objects are iterated as well. Nested arrays are always iterated,
Chris@0 396 * regardless of the value of $recursive.
Chris@0 397 *
Chris@0 398 * @param array|\Traversable $collection The collection
Chris@0 399 * @param string $propertyPath The current property path
Chris@0 400 * @param string[] $groups The validated groups
Chris@0 401 * @param ExecutionContextInterface $context The current execution context
Chris@0 402 *
Chris@0 403 * @see ClassNode
Chris@0 404 * @see CollectionNode
Chris@0 405 */
Chris@0 406 private function validateEachObjectIn($collection, $propertyPath, array $groups, ExecutionContextInterface $context)
Chris@0 407 {
Chris@0 408 foreach ($collection as $key => $value) {
Chris@0 409 if (is_array($value)) {
Chris@0 410 // Arrays are always cascaded, independent of the specified
Chris@0 411 // traversal strategy
Chris@0 412 $this->validateEachObjectIn(
Chris@0 413 $value,
Chris@0 414 $propertyPath.'['.$key.']',
Chris@0 415 $groups,
Chris@0 416 $context
Chris@0 417 );
Chris@0 418
Chris@0 419 continue;
Chris@0 420 }
Chris@0 421
Chris@0 422 // Scalar and null values in the collection are ignored
Chris@0 423 if (is_object($value)) {
Chris@0 424 $this->validateObject(
Chris@0 425 $value,
Chris@0 426 $propertyPath.'['.$key.']',
Chris@0 427 $groups,
Chris@0 428 TraversalStrategy::IMPLICIT,
Chris@0 429 $context
Chris@0 430 );
Chris@0 431 }
Chris@0 432 }
Chris@0 433 }
Chris@0 434
Chris@0 435 /**
Chris@0 436 * Validates a class node.
Chris@0 437 *
Chris@0 438 * A class node is a combination of an object with a {@link ClassMetadataInterface}
Chris@0 439 * instance. Each class node (conceptionally) has zero or more succeeding
Chris@0 440 * property nodes:
Chris@0 441 *
Chris@0 442 * (Article:class node)
Chris@0 443 * \
Chris@0 444 * ($title:property node)
Chris@0 445 *
Chris@0 446 * This method validates the passed objects against all constraints defined
Chris@0 447 * at class level. It furthermore triggers the validation of each of the
Chris@0 448 * class' properties against the constraints for that property.
Chris@0 449 *
Chris@0 450 * If the selected traversal strategy allows traversal, the object is
Chris@0 451 * iterated and each nested object is validated against its own constraints.
Chris@0 452 * The object is not traversed if traversal is disabled in the class
Chris@0 453 * metadata.
Chris@0 454 *
Chris@0 455 * If the passed groups contain the group "Default", the validator will
Chris@0 456 * check whether the "Default" group has been replaced by a group sequence
Chris@0 457 * in the class metadata. If this is the case, the group sequence is
Chris@0 458 * validated instead.
Chris@0 459 *
Chris@0 460 * @param object $object The validated object
Chris@0 461 * @param string $cacheKey The key for caching
Chris@0 462 * the validated object
Chris@0 463 * @param ClassMetadataInterface $metadata The class metadata of
Chris@0 464 * the object
Chris@0 465 * @param string $propertyPath The property path leading
Chris@0 466 * to the object
Chris@0 467 * @param string[] $groups The groups in which the
Chris@0 468 * object should be validated
Chris@0 469 * @param string[]|null $cascadedGroups The groups in which
Chris@0 470 * cascaded objects should
Chris@0 471 * be validated
Chris@0 472 * @param int $traversalStrategy The strategy used for
Chris@0 473 * traversing the object
Chris@0 474 * @param ExecutionContextInterface $context The current execution context
Chris@0 475 *
Chris@0 476 * @throws UnsupportedMetadataException If a property metadata does not
Chris@0 477 * implement {@link PropertyMetadataInterface}
Chris@0 478 * @throws ConstraintDefinitionException If traversal was enabled but the
Chris@0 479 * object does not implement
Chris@0 480 * {@link \Traversable}
Chris@0 481 *
Chris@0 482 * @see TraversalStrategy
Chris@0 483 */
Chris@0 484 private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
Chris@0 485 {
Chris@0 486 $context->setNode($object, $object, $metadata, $propertyPath);
Chris@0 487
Chris@0 488 if (!$context->isObjectInitialized($cacheKey)) {
Chris@0 489 foreach ($this->objectInitializers as $initializer) {
Chris@0 490 $initializer->initialize($object);
Chris@0 491 }
Chris@0 492
Chris@0 493 $context->markObjectAsInitialized($cacheKey);
Chris@0 494 }
Chris@0 495
Chris@0 496 foreach ($groups as $key => $group) {
Chris@0 497 // If the "Default" group is replaced by a group sequence, remember
Chris@0 498 // to cascade the "Default" group when traversing the group
Chris@0 499 // sequence
Chris@0 500 $defaultOverridden = false;
Chris@0 501
Chris@0 502 // Use the object hash for group sequences
Chris@0 503 $groupHash = is_object($group) ? spl_object_hash($group) : $group;
Chris@0 504
Chris@0 505 if ($context->isGroupValidated($cacheKey, $groupHash)) {
Chris@0 506 // Skip this group when validating the properties and when
Chris@0 507 // traversing the object
Chris@0 508 unset($groups[$key]);
Chris@0 509
Chris@0 510 continue;
Chris@0 511 }
Chris@0 512
Chris@0 513 $context->markGroupAsValidated($cacheKey, $groupHash);
Chris@0 514
Chris@0 515 // Replace the "Default" group by the group sequence defined
Chris@0 516 // for the class, if applicable.
Chris@0 517 // This is done after checking the cache, so that
Chris@0 518 // spl_object_hash() isn't called for this sequence and
Chris@0 519 // "Default" is used instead in the cache. This is useful
Chris@0 520 // if the getters below return different group sequences in
Chris@0 521 // every call.
Chris@0 522 if (Constraint::DEFAULT_GROUP === $group) {
Chris@0 523 if ($metadata->hasGroupSequence()) {
Chris@0 524 // The group sequence is statically defined for the class
Chris@0 525 $group = $metadata->getGroupSequence();
Chris@0 526 $defaultOverridden = true;
Chris@0 527 } elseif ($metadata->isGroupSequenceProvider()) {
Chris@0 528 // The group sequence is dynamically obtained from the validated
Chris@0 529 // object
Chris@0 530 /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
Chris@0 531 $group = $object->getGroupSequence();
Chris@0 532 $defaultOverridden = true;
Chris@0 533
Chris@0 534 if (!$group instanceof GroupSequence) {
Chris@0 535 $group = new GroupSequence($group);
Chris@0 536 }
Chris@0 537 }
Chris@0 538 }
Chris@0 539
Chris@0 540 // If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
Chris@0 541 // (=<G1,G2>), then call validateClassNode() with each entry of the
Chris@0 542 // group sequence and abort if necessary (G1, G2)
Chris@0 543 if ($group instanceof GroupSequence) {
Chris@0 544 $this->stepThroughGroupSequence(
Chris@0 545 $object,
Chris@0 546 $object,
Chris@0 547 $cacheKey,
Chris@0 548 $metadata,
Chris@0 549 $propertyPath,
Chris@0 550 $traversalStrategy,
Chris@0 551 $group,
Chris@0 552 $defaultOverridden ? Constraint::DEFAULT_GROUP : null,
Chris@0 553 $context
Chris@0 554 );
Chris@0 555
Chris@0 556 // Skip the group sequence when validating properties, because
Chris@0 557 // stepThroughGroupSequence() already validates the properties
Chris@0 558 unset($groups[$key]);
Chris@0 559
Chris@0 560 continue;
Chris@0 561 }
Chris@0 562
Chris@0 563 $this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
Chris@0 564 }
Chris@0 565
Chris@0 566 // If no more groups should be validated for the property nodes,
Chris@0 567 // we can safely quit
Chris@0 568 if (0 === count($groups)) {
Chris@0 569 return;
Chris@0 570 }
Chris@0 571
Chris@0 572 // Validate all properties against their constraints
Chris@0 573 foreach ($metadata->getConstrainedProperties() as $propertyName) {
Chris@0 574 // If constraints are defined both on the getter of a property as
Chris@0 575 // well as on the property itself, then getPropertyMetadata()
Chris@0 576 // returns two metadata objects, not just one
Chris@0 577 foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
Chris@0 578 if (!$propertyMetadata instanceof PropertyMetadataInterface) {
Chris@0 579 throw new UnsupportedMetadataException(sprintf(
Chris@0 580 'The property metadata instances should implement '.
Chris@0 581 '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '.
Chris@0 582 'got: "%s".',
Chris@0 583 is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata)
Chris@0 584 ));
Chris@0 585 }
Chris@0 586
Chris@0 587 $propertyValue = $propertyMetadata->getPropertyValue($object);
Chris@0 588
Chris@0 589 $this->validateGenericNode(
Chris@0 590 $propertyValue,
Chris@0 591 $object,
Chris@0 592 $cacheKey.':'.get_class($object).':'.$propertyName,
Chris@0 593 $propertyMetadata,
Chris@0 594 PropertyPath::append($propertyPath, $propertyName),
Chris@0 595 $groups,
Chris@0 596 $cascadedGroups,
Chris@0 597 TraversalStrategy::IMPLICIT,
Chris@0 598 $context
Chris@0 599 );
Chris@0 600 }
Chris@0 601 }
Chris@0 602
Chris@0 603 // If no specific traversal strategy was requested when this method
Chris@0 604 // was called, use the traversal strategy of the class' metadata
Chris@0 605 if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
Chris@0 606 $traversalStrategy = $metadata->getTraversalStrategy();
Chris@0 607 }
Chris@0 608
Chris@0 609 // Traverse only if IMPLICIT or TRAVERSE
Chris@0 610 if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
Chris@0 611 return;
Chris@0 612 }
Chris@0 613
Chris@0 614 // If IMPLICIT, stop unless we deal with a Traversable
Chris@0 615 if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
Chris@0 616 return;
Chris@0 617 }
Chris@0 618
Chris@0 619 // If TRAVERSE, fail if we have no Traversable
Chris@0 620 if (!$object instanceof \Traversable) {
Chris@0 621 throw new ConstraintDefinitionException(sprintf(
Chris@0 622 'Traversal was enabled for "%s", but this class '.
Chris@0 623 'does not implement "\Traversable".',
Chris@0 624 get_class($object)
Chris@0 625 ));
Chris@0 626 }
Chris@0 627
Chris@0 628 $this->validateEachObjectIn(
Chris@0 629 $object,
Chris@0 630 $propertyPath,
Chris@0 631 $groups,
Chris@0 632 $context
Chris@0 633 );
Chris@0 634 }
Chris@0 635
Chris@0 636 /**
Chris@0 637 * Validates a node that is not a class node.
Chris@0 638 *
Chris@0 639 * Currently, two such node types exist:
Chris@0 640 *
Chris@0 641 * - property nodes, which consist of the value of an object's
Chris@0 642 * property together with a {@link PropertyMetadataInterface} instance
Chris@0 643 * - generic nodes, which consist of a value and some arbitrary
Chris@0 644 * constraints defined in a {@link MetadataInterface} container
Chris@0 645 *
Chris@0 646 * In both cases, the value is validated against all constraints defined
Chris@0 647 * in the passed metadata object. Then, if the value is an instance of
Chris@0 648 * {@link \Traversable} and the selected traversal strategy permits it,
Chris@0 649 * the value is traversed and each nested object validated against its own
Chris@0 650 * constraints. Arrays are always traversed.
Chris@0 651 *
Chris@0 652 * @param mixed $value The validated value
Chris@0 653 * @param object|null $object The current object
Chris@0 654 * @param string $cacheKey The key for caching
Chris@0 655 * the validated value
Chris@0 656 * @param MetadataInterface $metadata The metadata of the
Chris@0 657 * value
Chris@0 658 * @param string $propertyPath The property path leading
Chris@0 659 * to the value
Chris@0 660 * @param string[] $groups The groups in which the
Chris@0 661 * value should be validated
Chris@0 662 * @param string[]|null $cascadedGroups The groups in which
Chris@0 663 * cascaded objects should
Chris@0 664 * be validated
Chris@0 665 * @param int $traversalStrategy The strategy used for
Chris@0 666 * traversing the value
Chris@0 667 * @param ExecutionContextInterface $context The current execution context
Chris@0 668 *
Chris@0 669 * @see TraversalStrategy
Chris@0 670 */
Chris@0 671 private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context)
Chris@0 672 {
Chris@0 673 $context->setNode($value, $object, $metadata, $propertyPath);
Chris@0 674
Chris@0 675 foreach ($groups as $key => $group) {
Chris@0 676 if ($group instanceof GroupSequence) {
Chris@0 677 $this->stepThroughGroupSequence(
Chris@0 678 $value,
Chris@0 679 $object,
Chris@0 680 $cacheKey,
Chris@0 681 $metadata,
Chris@0 682 $propertyPath,
Chris@0 683 $traversalStrategy,
Chris@0 684 $group,
Chris@0 685 null,
Chris@0 686 $context
Chris@0 687 );
Chris@0 688
Chris@0 689 // Skip the group sequence when cascading, as the cascading
Chris@0 690 // logic is already done in stepThroughGroupSequence()
Chris@0 691 unset($groups[$key]);
Chris@0 692
Chris@0 693 continue;
Chris@0 694 }
Chris@0 695
Chris@0 696 $this->validateInGroup($value, $cacheKey, $metadata, $group, $context);
Chris@0 697 }
Chris@0 698
Chris@0 699 if (0 === count($groups)) {
Chris@0 700 return;
Chris@0 701 }
Chris@0 702
Chris@0 703 if (null === $value) {
Chris@0 704 return;
Chris@0 705 }
Chris@0 706
Chris@0 707 $cascadingStrategy = $metadata->getCascadingStrategy();
Chris@0 708
Chris@0 709 // Quit unless we have an array or a cascaded object
Chris@0 710 if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) {
Chris@0 711 return;
Chris@0 712 }
Chris@0 713
Chris@0 714 // If no specific traversal strategy was requested when this method
Chris@0 715 // was called, use the traversal strategy of the node's metadata
Chris@0 716 if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
Chris@0 717 $traversalStrategy = $metadata->getTraversalStrategy();
Chris@0 718 }
Chris@0 719
Chris@0 720 // The $cascadedGroups property is set, if the "Default" group is
Chris@0 721 // overridden by a group sequence
Chris@0 722 // See validateClassNode()
Chris@0 723 $cascadedGroups = null !== $cascadedGroups && count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
Chris@0 724
Chris@0 725 if (is_array($value)) {
Chris@0 726 // Arrays are always traversed, independent of the specified
Chris@0 727 // traversal strategy
Chris@0 728 $this->validateEachObjectIn(
Chris@0 729 $value,
Chris@0 730 $propertyPath,
Chris@0 731 $cascadedGroups,
Chris@0 732 $context
Chris@0 733 );
Chris@0 734
Chris@0 735 return;
Chris@0 736 }
Chris@0 737
Chris@0 738 // If the value is a scalar, pass it anyway, because we want
Chris@0 739 // a NoSuchMetadataException to be thrown in that case
Chris@0 740 $this->validateObject(
Chris@0 741 $value,
Chris@0 742 $propertyPath,
Chris@0 743 $cascadedGroups,
Chris@0 744 $traversalStrategy,
Chris@0 745 $context
Chris@0 746 );
Chris@0 747
Chris@0 748 // Currently, the traversal strategy can only be TRAVERSE for a
Chris@0 749 // generic node if the cascading strategy is CASCADE. Thus, traversable
Chris@0 750 // objects will always be handled within validateObject() and there's
Chris@0 751 // nothing more to do here.
Chris@0 752
Chris@0 753 // see GenericMetadata::addConstraint()
Chris@0 754 }
Chris@0 755
Chris@0 756 /**
Chris@0 757 * Sequentially validates a node's value in each group of a group sequence.
Chris@0 758 *
Chris@0 759 * If any of the constraints generates a violation, subsequent groups in the
Chris@0 760 * group sequence are skipped.
Chris@0 761 *
Chris@0 762 * @param mixed $value The validated value
Chris@0 763 * @param object|null $object The current object
Chris@0 764 * @param string $cacheKey The key for caching
Chris@0 765 * the validated value
Chris@0 766 * @param MetadataInterface $metadata The metadata of the
Chris@0 767 * value
Chris@0 768 * @param string $propertyPath The property path leading
Chris@0 769 * to the value
Chris@0 770 * @param int $traversalStrategy The strategy used for
Chris@0 771 * traversing the value
Chris@0 772 * @param GroupSequence $groupSequence The group sequence
Chris@0 773 * @param string|null $cascadedGroup The group that should
Chris@0 774 * be passed to cascaded
Chris@0 775 * objects instead of
Chris@0 776 * the group sequence
Chris@0 777 * @param ExecutionContextInterface $context The execution context
Chris@0 778 */
Chris@0 779 private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
Chris@0 780 {
Chris@0 781 $violationCount = count($context->getViolations());
Chris@0 782 $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null;
Chris@0 783
Chris@0 784 foreach ($groupSequence->groups as $groupInSequence) {
Chris@0 785 $groups = (array) $groupInSequence;
Chris@0 786
Chris@0 787 if ($metadata instanceof ClassMetadataInterface) {
Chris@0 788 $this->validateClassNode(
Chris@0 789 $value,
Chris@0 790 $cacheKey,
Chris@0 791 $metadata,
Chris@0 792 $propertyPath,
Chris@0 793 $groups,
Chris@0 794 $cascadedGroups,
Chris@0 795 $traversalStrategy,
Chris@0 796 $context
Chris@0 797 );
Chris@0 798 } else {
Chris@0 799 $this->validateGenericNode(
Chris@0 800 $value,
Chris@0 801 $object,
Chris@0 802 $cacheKey,
Chris@0 803 $metadata,
Chris@0 804 $propertyPath,
Chris@0 805 $groups,
Chris@0 806 $cascadedGroups,
Chris@0 807 $traversalStrategy,
Chris@0 808 $context
Chris@0 809 );
Chris@0 810 }
Chris@0 811
Chris@0 812 // Abort sequence validation if a violation was generated
Chris@0 813 if (count($context->getViolations()) > $violationCount) {
Chris@0 814 break;
Chris@0 815 }
Chris@0 816 }
Chris@0 817 }
Chris@0 818
Chris@0 819 /**
Chris@0 820 * Validates a node's value against all constraints in the given group.
Chris@0 821 *
Chris@0 822 * @param mixed $value The validated value
Chris@0 823 * @param string $cacheKey The key for caching the
Chris@0 824 * validated value
Chris@0 825 * @param MetadataInterface $metadata The metadata of the value
Chris@0 826 * @param string $group The group to validate
Chris@0 827 * @param ExecutionContextInterface $context The execution context
Chris@0 828 */
Chris@0 829 private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context)
Chris@0 830 {
Chris@0 831 $context->setGroup($group);
Chris@0 832
Chris@0 833 foreach ($metadata->findConstraints($group) as $constraint) {
Chris@0 834 // Prevent duplicate validation of constraints, in the case
Chris@0 835 // that constraints belong to multiple validated groups
Chris@0 836 if (null !== $cacheKey) {
Chris@0 837 $constraintHash = spl_object_hash($constraint);
Chris@0 838
Chris@0 839 if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
Chris@0 840 continue;
Chris@0 841 }
Chris@0 842
Chris@0 843 $context->markConstraintAsValidated($cacheKey, $constraintHash);
Chris@0 844 }
Chris@0 845
Chris@0 846 $context->setConstraint($constraint);
Chris@0 847
Chris@0 848 $validator = $this->validatorFactory->getInstance($constraint);
Chris@0 849 $validator->initialize($context);
Chris@0 850 $validator->validate($value, $constraint);
Chris@0 851 }
Chris@0 852 }
Chris@0 853 }