bug #36865 [Form] validate subforms in all validation groups (xabbuh)

This PR was merged into the 3.4 branch.

Discussion
----------

[Form] validate subforms in all validation groups

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #36852
| License       | MIT
| Doc PR        |

Commits
-------

b819d94d14 validate subforms in all validation groups
This commit is contained in:
Nicolas Grekas 2020-05-30 20:41:29 +02:00
commit 2e8ae40183
5 changed files with 61 additions and 18 deletions

View File

@ -63,12 +63,16 @@ class FormValidator extends ConstraintValidator
/** @var Constraint[] $constraints */
$constraints = $config->getOption('constraints', []);
$hasChildren = $form->count() > 0;
if ($hasChildren && $form->isRoot()) {
$this->resolvedGroups = new \SplObjectStorage();
}
if ($groups instanceof GroupSequence) {
// Validate the data, the form AND nested fields in sequence
$violationsCount = $this->context->getViolations()->count();
$fieldPropertyPath = \is_object($data) ? 'children[%s]' : 'children%s';
$hasChildren = $form->count() > 0;
$this->resolvedGroups = $hasChildren ? new \SplObjectStorage() : null;
foreach ($groups->groups as $group) {
if ($validateDataGraph) {
@ -86,7 +90,8 @@ class FormValidator extends ConstraintValidator
// sequence recursively, thus some fields could fail
// in different steps without breaking early enough
$this->resolvedGroups[$field] = (array) $group;
$validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $formConstraint);
$fieldFormConstraint = new Form();
$validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $fieldFormConstraint);
}
}
@ -94,12 +99,9 @@ class FormValidator extends ConstraintValidator
break;
}
}
if ($hasChildren) {
// destroy storage at the end of the sequence to avoid memory leaks
$this->resolvedGroups = null;
}
} else {
$fieldPropertyPath = \is_object($data) ? 'children[%s]' : 'children%s';
if ($validateDataGraph) {
$validator->atPath('data')->validate($data, null, $groups);
}
@ -125,6 +127,19 @@ class FormValidator extends ConstraintValidator
}
}
}
foreach ($form->all() as $field) {
if ($field->isSubmitted()) {
$this->resolvedGroups[$field] = $groups;
$fieldFormConstraint = new Form();
$validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $fieldFormConstraint);
}
}
}
if ($hasChildren && $form->isRoot()) {
// destroy storage to avoid memory leaks
$this->resolvedGroups = new \SplObjectStorage();
}
} elseif (!$form->isSynchronized()) {
$childrenSynchronized = true;

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Validator;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validator\ValidatorInterface;
@ -37,7 +37,7 @@ class ValidatorExtension extends AbstractExtension
/* @var $metadata ClassMetadata */
$metadata->addConstraint(new Form());
$metadata->addPropertyConstraint('children', new Valid());
$metadata->addConstraint(new Traverse(false));
$this->validator = $validator;
}

View File

@ -6,8 +6,8 @@
<class name="Symfony\Component\Form\Form">
<constraint name="Symfony\Component\Form\Extension\Validator\Constraints\Form" />
<property name="children">
<constraint name="Valid" />
</property>
<constraint name="Symfony\Component\Validator\Constraints\Traverse">
<option name="traverse">false</option>
</constraint>
</class>
</constraint-mapping>

View File

@ -615,7 +615,8 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->expectValidateValueAt(0, 'children[child]', $form->get('child'), new Form());
$this->validator->validate($form, new Form());
@ -638,7 +639,8 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->expectValidateValueAt(0, 'children[child]', $form->get('child'), new Form());
$this->validator->validate($form, new Form());

View File

@ -54,9 +54,8 @@ class ValidatorExtensionTest extends TestCase
$this->assertInstanceOf(FormConstraint::class, $metadata->getConstraints()[0]);
$this->assertSame(CascadingStrategy::NONE, $metadata->cascadingStrategy);
$this->assertSame(TraversalStrategy::IMPLICIT, $metadata->traversalStrategy);
$this->assertSame(CascadingStrategy::CASCADE, $metadata->getPropertyMetadata('children')[0]->cascadingStrategy);
$this->assertSame(TraversalStrategy::IMPLICIT, $metadata->getPropertyMetadata('children')[0]->traversalStrategy);
$this->assertSame(TraversalStrategy::NONE, $metadata->traversalStrategy);
$this->assertCount(0, $metadata->getPropertyMetadata('children'));
}
public function testDataConstraintsInvalidateFormEvenIfFieldIsNotSubmitted()
@ -138,6 +137,33 @@ class ValidatorExtensionTest extends TestCase
$this->assertInstanceOf(Length::class, $errors[1]->getCause()->getConstraint());
}
public function testConstraintsInDifferentGroupsOnSingleField()
{
$form = $this->createForm(FormType::class, null, [
'validation_groups' => new GroupSequence(['group1', 'group2']),
])
->add('foo', TextType::class, [
'constraints' => [
new NotBlank([
'groups' => ['group1'],
]),
new Length([
'groups' => ['group2'],
'max' => 3,
]),
],
]);
$form->submit([
'foo' => 'test@example.com',
]);
$errors = $form->getErrors(true);
$this->assertFalse($form->isValid());
$this->assertCount(1, $errors);
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
}
private function createForm($type, $data = null, array $options = [])
{
$validator = Validation::createValidatorBuilder()