RecursiveContextualValidator.php 34 KB

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