Skip to content

Commit

Permalink
[Form] Fixed handling groups sequence validation
Browse files Browse the repository at this point in the history
  • Loading branch information
HeahDude committed Apr 4, 2020
1 parent 5da141b commit 9e61cd3
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 20 deletions.
Expand Up @@ -24,6 +24,15 @@
*/
class FormValidator extends ConstraintValidator
{
private static $resolvedGroups;

public function __construct()
{
if (null === self::$resolvedGroups) {
self::$resolvedGroups = new \SplObjectStorage();
}
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -55,31 +64,38 @@ public function validate($form, Constraint $formConstraint)
// Validate the data against its own constraints
if ($form->isRoot() && (\is_object($data) || \is_array($data))) {
if (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups)) {
$validator->atPath('data')->validate($form->getData(), null, $groups);
$validator->atPath('data')->validate($data, null, $groups);
}
}

// Validate the data against the constraints defined
// in the form
// Validate the data against the constraints defined in the form
/** @var Constraint[] $constraints */
$constraints = $config->getOption('constraints', []);

if ($groups instanceof GroupSequence) {
$validator->atPath('data')->validate($form->getData(), $constraints, $groups);
// Otherwise validate a constraint only once for the first
// matching group
foreach ($groups as $group) {
if (\in_array($group, $formConstraint->groups)) {
$validator->atPath('data')->validate($form->getData(), $formConstraint, $group);
if (\count($this->context->getViolations()) > 0) {
break;
// Validate the form AND nested fields in sequence
$violationsCount = $this->context->getViolations()->count();
$fieldPropertyPath = \is_object($data) ? 'data.%s' : '%s';

foreach ($groups->groups as $group) {
$validator->atPath('data')->validate($data, $constraints, $group);

foreach ($form->all() as $field) {
if ($field->isSubmitted()) {
self::$resolvedGroups[$field] = [$group];
$validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $formConstraint);
}
}

if ($violationsCount < $this->context->getViolations()->count()) {
break;
}
}
} else {
foreach ($constraints as $constraint) {
// For the "Valid" constraint, validate the data in all groups
if ($constraint instanceof Valid) {
$validator->atPath('data')->validate($form->getData(), $constraint, $groups);
$validator->atPath('data')->validate($data, $constraint, $groups);

continue;
}
Expand All @@ -88,7 +104,7 @@ public function validate($form, Constraint $formConstraint)
// matching group
foreach ($groups as $group) {
if (\in_array($group, $constraint->groups)) {
$validator->atPath('data')->validate($form->getData(), $constraint, $group);
$validator->atPath('data')->validate($data, $constraint, $group);

// Prevent duplicate validation
if (!$constraint instanceof Composite) {
Expand Down Expand Up @@ -171,6 +187,10 @@ private static function getValidationGroups(FormInterface $form)
return self::resolveValidationGroups($groups, $form);
}

if (isset(self::$resolvedGroups[$form])) {
return self::$resolvedGroups[$form];
}

$form = $form->getParent();
} while (null !== $form);

Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Form/Resources/config/validation.xml
Expand Up @@ -7,7 +7,7 @@
<class name="Symfony\Component\Form\Form">
<constraint name="Symfony\Component\Form\Extension\Validator\Constraints\Form" />
<property name="children">
<constraint name="Valid" />
<constraint name="Valid" />
</property>
</class>
</constraint-mapping>
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator;
use Symfony\Component\Form\FormBuilder;
Expand Down Expand Up @@ -402,7 +403,8 @@ public function testHandleGroupSequenceValidationGroups()
$form->submit([]);

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

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

Expand Down Expand Up @@ -756,6 +758,39 @@ public function testCompositeConstraintValidatedInEachGroup()
$this->assertSame('data[field2]', $context->getViolations()[1]->getPropertyPath());
}

public function testCompositeConstraintValidatedInSequence()
{
$form = $this->getCompoundForm([], [
'constraints' => [
new Collection([
'field1' => new NotBlank([
'groups' => ['field1'],
]),
'field2' => new NotBlank([
'groups' => ['field2'],
]),
]),
],
'validation_groups' => new GroupSequence(['field1', 'field2']),
])
->add($this->getForm('field1'))
->add($this->getForm('field2'))
;

$form->submit([
'field1' => '',
'field2' => '',
]);

$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
$this->validator->initialize($context);
$this->validator->validate($form, new Form());

$this->assertCount(1, $context->getViolations());
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
$this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath());
}

protected function createValidator()
{
return new FormValidator();
Expand Down Expand Up @@ -784,7 +819,7 @@ private function getForm($name = 'name', $dataClass = null, array $options = [])

private function getCompoundForm($data, array $options = [])
{
return $this->getBuilder('name', \get_class($data), $options)
return $this->getBuilder('name', \is_object($data) ? \get_class($data) : null, $options)
->setData($data)
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -64,14 +65,43 @@ public function testGroupSequenceWithConstraintsOption()
->add('field', TextTypeTest::TESTED_TYPE, [
'constraints' => [
new Length(['min' => 10, 'groups' => ['First']]),
new Email(['groups' => ['Second']]),
new NotBlank(['groups' => ['Second']]),
],
])
;

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

$this->assertCount(1, $form->getErrors(true));
$errors = $form->getErrors(true);

$this->assertCount(1, $errors);
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
}

public function testManyFieldsGroupSequenceWithConstraintsOption()
{
$form = Forms::createFormFactoryBuilder()
->addExtension(new ValidatorExtension(Validation::createValidator()))
->getFormFactory()
->create(FormTypeTest::TESTED_TYPE, null, (['validation_groups' => new GroupSequence(['First', 'Second'])]))
->add('field1', TextTypeTest::TESTED_TYPE, [
'constraints' => [
new Length(['min' => 10, 'groups' => ['First']]),
],
])
->add('field2', TextTypeTest::TESTED_TYPE, [
'constraints' => [
new NotBlank(['groups' => ['Second']]),
],
])
;

$form->submit(['field1' => 'wrong_1', 'field2' => 'wrong_2']);

$errors = $form->getErrors(true);

$this->assertCount(1, $errors);
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
}

protected function createForm(array $options = [])
Expand Down
Expand Up @@ -13,14 +13,19 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryBuilder;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
Expand Down Expand Up @@ -49,6 +54,8 @@ public function test2Dot5ValidationApi()
$this->assertCount(1, $metadata->getConstraints());
$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);
}
Expand Down Expand Up @@ -86,7 +93,28 @@ public function testFieldConstraintsInvalidateFormIfFieldIsSubmitted()
$this->assertFalse($form->get('baz')->isValid());
}

private function createForm($type)
public function testFieldsValidateInSequence()
{
$form = $this->createForm(FormType::class, null, [
'validation_groups' => new GroupSequence(['group1', 'group2']),
])
->add('foo', TextType::class, [
'constraints' => [new Length(['min' => 10, 'groups' => ['group1']])],
])
->add('bar', TextType::class, [
'constraints' => [new NotBlank(['groups' => ['group2']])],
])
;

$form->submit(['foo' => 'invalid', 'bar' => null]);

$errors = $form->getErrors(true);

$this->assertCount(1, $errors);
$this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint());
}

private function createForm($type, $data = null, array $options = [])
{
$validator = Validation::createValidatorBuilder()
->setMetadataFactory(new LazyLoadingMetadataFactory(new StaticMethodLoader()))
Expand All @@ -95,7 +123,7 @@ private function createForm($type)
$formFactoryBuilder->addExtension(new ValidatorExtension($validator));
$formFactory = $formFactoryBuilder->getFormFactory();

return $formFactory->create($type);
return $formFactory->create($type, $data, $options);
}
}

Expand Down

0 comments on commit 9e61cd3

Please sign in to comment.