Files
archived-form/Tests/Extension/Validator/Constraints/FormValidatorTest.php
2025-12-28 17:33:20 +01:00

741 lines
24 KiB
PHP

<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Validator\Constraints;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormFactoryBuilder;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\SubmitButtonBuilder;
use Symfony\Component\Translation\IdentityTranslator;
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\Constraints\Valid;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
use Symfony\Component\Validator\Validation;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormValidatorTest extends ConstraintValidatorTestCase
{
private EventDispatcher $dispatcher;
private FormFactoryInterface $factory;
protected function setUp(): void
{
$this->dispatcher = new EventDispatcher();
$this->factory = (new FormFactoryBuilder())
->addExtension(new ValidatorExtension(Validation::createValidator()))
->getFormFactory();
parent::setUp();
$this->constraint = new Form();
}
public function testValidate()
{
$object = new \stdClass();
$options = ['validation_groups' => ['group1', 'group2']];
$form = $this->getCompoundForm($object, $options);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testValidateConstraints()
{
$object = new \stdClass();
$constraint1 = new NotNull(groups: ['group1', 'group2']);
$constraint2 = new NotBlank(groups: ['group2']);
$constraint3 = new Length(groups: ['group2'], min: 3);
$options = [
'validation_groups' => ['group1', 'group2'],
'constraints' => [$constraint1, $constraint2, $constraint3],
];
$form = $this->getCompoundForm($object, $options);
$form->submit([]);
// First default constraints
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
// Then custom constraints
$this->expectValidateValueAt(1, 'data', $object, [$constraint1], 'group1');
$this->expectValidateValueAt(2, 'data', $object, [$constraint2, $constraint3], 'group2');
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testValidateChildIfValidConstraint()
{
$object = new \stdClass();
$parent = $this->getBuilder('parent')
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$options = [
'validation_groups' => ['group1', 'group2'],
'constraints' => [new Valid()],
];
$form = $this->getCompoundForm($object, $options);
$parent->add($form);
$parent->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontValidateIfParentWithoutValidConstraint()
{
$object = new \stdClass();
$parent = $this->getBuilder('parent', null)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$options = ['validation_groups' => ['group1', 'group2']];
$form = $this->getBuilder('name', '\stdClass', $options)->getForm();
$parent->add($form);
$form->setData($object);
$parent->submit([]);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testMissingConstraintIndex()
{
$object = new \stdClass();
$form = $this->getCompoundForm($object);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, ['Default']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testValidateConstraintsOptionEvenIfNoValidConstraint()
{
$object = new \stdClass();
$constraint1 = new NotNull(groups: ['group1', 'group2']);
$constraint2 = new NotBlank(groups: ['group2']);
$parent = $this->getBuilder('parent', null)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$options = [
'validation_groups' => ['group1', 'group2'],
'constraints' => [$constraint1, $constraint2],
];
$form = $this->getCompoundForm($object, $options);
$parent->add($form);
$parent->submit([]);
$this->expectValidateValueAt(0, 'data', $object, [$constraint1], 'group1');
$this->expectValidateValueAt(1, 'data', $object, [$constraint2], 'group2');
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontValidateIfNoValidationGroups()
{
$object = new \stdClass();
$form = $this->getBuilder('name', '\stdClass', [
'validation_groups' => [],
])
->setData($object)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$form->setData($object);
$form->submit([]);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontValidateConstraintsIfNoValidationGroups()
{
$object = new \stdClass();
$options = [
'validation_groups' => [],
'constraints' => [new NotBlank(), new NotNull()],
];
$form = $this->getBuilder('name', '\stdClass', $options)
->setData($object)
->getForm();
// Launch transformer
$form->submit('foo');
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontValidateChildConstraintsIfCallableNoValidationGroups()
{
$formOptions = [
'constraints' => [new Valid()],
'validation_groups' => [],
];
$form = $this->getBuilder('name', null, $formOptions)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$childOptions = ['constraints' => [new NotBlank()]];
$child = $this->getCompoundForm(new \stdClass(), $childOptions);
$form->add($child);
$form->submit([]);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontValidateIfNotSynchronized()
{
$object = new \stdClass();
$form = $this->getBuilder('name', '\stdClass', [
'invalid_message' => 'invalid_message_key',
// Invalid message parameters must be supported, because the
// invalid message can be a translation key
// see https://github.com/symfony/symfony/issues/5144
'invalid_message_parameters' => ['{{ foo }}' => 'bar'],
])
->setData($object)
->addViewTransformer(new CallbackTransformer(
static fn ($data) => $data,
static fn () => throw new TransformationFailedException()
))
->getForm();
// Launch transformer
$form->submit('foo');
$this->assertTrue($form->isSubmitted());
$this->assertFalse($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->buildViolation('invalid_message_key')
->setParameter('{{ value }}', 'foo')
->setParameter('{{ foo }}', 'bar')
->setInvalidValue('foo')
->setCode(Form::NOT_SYNCHRONIZED_ERROR)
->setCause($form->getTransformationFailure())
->assertRaised();
}
public function testAddInvalidErrorEvenIfNoValidationGroups()
{
$object = new \stdClass();
$form = $this->getBuilder('name', '\stdClass', [
'invalid_message' => 'invalid_message_key',
// Invalid message parameters must be supported, because the
// invalid message can be a translation key
// see https://github.com/symfony/symfony/issues/5144
'invalid_message_parameters' => ['{{ foo }}' => 'bar'],
'validation_groups' => [],
])
->setData($object)
->addViewTransformer(new CallbackTransformer(
static fn ($data) => $data,
static fn () => throw new TransformationFailedException()
))
->getForm();
// Launch transformer
$form->submit('foo');
$this->assertTrue($form->isSubmitted());
$this->assertFalse($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->buildViolation('invalid_message_key')
->setParameter('{{ value }}', 'foo')
->setParameter('{{ foo }}', 'bar')
->setInvalidValue('foo')
->setCode(Form::NOT_SYNCHRONIZED_ERROR)
->setCause($form->getTransformationFailure())
->assertRaised();
}
public function testDontValidateConstraintsIfNotSynchronized()
{
$object = new \stdClass();
$options = [
'invalid_message' => 'invalid_message_key',
'validation_groups' => ['group1', 'group2'],
'constraints' => [new NotBlank(), new NotBlank()],
];
$form = $this->getBuilder('name', '\stdClass', $options)
->setData($object)
->addViewTransformer(new CallbackTransformer(
static fn ($data) => $data,
static fn () => throw new TransformationFailedException()
))
->getForm();
// Launch transformer
$form->submit('foo');
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->buildViolation('invalid_message_key')
->setParameter('{{ value }}', 'foo')
->setInvalidValue('foo')
->setCode(Form::NOT_SYNCHRONIZED_ERROR)
->setCause($form->getTransformationFailure())
->assertRaised();
}
public function testTransformationFailedExceptionInvalidMessageIsUsed()
{
$object = new \stdClass();
$form = $this
->getBuilder('name', '\stdClass', [
'invalid_message' => 'invalid_message_key',
'invalid_message_parameters' => ['{{ foo }}' => 'foo'],
])
->setData($object)
->addViewTransformer(new CallbackTransformer(
static fn ($data) => $data,
static function () {
$failure = new TransformationFailedException();
$failure->setInvalidMessage('safe message to be used', ['{{ bar }}' => 'bar']);
throw $failure;
}
))
->getForm()
;
$form->submit('value');
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->buildViolation('safe message to be used')
->setParameters([
'{{ value }}' => 'value',
'{{ foo }}' => 'foo',
'{{ bar }}' => 'bar',
])
->setInvalidValue('value')
->setCode(Form::NOT_SYNCHRONIZED_ERROR)
->setCause($form->getTransformationFailure())
->assertRaised()
;
}
public function testHandleGroupSequenceValidationGroups()
{
$object = new \stdClass();
$options = ['validation_groups' => new GroupSequence(['group1', 'group2'])];
$form = $this->getCompoundForm($object, $options);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, 'group1');
$this->expectValidateAt(1, 'data', $object, 'group2');
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testHandleCallbackValidationGroups()
{
$object = new \stdClass();
$options = ['validation_groups' => $this->getValidationGroups(...)];
$form = $this->getCompoundForm($object, $options);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontExecuteFunctionNames()
{
$object = new \stdClass();
$options = ['validation_groups' => 'header'];
$form = $this->getCompoundForm($object, $options);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, ['header']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testHandleClosureValidationGroups()
{
$object = new \stdClass();
$options = ['validation_groups' => static fn (FormInterface $form) => ['group1', 'group2']];
$form = $this->getCompoundForm($object, $options);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testUseValidationGroupOfClickedButton()
{
$object = new \stdClass();
$parent = $this->getBuilder('parent')
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$form = $this->getForm('name', '\stdClass', [
'validation_groups' => 'form_group',
'constraints' => [new Valid()],
]);
$parent->add($form);
$parent->add($this->getSubmitButton('submit', [
'validation_groups' => 'button_group',
]));
$parent->submit(['name' => $object, 'submit' => '']);
$this->expectValidateAt(0, 'data', $object, ['button_group']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontUseValidationGroupOfUnclickedButton()
{
$object = new \stdClass();
$parent = $this->getBuilder('parent')
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$form = $this->getCompoundForm($object, [
'validation_groups' => 'form_group',
'constraints' => [new Valid()],
]);
$parent->add($form);
$parent->add($this->getSubmitButton('submit', [
'validation_groups' => 'button_group',
]));
$parent->submit([]);
$this->expectValidateAt(0, 'data', $object, ['form_group']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testUseInheritedValidationGroup()
{
$object = new \stdClass();
$parentOptions = ['validation_groups' => 'group'];
$parent = $this->getBuilder('parent', null, $parentOptions)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$formOptions = ['constraints' => [new Valid()]];
$form = $this->getCompoundForm($object, $formOptions);
$parent->add($form);
$parent->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testUseInheritedCallbackValidationGroup()
{
$object = new \stdClass();
$parentOptions = ['validation_groups' => $this->getValidationGroups(...)];
$parent = $this->getBuilder('parent', null, $parentOptions)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$formOptions = ['constraints' => [new Valid()]];
$form = $this->getCompoundForm($object, $formOptions);
$parent->add($form);
$parent->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testUseInheritedClosureValidationGroup()
{
$object = new \stdClass();
$parentOptions = [
'validation_groups' => static fn () => ['group1', 'group2'],
];
$parent = $this->getBuilder('parent', null, $parentOptions)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$formOptions = ['constraints' => [new Valid()]];
$form = $this->getCompoundForm($object, $formOptions);
$parent->add($form);
$parent->submit([]);
$this->expectValidateAt(0, 'data', $object, ['group1', 'group2']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testAppendPropertyPath()
{
$object = new \stdClass();
$form = $this->getCompoundForm($object);
$form->submit([]);
$this->expectValidateAt(0, 'data', $object, ['Default']);
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testDontWalkScalars()
{
$form = $this->getBuilder()
->setData('scalar')
->getForm();
$form->submit('foo');
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
public function testViolationIfExtraData()
{
$form = $this->getBuilder('parent', null, ['extra_fields_message' => 'Extra!|Extras!'])
->setCompound(true)
->setDataMapper(new DataMapper())
->add($this->getBuilder('child'))
->getForm();
$form->submit(['foo' => 'bar']);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectValidateValueAt(0, 'children[child]', $form->get('child'), new Form());
$this->validator->validate($form, new Form());
$this->buildViolation('Extra!|Extras!')
->setParameter('{{ extra_fields }}', '"foo"')
->setInvalidValue(['foo' => 'bar'])
->setPlural(1)
->setCode(Form::NO_SUCH_FIELD_ERROR)
->assertRaised();
}
public function testViolationFormatIfMultipleExtraFields()
{
$form = $this->getBuilder('parent', null, ['extra_fields_message' => 'Extra!|Extras!!'])
->setCompound(true)
->setDataMapper(new DataMapper())
->add($this->getBuilder('child'))
->getForm();
$form->submit(['foo' => 'bar', 'baz' => 'qux', 'quux' => 'quuz']);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->expectValidateValueAt(0, 'children[child]', $form->get('child'), new Form());
$this->validator->validate($form, new Form());
$this->buildViolation('Extra!|Extras!!')
->setParameter('{{ extra_fields }}', '"foo", "baz", "quux"')
->setInvalidValue(['foo' => 'bar', 'baz' => 'qux', 'quux' => 'quuz'])
->setPlural(3)
->setCode(Form::NO_SUCH_FIELD_ERROR)
->assertRaised();
}
public function testNoViolationIfAllowExtraData()
{
$form = $this
->getBuilder('parent', null, ['allow_extra_fields' => true])
->setCompound(true)
->setDataMapper(new DataMapper())
->add($this->getBuilder('child'))
->getForm();
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
$form->submit(['foo' => 'bar']);
$this->validator->initialize($context);
$this->validator->validate($form, new Form());
$this->assertCount(0, $context->getViolations());
}
/**
* Access has to be public, as this method is called via callback array
* in {@link testValidateFormDataCanHandleCallbackValidationGroups()}
* and {@link testValidateFormDataUsesInheritedCallbackValidationGroup()}.
*/
public function getValidationGroups(FormInterface $form)
{
return ['group1', 'group2'];
}
public function testCauseForNotAllowedExtraFieldsIsTheFormConstraint()
{
$form = $this
->getBuilder('form', null, ['constraints' => [new NotBlank(groups: ['foo'])]])
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
$form->submit([
'extra_data' => 'foo',
]);
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
$constraint = new Form();
$this->validator->initialize($context);
$this->validator->validate($form, $constraint);
$this->assertCount(1, $context->getViolations());
$this->assertSame($constraint, $context->getViolations()->get(0)->getConstraint());
}
protected function createValidator(): FormValidator
{
return new FormValidator();
}
private function getBuilder(string $name = 'name', ?string $dataClass = null, array $options = []): FormBuilder
{
$options = array_replace([
'constraints' => [],
'invalid_message_parameters' => [],
], $options);
return new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory, $options);
}
private function getForm($name = 'name', $dataClass = null, array $options = [])
{
return $this->getBuilder($name, $dataClass, $options)->getForm();
}
private function getCompoundForm($data, array $options = [])
{
return $this->getBuilder('name', \is_object($data) ? $data::class : null, $options)
->setData($data)
->setCompound(true)
->setDataMapper(new DataMapper())
->getForm();
}
private function getSubmitButton($name = 'name', array $options = [])
{
$builder = new SubmitButtonBuilder($name, $options);
return $builder->getForm();
}
}