From 9112ef8b09c593a01c15a2f2956d5ab3722a532a Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 23 Jul 2020 14:09:46 +0200 Subject: [PATCH] [Form] Introduce validation events for forms --- .../Validator/Event/PostValidateEvent.php | 28 ++++++++++++++ .../Validator/Event/PreValidateEvent.php | 29 ++++++++++++++ .../EventListener/ValidationListener.php | 22 ++++++++++- .../Validator/FormValidationEvents.php | 32 ++++++++++++++++ .../EventListener/ValidationListenerTest.php | 38 +++++++++++++++++-- 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php create mode 100644 src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php create mode 100644 src/Symfony/Component/Form/Extension/Validator/FormValidationEvents.php diff --git a/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php b/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php new file mode 100644 index 0000000000000..350e46c9b1b20 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php @@ -0,0 +1,28 @@ + + * + * 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\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched after validation completes. + * + * In this stage, the form will return a correct value to Form::isValid() and allow for + * further working with the form data. + */ +final class PostValidateEvent extends FormEvent +{ + public function isFormValid(): bool + { + return $this->getForm()->isValid(); + } +} diff --git a/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php b/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php new file mode 100644 index 0000000000000..dcfd27062f06a --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php @@ -0,0 +1,29 @@ + + * + * 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\Event; + +use Symfony\Component\Form\FormEvent; + +/** + * This event is dispatched before validation begins. + * + * In this stage the model and view data may have been denormalized. Otherwise the form + * is desynchronized because transformation failed during submission. + * + * It can be used to fetch data after denormalization. + * + * The event attaches the current view data. To know whether this is the renormalized data + * or the invalid request data, call Form::isSynchronized() first. + */ +final class PreValidateEvent extends FormEvent +{ +} diff --git a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php index 867a5768aee6c..a29e5c1acb9a9 100644 --- a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php @@ -11,8 +11,12 @@ namespace Symfony\Component\Form\Extension\Validator\EventListener; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\Extension\Validator\Event\PostValidateEvent; +use Symfony\Component\Form\Extension\Validator\Event\PreValidateEvent; +use Symfony\Component\Form\Extension\Validator\FormValidationEvents; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -27,6 +31,8 @@ class ValidationListener implements EventSubscriberInterface private $violationMapper; + private $eventDispatcher; + /** * {@inheritdoc} */ @@ -35,10 +41,16 @@ public static function getSubscribedEvents() return [FormEvents::POST_SUBMIT => 'validateForm']; } - public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper) + public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper, ?EventDispatcherInterface $eventDispatcher = null) { $this->validator = $validator; $this->violationMapper = $violationMapper; + + if (!$this->eventDispatcher) { + @trigger_error(sprintf('The "$eventDispatcher" argument to the "%s" constructor will be required in Symfony 6.0.', self::class)); + } + + $this->eventDispatcher = $eventDispatcher; } public function validateForm(FormEvent $event) @@ -46,6 +58,10 @@ public function validateForm(FormEvent $event) $form = $event->getForm(); if ($form->isRoot()) { + if ($this->eventDispatcher) { + $this->eventDispatcher->dispatch(new PreValidateEvent($event->getForm(), $event->getData()), FormValidationEvents::PRE_VALIDATE); + } + // Form groups are validated internally (FormValidator). Here we don't set groups as they are retrieved into the validator. foreach ($this->validator->validate($form) as $violation) { // Allow the "invalid" constraint to be put onto @@ -54,6 +70,10 @@ public function validateForm(FormEvent $event) $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized); } + + if ($this->eventDispatcher) { + $this->eventDispatcher->dispatch(new PostValidateEvent($event->getForm(), $event->getData()), FormValidationEvents::POST_VALIDATE); + } } } } diff --git a/src/Symfony/Component/Form/Extension/Validator/FormValidationEvents.php b/src/Symfony/Component/Form/Extension/Validator/FormValidationEvents.php new file mode 100644 index 0000000000000..8e34493a75f02 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/FormValidationEvents.php @@ -0,0 +1,32 @@ + + * + * 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; + +final class FormValidationEvents +{ + /** + * @see Event\PreValidateEvent + * @Event(Event\PreValidateEvent::class) + */ + const PRE_VALIDATE = 'form.pre_validate'; + + /** + * This event is dispatched after validation completes. + * + * In this stage, the form will return a correct value to Form::isValid() and allow for + * further working with the form data. + * + * @see Event\PostValidateEvent + * @Event(Event\PostValidateEvent::class) + */ + const POST_VALIDATE = 'form.post_validate'; +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php index f8fbabd92a019..ceedeae9f4847 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint; use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; +use Symfony\Component\Form\Extension\Validator\FormValidationEvents; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilder; @@ -67,21 +68,26 @@ protected function setUp(): void $this->dispatcher = new EventDispatcher(); $this->factory = (new FormFactoryBuilder())->getFormFactory(); $this->validator = Validation::createValidator(); - $this->listener = new ValidationListener($this->validator, new ViolationMapper()); + $this->listener = new ValidationListener($this->validator, new ViolationMapper(), $this->dispatcher); $this->message = 'Message'; $this->messageTemplate = 'Message template'; $this->params = ['foo' => 'bar']; } - private function createForm($name = '', $compound = false) + private function createForm($name = '', $compound = false, ?object $listener = null) { - $config = new FormBuilder($name, null, new EventDispatcher(), (new FormFactoryBuilder())->getFormFactory()); + $config = new FormBuilder($name, null, $this->dispatcher, (new FormFactoryBuilder())->getFormFactory()); $config->setCompound($compound); if ($compound) { $config->setDataMapper(new PropertyPathMapper()); } + if ($listener) { + $config->addEventListener(FormValidationEvents::PRE_VALIDATE, [$listener, 'preValidate']); + $config->addEventListener(FormValidationEvents::POST_VALIDATE, [$listener, 'postValidate']); + } + return new Form($config); } @@ -136,6 +142,32 @@ public function testValidateWithEmptyViolationList() $this->assertTrue($form->isValid()); } + + public function testEventsAreDispatched() + { + $data = ['foo' => 'bar']; + + $mock = $this->getMockBuilder('\stdClass') + ->setMethods(['preValidate', 'postValidate']) + ->getMock(); + $mock->expects($this->once()) + ->method('preValidate') + ->with($this->callback(function ($event) use ($data) { + return $data === $event->getData(); + })); + $mock->expects($this->once()) + ->method('postValidate') + ->with($this->callback(function ($event) use ($data) { + return $data === $event->getData(); + })); + + $form = $this->createForm('', false, $mock); + $form->submit($data); + + $this->listener->validateForm(new FormEvent($form, $data)); + + $this->assertTrue($form->isValid()); + } } class SubmittedNotSynchronizedForm extends Form