Skip to content

[Form] Add constraints_from_* options #50459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

6.4
---
* Add `constraints_from_entity` and `constraints_from_property` option to `FormType`

6.3
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?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\Extension\Validator\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class ConstraintsFromListener implements EventSubscriberInterface
{
private ValidatorInterface $validator;

public static function getSubscribedEvents(): array
{
return [FormEvents::POST_SUBMIT => 'validateConstraints'];
}

public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}

public function validateConstraints(FormEvent $event): void
{
$form = $event->getForm();

$entity = $form->getConfig()->getOption('constraints_from_entity');

if (null !== $entity) {
$property = $form->getConfig()->getOption('constraints_from_property') ?? $form->getName();

$violations = $this->validator->validatePropertyValue($entity, $property, $event->getData());
/** @var ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
$form->addError(
new FormError(
$violation->getMessage(),
$violation->getMessageTemplate(),
$violation->getParameters(),
$violation->getPlural()
)
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Validator\Type;

use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Validator\EventListener\ConstraintsFromListener;
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\FormBuilderInterface;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function __construct(ValidatorInterface $validator, bool $legacyErrorMess
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber(new ConstraintsFromListener($this->validator));
$builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper));
}

Expand All @@ -58,13 +60,17 @@ public function configureOptions(OptionsResolver $resolver)
$resolver->setDefaults([
'error_mapping' => [],
'constraints' => [],
'constraints_from_entity' => null,
'constraints_from_property' => null,
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => [],
'allow_extra_fields' => false,
'extra_fields_message' => 'This form should not contain extra fields.',
]);
$resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']);
$resolver->setNormalizer('constraints', $constraintsNormalizer);
$resolver->setAllowedTypes('constraints_from_entity', ['string', 'null']);
$resolver->setAllowedTypes('constraints_from_property', ['string', 'null']);
}

public static function getExtendedTypes(): iterable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,119 @@ public function testInvalidMessage()
$this->assertEquals('This value is not valid.', $form->getConfig()->getOption('invalid_message'));
}

public function testConstraintsFromEntityValid()
{
// Maps firstName field to Author::firstName -> Length(3) constraint
$form = $this->createFormForConstraintsFrom();

$form->submit(['firstName' => 'foo']);

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

$this->assertCount(0, $errors);
}

public function testConstraintsFromEntityEmpty()
{
// Maps firstName field to Author::firstName -> Length(3) constraint
$form = $this->createFormForConstraintsFrom();

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

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

$this->assertCount(1, $errors);
}

public function testConstraintsFromEntityInvalid()
{
// Maps firstName field to Author::firstName -> Length(3) constraint
$form = $this->createFormForConstraintsFrom();

$form->submit(['firstName' => 'foobar']);

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

$this->assertCount(1, $errors);
}

public function testConstraintsFromEntityCustomPropertyValid()
{
// Maps firstName field to Author::lastName -> Length(min: 5) constraint
$form = $this->createFormForConstraintsFrom('lastName');

$form->submit(['firstName' => 'foobar']);

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

$this->assertCount(0, $errors);
}

public function testConstraintsFromEntityCustomPropertyEmpty()
{
// Maps firstName field to Author::lastName -> Length(min: 5) constraint
$form = $this->createFormForConstraintsFrom('lastName');

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

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

$this->assertCount(1, $errors);
}

public function testConstraintsFromEntityCustomPropertyInvalid()
{
// Maps firstName field to Author::lastName -> Length(min: 5) constraint
$form = $this->createFormForConstraintsFrom('lastName');

$form->submit(['firstName' => 'foo']);

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

$this->assertCount(1, $errors);
}

protected function createFormForConstraintsFrom(string $propertyName = null)
{
$formMetadata = new ClassMetadata(Form::class);
$authorMetadata = (new ClassMetadata(Author::class))
->addPropertyConstraint('firstName', new Length(3))
->addPropertyConstraint('lastName', new Length(min: 5))
;
$metadataFactory = $this->createMock(MetadataFactoryInterface::class);
$metadataFactory->expects($this->any())
->method('getMetadataFor')
->willReturnCallback(static function ($classOrObject) use ($formMetadata, $authorMetadata) {
if (Author::class === $classOrObject || $classOrObject instanceof Author) {
return $authorMetadata;
}

if (Form::class === $classOrObject || $classOrObject instanceof Form) {
return $formMetadata;
}

return new ClassMetadata(\is_string($classOrObject) ? $classOrObject : $classOrObject::class);
})
;

$validator = Validation::createValidatorBuilder()
->setMetadataFactory($metadataFactory)
->getValidator()
;

$form = Forms::createFormFactoryBuilder()
->addExtension(new ValidatorExtension($validator))
->getFormFactory()
->create(FormTypeTest::TESTED_TYPE)
->add('firstName', TextTypeTest::TESTED_TYPE, [
'constraints_from_entity' => Author::class,
'constraints_from_property' => $propertyName ?? null,
])
;

return $form;
}

protected function createForm(array $options = [])
{
return $this->factory->create(FormTypeTest::TESTED_TYPE, null, $options);
Expand Down