From fbe76e96b5a415c09744fefa6f62d57c903dda8f Mon Sep 17 00:00:00 2001 From: Roland Dufour Date: Mon, 29 May 2023 00:54:17 +0200 Subject: [PATCH] [Form] Add constraints_from_* options Apply constraints to field from an entity not mapped by the form data_class. Fix fabbot review Fix changelog to 6.4 --- src/Symfony/Component/Form/CHANGELOG.md | 4 + .../EventListener/ConstraintsFromListener.php | 58 +++++++++ .../Type/FormTypeValidatorExtension.php | 6 + .../Type/FormTypeValidatorExtensionTest.php | 113 ++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 src/Symfony/Component/Form/Extension/Validator/EventListener/ConstraintsFromListener.php diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 1ff39c9726070..4e08b79ab1a3f 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +6.4 +--- + * Add `constraints_from_entity` and `constraints_from_property` option to `FormType` + 6.3 --- diff --git a/src/Symfony/Component/Form/Extension/Validator/EventListener/ConstraintsFromListener.php b/src/Symfony/Component/Form/Extension/Validator/EventListener/ConstraintsFromListener.php new file mode 100644 index 0000000000000..ac14bab991dfb --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/EventListener/ConstraintsFromListener.php @@ -0,0 +1,58 @@ + + * + * 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() + ) + ); + } + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php index 54eebaf63e43b..1d95c740f0b67 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -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; @@ -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)); } @@ -58,6 +60,8 @@ 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, @@ -65,6 +69,8 @@ public function configureOptions(OptionsResolver $resolver) ]); $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 diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 3b4cd77396c60..f4e4d8262af31 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -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);