RecursiveContextualValidator.php 29 KB

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