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 }
|