PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php

http://github.com/symfony/symfony
PHP | 251 lines | 158 code | 42 blank | 51 comment | 35 complexity | 0618250561b9d42f99d7291664b8ff84 MD5 | raw file
  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\Form\Extension\Validator\Constraints;
  11. use Symfony\Component\Form\FormInterface;
  12. use Symfony\Component\Validator\Constraint;
  13. use Symfony\Component\Validator\Constraints\Composite;
  14. use Symfony\Component\Validator\Constraints\GroupSequence;
  15. use Symfony\Component\Validator\Constraints\Valid;
  16. use Symfony\Component\Validator\ConstraintValidator;
  17. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  18. /**
  19. * @author Bernhard Schussek <bschussek@gmail.com>
  20. */
  21. class FormValidator extends ConstraintValidator
  22. {
  23. private $resolvedGroups;
  24. /**
  25. * {@inheritdoc}
  26. */
  27. public function validate($form, Constraint $formConstraint)
  28. {
  29. if (!$formConstraint instanceof Form) {
  30. throw new UnexpectedTypeException($formConstraint, Form::class);
  31. }
  32. if (!$form instanceof FormInterface) {
  33. return;
  34. }
  35. /* @var FormInterface $form */
  36. $config = $form->getConfig();
  37. $validator = $this->context->getValidator()->inContext($this->context);
  38. if ($form->isSubmitted() && $form->isSynchronized()) {
  39. // Validate the form data only if transformation succeeded
  40. $groups = $this->getValidationGroups($form);
  41. if (!$groups) {
  42. return;
  43. }
  44. $data = $form->getData();
  45. // Validate the data against its own constraints
  46. $validateDataGraph = $form->isRoot()
  47. && (\is_object($data) || \is_array($data))
  48. && (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups))
  49. ;
  50. // Validate the data against the constraints defined in the form
  51. /** @var Constraint[] $constraints */
  52. $constraints = $config->getOption('constraints', []);
  53. if ($groups instanceof GroupSequence) {
  54. // Validate the data, the form AND nested fields in sequence
  55. $violationsCount = $this->context->getViolations()->count();
  56. $fieldPropertyPath = \is_object($data) ? 'children[%s]' : 'children%s';
  57. $hasChildren = $form->count() > 0;
  58. $this->resolvedGroups = $hasChildren ? new \SplObjectStorage() : null;
  59. foreach ($groups->groups as $group) {
  60. if ($validateDataGraph) {
  61. $validator->atPath('data')->validate($data, null, $group);
  62. }
  63. if ($groupedConstraints = self::getConstraintsInGroups($constraints, $group)) {
  64. $validator->atPath('data')->validate($data, $groupedConstraints, $group);
  65. }
  66. foreach ($form->all() as $field) {
  67. if ($field->isSubmitted()) {
  68. // remember to validate this field is one group only
  69. // otherwise resolving the groups would reuse the same
  70. // sequence recursively, thus some fields could fail
  71. // in different steps without breaking early enough
  72. $this->resolvedGroups[$field] = (array) $group;
  73. $validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $formConstraint);
  74. }
  75. }
  76. if ($violationsCount < $this->context->getViolations()->count()) {
  77. break;
  78. }
  79. }
  80. if ($hasChildren) {
  81. // destroy storage at the end of the sequence to avoid memory leaks
  82. $this->resolvedGroups = null;
  83. }
  84. } else {
  85. if ($validateDataGraph) {
  86. $validator->atPath('data')->validate($data, null, $groups);
  87. }
  88. $groupedConstraints = [];
  89. foreach ($constraints as $constraint) {
  90. // For the "Valid" constraint, validate the data in all groups
  91. if ($constraint instanceof Valid) {
  92. $validator->atPath('data')->validate($data, $constraint, $groups);
  93. continue;
  94. }
  95. // Otherwise validate a constraint only once for the first
  96. // matching group
  97. foreach ($groups as $group) {
  98. if (\in_array($group, $constraint->groups)) {
  99. $groupedConstraints[$group][] = $constraint;
  100. // Prevent duplicate validation
  101. if (!$constraint instanceof Composite) {
  102. continue 2;
  103. }
  104. }
  105. }
  106. }
  107. foreach ($groupedConstraints as $group => $constraint) {
  108. $validator->atPath('data')->validate($data, $constraint, $group);
  109. }
  110. }
  111. } elseif (!$form->isSynchronized()) {
  112. $childrenSynchronized = true;
  113. /** @var FormInterface $child */
  114. foreach ($form as $child) {
  115. if (!$child->isSynchronized()) {
  116. $childrenSynchronized = false;
  117. break;
  118. }
  119. }
  120. // Mark the form with an error if it is not synchronized BUT all
  121. // of its children are synchronized. If any child is not
  122. // synchronized, an error is displayed there already and showing
  123. // a second error in its parent form is pointless, or worse, may
  124. // lead to duplicate errors if error bubbling is enabled on the
  125. // child.
  126. // See also https://github.com/symfony/symfony/issues/4359
  127. if ($childrenSynchronized) {
  128. $clientDataAsString = is_scalar($form->getViewData())
  129. ? (string) $form->getViewData()
  130. : get_debug_type($form->getViewData());
  131. $failure = $form->getTransformationFailure();
  132. $this->context->setConstraint($formConstraint);
  133. $this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message'))
  134. ->setParameters(array_replace(
  135. ['{{ value }}' => $clientDataAsString],
  136. $config->getOption('invalid_message_parameters'),
  137. $failure->getInvalidMessageParameters()
  138. ))
  139. ->setInvalidValue($form->getViewData())
  140. ->setCode(Form::NOT_SYNCHRONIZED_ERROR)
  141. ->setCause($failure)
  142. ->addViolation();
  143. }
  144. }
  145. // Mark the form with an error if it contains extra fields
  146. if (!$config->getOption('allow_extra_fields') && \count($form->getExtraData()) > 0) {
  147. $this->context->setConstraint($formConstraint);
  148. $this->context->buildViolation($config->getOption('extra_fields_message', ''))
  149. ->setParameter('{{ extra_fields }}', '"'.implode('", "', array_keys($form->getExtraData())).'"')
  150. ->setPlural(\count($form->getExtraData()))
  151. ->setInvalidValue($form->getExtraData())
  152. ->setCode(Form::NO_SUCH_FIELD_ERROR)
  153. ->addViolation();
  154. }
  155. }
  156. /**
  157. * Returns the validation groups of the given form.
  158. *
  159. * @return string|GroupSequence|(string|GroupSequence)[] The validation groups
  160. */
  161. private function getValidationGroups(FormInterface $form)
  162. {
  163. // Determine the clicked button of the complete form tree
  164. $clickedButton = null;
  165. if (method_exists($form, 'getClickedButton')) {
  166. $clickedButton = $form->getClickedButton();
  167. }
  168. if (null !== $clickedButton) {
  169. $groups = $clickedButton->getConfig()->getOption('validation_groups');
  170. if (null !== $groups) {
  171. return self::resolveValidationGroups($groups, $form);
  172. }
  173. }
  174. do {
  175. $groups = $form->getConfig()->getOption('validation_groups');
  176. if (null !== $groups) {
  177. return self::resolveValidationGroups($groups, $form);
  178. }
  179. if (isset($this->resolvedGroups[$form])) {
  180. return $this->resolvedGroups[$form];
  181. }
  182. $form = $form->getParent();
  183. } while (null !== $form);
  184. return [Constraint::DEFAULT_GROUP];
  185. }
  186. /**
  187. * Post-processes the validation groups option for a given form.
  188. *
  189. * @param string|GroupSequence|(string|GroupSequence)[]|callable $groups The validation groups
  190. *
  191. * @return GroupSequence|(string|GroupSequence)[] The validation groups
  192. */
  193. private static function resolveValidationGroups($groups, FormInterface $form)
  194. {
  195. if (!\is_string($groups) && \is_callable($groups)) {
  196. $groups = $groups($form);
  197. }
  198. if ($groups instanceof GroupSequence) {
  199. return $groups;
  200. }
  201. return (array) $groups;
  202. }
  203. private static function getConstraintsInGroups($constraints, $group)
  204. {
  205. return array_filter($constraints, static function (Constraint $constraint) use ($group) {
  206. return \in_array($group, $constraint->groups, true);
  207. });
  208. }
  209. }