vendor/symfony/form/Extension/Validator/Constraints/FormValidator.php line 32

  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.     /**
  24.      * @var \SplObjectStorage<FormInterface, array<int, string|string[]|GroupSequence>>
  25.      */
  26.     private \SplObjectStorage $resolvedGroups;
  27.     public function validate(mixed $formConstraint $formConstraint)
  28.     {
  29.         if (!$formConstraint instanceof Form) {
  30.             throw new UnexpectedTypeException($formConstraintForm::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.             $hasChildren $form->count() > 0;
  54.             if ($hasChildren && $form->isRoot()) {
  55.                 $this->resolvedGroups = new \SplObjectStorage();
  56.             }
  57.             if ($groups instanceof GroupSequence) {
  58.                 // Validate the data, the form AND nested fields in sequence
  59.                 $violationsCount $this->context->getViolations()->count();
  60.                 foreach ($groups->groups as $group) {
  61.                     if ($validateDataGraph) {
  62.                         $validator->atPath('data')->validate($datanull$group);
  63.                     }
  64.                     if ($groupedConstraints self::getConstraintsInGroups($constraints$group)) {
  65.                         $validator->atPath('data')->validate($data$groupedConstraints$group);
  66.                     }
  67.                     foreach ($form->all() as $field) {
  68.                         if ($field->isSubmitted()) {
  69.                             // remember to validate this field in one group only
  70.                             // otherwise resolving the groups would reuse the same
  71.                             // sequence recursively, thus some fields could fail
  72.                             // in different steps without breaking early enough
  73.                             $this->resolvedGroups[$field] = (array) $group;
  74.                             $fieldFormConstraint = new Form();
  75.                             $fieldFormConstraint->groups $group;
  76.                             $this->context->setNode($this->context->getValue(), $field$this->context->getMetadata(), $this->context->getPropertyPath());
  77.                             $validator->atPath(sprintf('children[%s]'$field->getName()))->validate($field$fieldFormConstraint$group);
  78.                         }
  79.                     }
  80.                     if ($violationsCount $this->context->getViolations()->count()) {
  81.                         break;
  82.                     }
  83.                 }
  84.             } else {
  85.                 if ($validateDataGraph) {
  86.                     $validator->atPath('data')->validate($datanull$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.                         if (\is_object($data) || \is_array($data)) {
  93.                             $validator->atPath('data')->validate($data$constraint$groups);
  94.                         }
  95.                         continue;
  96.                     }
  97.                     // Otherwise validate a constraint only once for the first
  98.                     // matching group
  99.                     foreach ($groups as $group) {
  100.                         if (\in_array($group$constraint->groups)) {
  101.                             $groupedConstraints[$group][] = $constraint;
  102.                             // Prevent duplicate validation
  103.                             if (!$constraint instanceof Composite) {
  104.                                 continue 2;
  105.                             }
  106.                         }
  107.                     }
  108.                 }
  109.                 foreach ($groupedConstraints as $group => $constraint) {
  110.                     $validator->atPath('data')->validate($data$constraint$group);
  111.                 }
  112.                 foreach ($form->all() as $field) {
  113.                     if ($field->isSubmitted()) {
  114.                         $this->resolvedGroups[$field] = $groups;
  115.                         $this->context->setNode($this->context->getValue(), $field$this->context->getMetadata(), $this->context->getPropertyPath());
  116.                         $validator->atPath(sprintf('children[%s]'$field->getName()))->validate($field$formConstraint);
  117.                     }
  118.                 }
  119.             }
  120.             if ($hasChildren && $form->isRoot()) {
  121.                 // destroy storage to avoid memory leaks
  122.                 $this->resolvedGroups = new \SplObjectStorage();
  123.             }
  124.         } elseif (!$form->isSynchronized()) {
  125.             $childrenSynchronized true;
  126.             /** @var FormInterface $child */
  127.             foreach ($form as $child) {
  128.                 if (!$child->isSynchronized()) {
  129.                     $childrenSynchronized false;
  130.                     $this->context->setNode($this->context->getValue(), $child$this->context->getMetadata(), $this->context->getPropertyPath());
  131.                     $validator->atPath(sprintf('children[%s]'$child->getName()))->validate($child$formConstraint);
  132.                 }
  133.             }
  134.             // Mark the form with an error if it is not synchronized BUT all
  135.             // of its children are synchronized. If any child is not
  136.             // synchronized, an error is displayed there already and showing
  137.             // a second error in its parent form is pointless, or worse, may
  138.             // lead to duplicate errors if error bubbling is enabled on the
  139.             // child.
  140.             // See also https://github.com/symfony/symfony/issues/4359
  141.             if ($childrenSynchronized) {
  142.                 $clientDataAsString \is_scalar($form->getViewData())
  143.                     ? (string) $form->getViewData()
  144.                     : get_debug_type($form->getViewData());
  145.                 $failure $form->getTransformationFailure();
  146.                 $this->context->setConstraint($formConstraint);
  147.                 $this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message'))
  148.                     ->setParameters(array_replace(
  149.                         ['{{ value }}' => $clientDataAsString],
  150.                         $config->getOption('invalid_message_parameters'),
  151.                         $failure->getInvalidMessageParameters()
  152.                     ))
  153.                     ->setInvalidValue($form->getViewData())
  154.                     ->setCode(Form::NOT_SYNCHRONIZED_ERROR)
  155.                     ->setCause($failure)
  156.                     ->addViolation();
  157.             }
  158.         }
  159.         // Mark the form with an error if it contains extra fields
  160.         if (!$config->getOption('allow_extra_fields') && \count($form->getExtraData()) > 0) {
  161.             $this->context->setConstraint($formConstraint);
  162.             $this->context->buildViolation($config->getOption('extra_fields_message'''))
  163.                 ->setParameter('{{ extra_fields }}''"'.implode('", "'array_keys($form->getExtraData())).'"')
  164.                 ->setPlural(\count($form->getExtraData()))
  165.                 ->setInvalidValue($form->getExtraData())
  166.                 ->setCode(Form::NO_SUCH_FIELD_ERROR)
  167.                 ->addViolation();
  168.         }
  169.     }
  170.     /**
  171.      * Returns the validation groups of the given form.
  172.      *
  173.      * @return string|GroupSequence|array<string|GroupSequence>
  174.      */
  175.     private function getValidationGroups(FormInterface $form): string|GroupSequence|array
  176.     {
  177.         // Determine the clicked button of the complete form tree
  178.         $clickedButton null;
  179.         if (method_exists($form'getClickedButton')) {
  180.             $clickedButton $form->getClickedButton();
  181.         }
  182.         if (null !== $clickedButton) {
  183.             $groups $clickedButton->getConfig()->getOption('validation_groups');
  184.             if (null !== $groups) {
  185.                 return self::resolveValidationGroups($groups$form);
  186.             }
  187.         }
  188.         do {
  189.             $groups $form->getConfig()->getOption('validation_groups');
  190.             if (null !== $groups) {
  191.                 return self::resolveValidationGroups($groups$form);
  192.             }
  193.             if (isset($this->resolvedGroups[$form])) {
  194.                 return $this->resolvedGroups[$form];
  195.             }
  196.             $form $form->getParent();
  197.         } while (null !== $form);
  198.         return [Constraint::DEFAULT_GROUP];
  199.     }
  200.     /**
  201.      * Post-processes the validation groups option for a given form.
  202.      *
  203.      * @param string|GroupSequence|array<string|GroupSequence>|callable $groups The validation groups
  204.      *
  205.      * @return GroupSequence|array<string|GroupSequence>
  206.      */
  207.     private static function resolveValidationGroups(string|GroupSequence|array|callable $groupsFormInterface $form): GroupSequence|array
  208.     {
  209.         if (!\is_string($groups) && \is_callable($groups)) {
  210.             $groups $groups($form);
  211.         }
  212.         if ($groups instanceof GroupSequence) {
  213.             return $groups;
  214.         }
  215.         return (array) $groups;
  216.     }
  217.     private static function getConstraintsInGroups(array $constraintsstring|array $group): array
  218.     {
  219.         $groups = (array) $group;
  220.         return array_filter($constraints, static function (Constraint $constraint) use ($groups) {
  221.             foreach ($groups as $group) {
  222.                 if (\in_array($group$constraint->groupstrue)) {
  223.                     return true;
  224.                 }
  225.             }
  226.             return false;
  227.         });
  228.     }
  229. }