Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 98743fc

Browse files
committed
[Form] Fixed handling groups sequence validation
1 parent 9b41a32 commit 98743fc

File tree

5 files changed

+137
-23
lines changed

5 files changed

+137
-23
lines changed

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

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
*/
2525
class FormValidator extends ConstraintValidator
2626
{
27+
private $resolvedGroups;
28+
2729
/**
2830
* {@inheritdoc}
2931
*/
@@ -44,7 +46,7 @@ public function validate($form, Constraint $formConstraint)
4446

4547
if ($form->isSubmitted() && $form->isSynchronized()) {
4648
// Validate the form data only if transformation succeeded
47-
$groups = self::getValidationGroups($form);
49+
$groups = $this->getValidationGroups($form);
4850

4951
if (!$groups) {
5052
return;
@@ -55,31 +57,49 @@ public function validate($form, Constraint $formConstraint)
5557
// Validate the data against its own constraints
5658
if ($form->isRoot() && (\is_object($data) || \is_array($data))) {
5759
if (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups)) {
58-
$validator->atPath('data')->validate($form->getData(), null, $groups);
60+
$validator->atPath('data')->validate($data, null, $groups);
5961
}
6062
}
6163

62-
// Validate the data against the constraints defined
63-
// in the form
64+
// Validate the data against the constraints defined in the form
65+
/** @var Constraint[] $constraints */
6466
$constraints = $config->getOption('constraints', []);
6567

6668
if ($groups instanceof GroupSequence) {
67-
$validator->atPath('data')->validate($form->getData(), $constraints, $groups);
68-
// Otherwise validate a constraint only once for the first
69-
// matching group
70-
foreach ($groups as $group) {
71-
if (\in_array($group, $formConstraint->groups)) {
72-
$validator->atPath('data')->validate($form->getData(), $formConstraint, $group);
73-
if (\count($this->context->getViolations()) > 0) {
74-
break;
69+
// Validate the form AND nested fields in sequence
70+
$violationsCount = \count($this->context->getViolations());
71+
$fieldPropertyPath = \is_object($data) ? 'data.%s' : '%s';
72+
$hasChildren = \count($form) > 0;
73+
$this->resolvedGroups = $hasChildren ? new \SplObjectStorage() : null;
74+
75+
foreach ($groups->groups as $group) {
76+
$validator->atPath('data')->validate($data, $constraints, $group);
77+
78+
foreach ($form->all() as $field) {
79+
if ($field->isSubmitted()) {
80+
// remember to validate this field is one group only
81+
// otherwise resolving the groups would reuse the same
82+
// sequence recursively, thus some fields could fail
83+
// in different steps without breaking early enough
84+
$this->resolvedGroups[$field] = [$group];
85+
$validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $formConstraint);
7586
}
7687
}
88+
89+
if ($violationsCount < \count($this->context->getViolations())) {
90+
break;
91+
}
92+
}
93+
94+
if ($hasChildren) {
95+
// destroy storage at the end of the sequence to avoid memory leaks
96+
$this->resolvedGroups = null;
7797
}
7898
} else {
7999
foreach ($constraints as $constraint) {
80100
// For the "Valid" constraint, validate the data in all groups
81101
if ($constraint instanceof Valid) {
82-
$validator->atPath('data')->validate($form->getData(), $constraint, $groups);
102+
$validator->atPath('data')->validate($data, $constraint, $groups);
83103

84104
continue;
85105
}
@@ -88,7 +108,7 @@ public function validate($form, Constraint $formConstraint)
88108
// matching group
89109
foreach ($groups as $group) {
90110
if (\in_array($group, $constraint->groups)) {
91-
$validator->atPath('data')->validate($form->getData(), $constraint, $group);
111+
$validator->atPath('data')->validate($data, $constraint, $group);
92112

93113
// Prevent duplicate validation
94114
if (!$constraint instanceof Composite) {
@@ -147,7 +167,7 @@ public function validate($form, Constraint $formConstraint)
147167
*
148168
* @return string|GroupSequence|(string|GroupSequence)[] The validation groups
149169
*/
150-
private static function getValidationGroups(FormInterface $form)
170+
private function getValidationGroups(FormInterface $form)
151171
{
152172
// Determine the clicked button of the complete form tree
153173
$clickedButton = null;
@@ -171,6 +191,10 @@ private static function getValidationGroups(FormInterface $form)
171191
return self::resolveValidationGroups($groups, $form);
172192
}
173193

194+
if (isset($this->resolvedGroups[$form])) {
195+
return $this->resolvedGroups[$form];
196+
}
197+
174198
$form = $form->getParent();
175199
} while (null !== $form);
176200

src/Symfony/Component/Form/Resources/config/validation.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<class name="Symfony\Component\Form\Form">
88
<constraint name="Symfony\Component\Form\Extension\Validator\Constraints\Form" />
99
<property name="children">
10-
<constraint name="Valid" />
10+
<constraint name="Valid" />
1111
</property>
1212
</class>
1313
</constraint-mapping>

src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,8 @@ public function testHandleGroupSequenceValidationGroups()
402402
$form->submit([]);
403403

404404
$this->expectValidateAt(0, 'data', $object, new GroupSequence(['group1', 'group2']));
405-
$this->expectValidateAt(1, 'data', $object, new GroupSequence(['group1', 'group2']));
405+
$this->expectValidateAt(1, 'data', $object, 'group1');
406+
$this->expectValidateAt(2, 'data', $object, 'group2');
406407

407408
$this->validator->validate($form, new Form());
408409

@@ -756,6 +757,39 @@ public function testCompositeConstraintValidatedInEachGroup()
756757
$this->assertSame('data[field2]', $context->getViolations()[1]->getPropertyPath());
757758
}
758759

760+
public function testCompositeConstraintValidatedInSequence()
761+
{
762+
$form = $this->getCompoundForm([], [
763+
'constraints' => [
764+
new Collection([
765+
'field1' => new NotBlank([
766+
'groups' => ['field1'],
767+
]),
768+
'field2' => new NotBlank([
769+
'groups' => ['field2'],
770+
]),
771+
]),
772+
],
773+
'validation_groups' => new GroupSequence(['field1', 'field2']),
774+
])
775+
->add($this->getForm('field1'))
776+
->add($this->getForm('field2'))
777+
;
778+
779+
$form->submit([
780+
'field1' => '',
781+
'field2' => '',
782+
]);
783+
784+
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
785+
$this->validator->initialize($context);
786+
$this->validator->validate($form, new Form());
787+
788+
$this->assertCount(1, $context->getViolations());
789+
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
790+
$this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath());
791+
}
792+
759793
protected function createValidator()
760794
{
761795
return new FormValidator();
@@ -784,7 +818,7 @@ private function getForm($name = 'name', $dataClass = null, array $options = [])
784818

785819
private function getCompoundForm($data, array $options = [])
786820
{
787-
return $this->getBuilder('name', \get_class($data), $options)
821+
return $this->getBuilder('name', \is_object($data) ? \get_class($data) : null, $options)
788822
->setData($data)
789823
->setCompound(true)
790824
->setDataMapper(new PropertyPathMapper())

src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
1717
use Symfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest;
1818
use Symfony\Component\Form\Tests\Extension\Core\Type\TextTypeTest;
19-
use Symfony\Component\Validator\Constraints\Email;
2019
use Symfony\Component\Validator\Constraints\GroupSequence;
2120
use Symfony\Component\Validator\Constraints\Length;
21+
use Symfony\Component\Validator\Constraints\NotBlank;
2222
use Symfony\Component\Validator\Constraints\Valid;
2323
use Symfony\Component\Validator\ConstraintViolationList;
2424
use Symfony\Component\Validator\Validation;
@@ -64,14 +64,43 @@ public function testGroupSequenceWithConstraintsOption()
6464
->add('field', TextTypeTest::TESTED_TYPE, [
6565
'constraints' => [
6666
new Length(['min' => 10, 'groups' => ['First']]),
67-
new Email(['groups' => ['Second']]),
67+
new NotBlank(['groups' => ['Second']]),
6868
],
6969
])
7070
;
7171

7272
$form->submit(['field' => 'wrong']);
7373

74-
$this->assertCount(1, $form->getErrors(true));
74+
$errors = $form->getErrors(true);
75+
76+
$this->assertCount(1, $errors);
77+
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
78+
}
79+
80+
public function testManyFieldsGroupSequenceWithConstraintsOption()
81+
{
82+
$form = Forms::createFormFactoryBuilder()
83+
->addExtension(new ValidatorExtension(Validation::createValidator()))
84+
->getFormFactory()
85+
->create(FormTypeTest::TESTED_TYPE, null, (['validation_groups' => new GroupSequence(['First', 'Second'])]))
86+
->add('field1', TextTypeTest::TESTED_TYPE, [
87+
'constraints' => [
88+
new Length(['min' => 10, 'groups' => ['First']]),
89+
],
90+
])
91+
->add('field2', TextTypeTest::TESTED_TYPE, [
92+
'constraints' => [
93+
new NotBlank(['groups' => ['Second']]),
94+
],
95+
])
96+
;
97+
98+
$form->submit(['field1' => 'wrong_1', 'field2' => '']);
99+
100+
$errors = $form->getErrors(true);
101+
102+
$this->assertCount(1, $errors);
103+
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
75104
}
76105

77106
protected function createForm(array $options = [])

src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Form\AbstractType;
16+
use Symfony\Component\Form\Extension\Core\Type\FormType;
17+
use Symfony\Component\Form\Extension\Core\Type\TextType;
1618
use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint;
1719
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
1820
use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser;
1921
use Symfony\Component\Form\Form;
2022
use Symfony\Component\Form\FormBuilderInterface;
2123
use Symfony\Component\Form\FormFactoryBuilder;
2224
use Symfony\Component\OptionsResolver\OptionsResolver;
25+
use Symfony\Component\Validator\Constraints\GroupSequence;
26+
use Symfony\Component\Validator\Constraints\Length;
2327
use Symfony\Component\Validator\Constraints\NotBlank;
2428
use Symfony\Component\Validator\Mapping\CascadingStrategy;
2529
use Symfony\Component\Validator\Mapping\ClassMetadata;
@@ -49,6 +53,8 @@ public function test2Dot5ValidationApi()
4953
$this->assertCount(1, $metadata->getConstraints());
5054
$this->assertInstanceOf(FormConstraint::class, $metadata->getConstraints()[0]);
5155

56+
$this->assertSame(CascadingStrategy::NONE, $metadata->cascadingStrategy);
57+
$this->assertSame(TraversalStrategy::IMPLICIT, $metadata->traversalStrategy);
5258
$this->assertSame(CascadingStrategy::CASCADE, $metadata->getPropertyMetadata('children')[0]->cascadingStrategy);
5359
$this->assertSame(TraversalStrategy::IMPLICIT, $metadata->getPropertyMetadata('children')[0]->traversalStrategy);
5460
}
@@ -86,7 +92,28 @@ public function testFieldConstraintsInvalidateFormIfFieldIsSubmitted()
8692
$this->assertFalse($form->get('baz')->isValid());
8793
}
8894

89-
private function createForm($type)
95+
public function testFieldsValidateInSequence()
96+
{
97+
$form = $this->createForm(FormType::class, null, [
98+
'validation_groups' => new GroupSequence(['group1', 'group2']),
99+
])
100+
->add('foo', TextType::class, [
101+
'constraints' => [new Length(['min' => 10, 'groups' => ['group1']])],
102+
])
103+
->add('bar', TextType::class, [
104+
'constraints' => [new NotBlank(['groups' => ['group2']])],
105+
])
106+
;
107+
108+
$form->submit(['foo' => 'invalid', 'bar' => null]);
109+
110+
$errors = $form->getErrors(true);
111+
112+
$this->assertCount(1, $errors);
113+
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
114+
}
115+
116+
private function createForm($type, $data = null, array $options = [])
90117
{
91118
$validator = Validation::createValidatorBuilder()
92119
->setMetadataFactory(new LazyLoadingMetadataFactory(new StaticMethodLoader()))
@@ -95,7 +122,7 @@ private function createForm($type)
95122
$formFactoryBuilder->addExtension(new ValidatorExtension($validator));
96123
$formFactory = $formFactoryBuilder->getFormFactory();
97124

98-
return $formFactory->create($type);
125+
return $formFactory->create($type, $data, $options);
99126
}
100127
}
101128

0 commit comments

Comments
 (0)