From 6be9d50c227f631acf7cbb29656e1160a9370944 Mon Sep 17 00:00:00 2001 From: Ivan Grigoriev Date: Sat, 1 Feb 2020 23:12:46 +0300 Subject: [PATCH] filter out excess violations while validating form field constraints with sequence of groups --- .../Validator/Constraints/FormValidator.php | 49 +++++++ .../Type/FormTypeValidatorExtensionTest.php | 127 ++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 07ecabdad134..8b84cd7740f6 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -65,6 +65,9 @@ public function validate($form, Constraint $formConstraint) if ($groups instanceof GroupSequence) { $validator->atPath('data')->validate($form->getData(), $constraints, $groups); + + $this->filterFormFieldsGroupSequenceViolations($groups); + // Otherwise validate a constraint only once for the first // matching group foreach ($groups as $group) { @@ -142,6 +145,52 @@ public function validate($form, Constraint $formConstraint) } } + /** + * Filter out form field violations to meet the requirements of the sequence of groups. + * + * If there is a violation with a group of current groups of the sequence, + * remove all other violations that don't belong this groups. + * + * This is necessary because each form field is validated independently. + */ + private function filterFormFieldsGroupSequenceViolations(GroupSequence $groupSequence) + { + if (\count($violations = $this->context->getViolations()) < 2) { + return; + } + + $violationGroups = []; + + foreach ($violations as $offset => $violation) { + $violationGroups[$offset] = array_map(static function ($group) { + return $group; + }, $violation->getConstraint()->groups); + } + + $groupsToKeep = []; + + foreach ($groupSequence->groups as $seqGroups) { + $seqGroups = !\is_array($seqGroups) ? [$seqGroups] : $seqGroups; + + if (array_filter($violationGroups, static function ($groups) use ($seqGroups) { + return array_filter($groups, static function ($group) use ($seqGroups) { + return \in_array($group, $seqGroups, true); + }); + })) { + $groupsToKeep = $seqGroups; + break; + } + } + + foreach ($violationGroups as $offset => $groups) { + if (!array_filter($groups, static function ($group) use ($groupsToKeep) { + return \in_array($group, $groupsToKeep, true); + })) { + $violations->remove($offset); + } + } + } + /** * Returns the validation groups of the given form. * diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 57f92b6574e3..1097f95500bc 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validation; @@ -74,6 +75,132 @@ public function testGroupSequenceWithConstraintsOption() $this->assertCount(1, $form->getErrors(true)); } + /** + * @dataProvider provideGroupsSequenceAndResultData + */ + public function testGroupSequenceWithConstraintsOptionMatrix( + array $groups, + array $sequence, + $errorCount, + array $propertyPaths + ) { + $form = Forms::createFormFactoryBuilder() + ->addExtension(new ValidatorExtension(Validation::createValidator())) + ->getFormFactory() + ->create(FormTypeTest::TESTED_TYPE, null, ([ + 'validation_groups' => new GroupSequence($sequence), + ])); + + $data = []; + foreach ($groups as $fieldName => $fieldGroups) { + $form = $form->add( + $fieldName, TextTypeTest:: + TESTED_TYPE, + [ + 'constraints' => [new NotBlank(['groups' => $fieldGroups])], + ]); + + $data[$fieldName] = ''; + } + + $form->submit($data); + + $errors = $form->getErrors(true); + $this->assertCount($errorCount, $form->getErrors(true)); + + foreach ($errors as $i => $error) { + $this->assertEquals('children['.$propertyPaths[$i].'].data', $error->getCause()->getPropertyPath()); + } + } + + public function provideGroupsSequenceAndResultData() + { + return [ + // two fields (sequence of groups and group order): + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + ], + 'sequence' => ['First'], + 'errors' => 1, + 'propertyPaths' => ['field1'], + ], + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + ], + 'sequence' => ['Second'], + 'errors' => 1, + 'propertyPaths' => ['field2'], + ], + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + ], + 'sequence' => ['First', 'Second'], + 'errors' => 1, + 'propertyPaths' => ['field1'], + ], + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + ], + 'sequence' => ['Second', 'First'], + 'errors' => 1, + 'propertyPaths' => ['field2'], + ], + + // two fields (field with sequence of groups) + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second', 'First'], + ], + 'sequence' => ['First'], + 'errors' => 2, + 'propertyPaths' => ['field1', 'field2'], + ], + + // three fields (sequence with multigroup) + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + 'field3' => ['Third'], + ], + 'sequence' => [['First', 'Second'], 'Third'], + 'errors' => 2, + 'propertyPaths' => ['field1', 'field2'], + ], + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + 'field3' => ['Third'], + ], + 'sequence' => ['First', ['Second', 'Third']], + 'errors' => 1, + 'propertyPaths' => ['field1'], + ], + + // three fields (field with sequence of groups) + [ + 'groups' => [ + 'field1' => ['First'], + 'field2' => ['Second'], + 'field3' => ['Third', 'Second'], + ], + 'sequence' => [['First', 'Second'], 'Third'], + 'errors' => 3, + 'propertyPaths' => ['field1', 'field2', 'field3'], + ], + ]; + } + protected function createForm(array $options = []) { return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options);