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