From 91ac630cde09cb92fc41bab2801308c9d22fa5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20ALFAIATE?= Date: Sat, 6 Aug 2022 17:13:28 +0700 Subject: [PATCH 1/4] [Form] Introduce validation events --- src/Symfony/Component/Form/CHANGELOG.md | 1 + .../Validator/Event/PostValidateEvent.php | 21 +++++++++ .../Validator/Event/PreValidateEvent.php | 21 +++++++++ .../EventListener/ValidationListener.php | 32 +++++++++++++ .../Validator/ValidatorFormEvents.php | 46 +++++++++++++++++++ .../EventListener/ValidationListenerTest.php | 28 ++++++++++- 6 files changed, 148 insertions(+), 1 deletion(-) 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/ValidatorFormEvents.php diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index b5150a4c847a3..0b6e11db5fc0b 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Allow passing `TranslatableInterface` objects to the `ChoiceView` label * Allow passing `TranslatableInterface` objects to the `help` option +* Add `form.pre_validate` and `form.post_validate` events 6.1 --- 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..3ded15749a99e --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php @@ -0,0 +1,21 @@ + + * + * 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 the root form validation. + */ +final class PostValidateEvent extends FormEvent +{ +} 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..5678be78238ce --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php @@ -0,0 +1,21 @@ + + * + * 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 the root form validation. + */ +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 2963d6f7b02ef..b144791c31a78 100644 --- a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php @@ -13,9 +13,11 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\Extension\Validator\ValidatorFormEvents; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** @@ -26,6 +28,9 @@ class ValidationListener implements EventSubscriberInterface private ValidatorInterface $validator; private ViolationMapperInterface $violationMapper; + /** @var FormInterface[][] */ + private static array $dispatchEvents = []; + public static function getSubscribedEvents(): array { return [FormEvents::POST_SUBMIT => 'validateForm']; @@ -41,7 +46,16 @@ public function validateForm(FormEvent $event) { $form = $event->getForm(); + // Register events to dispatch during (root form) validation + foreach (ValidatorFormEvents::ALIASES as $eventName) { + if ($form->getConfig()->getEventDispatcher()->hasListeners($eventName)) { + self::$dispatchEvents[$eventName][] = $form; + } + } + if ($form->isRoot()) { + $this->dispatchEvents(ValidatorFormEvents::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 @@ -50,6 +64,24 @@ public function validateForm(FormEvent $event) $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized); } + + $this->dispatchEvents(ValidatorFormEvents::POST_VALIDATE); + } + } + + private function dispatchEvents(string $eventName) + { + if (!isset(self::$dispatchEvents[$eventName])) { + return; } + + $event = array_flip(ValidatorFormEvents::ALIASES)[$eventName]; + + foreach (self::$dispatchEvents[$eventName] as $form) { + $event = new $event($form, $form->getData()); + $form->getConfig()->getEventDispatcher()->dispatch($event, $eventName); + } + + unset(self::$dispatchEvents[$eventName]); } } diff --git a/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php b/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php new file mode 100644 index 0000000000000..9789512c65c15 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php @@ -0,0 +1,46 @@ + + * + * 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; + +use Symfony\Component\Form\Extension\Validator\Event\PostValidateEvent; +use Symfony\Component\Form\Extension\Validator\Event\PreValidateEvent; + +final class ValidatorFormEvents +{ + /** + * This event is dispatched after validation completes. + * + * @Event("Symfony\Component\Form\Extension\Validator\Event\PreValidateEvent") + */ + public const PRE_VALIDATE = 'form.pre_validate'; + + /** + * This event is dispatched after validation completes. + * + * @Event("Symfony\Component\Form\Extension\Validator\Event\PostValidateEvent") + */ + public const POST_VALIDATE = 'form.post_validate'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + public const ALIASES = [ + PreValidateEvent::class => self::PRE_VALIDATE, + PostValidateEvent::class => self::POST_VALIDATE, + ]; + + private function __construct() + { + } +} 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 ba0118391533e..a7f22409eb334 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\DataMapper; use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint; use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; +use Symfony\Component\Form\Extension\Validator\ValidatorFormEvents; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilder; @@ -73,7 +74,7 @@ protected function setUp(): void $this->params = ['foo' => 'bar']; } - private function createForm($name = '', $compound = false) + private function createForm($name = '', $compound = false, $listener = null) { $config = new FormBuilder($name, null, new EventDispatcher(), (new FormFactoryBuilder())->getFormFactory()); $config->setCompound($compound); @@ -82,6 +83,11 @@ private function createForm($name = '', $compound = false) $config->setDataMapper(new DataMapper()); } + if ($listener) { + $config->addEventListener(ValidatorFormEvents::PRE_VALIDATE, [$listener, 'preValidate']); + $config->addEventListener(ValidatorFormEvents::POST_VALIDATE, [$listener, 'postValidate']); + } + return new Form($config); } @@ -136,6 +142,26 @@ public function testValidateWithEmptyViolationList() $this->assertTrue($form->isValid()); } + + public function testEventsAreDispatched() + { + $listenerMock = $this->getMockBuilder(\stdClass::class)->setMethods(['preValidate', 'postValidate'])->getMock(); + + $childForm = $this->createForm('child', false, $listenerMock); + $form = $this->createForm('', true, $listenerMock); + $form->add($childForm); + + $form->submit(['child' => null]); + + $this->listener->validateForm(new FormEvent($childForm, null)); + + // Events are triggered only when the root form is validated + $listenerMock->expects($this->exactly(2))->method('preValidate'); + $listenerMock->expects($this->exactly(2))->method('postValidate'); + $this->listener->validateForm(new FormEvent($form, null)); + + $this->assertTrue($form->isValid()); + } } class SubmittedNotSynchronizedForm extends Form From 2113e828468eadeadb20edb4b2cd3778a44c693b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20ALFAIATE?= Date: Sun, 7 Aug 2022 15:40:06 +0700 Subject: [PATCH 2/4] Fix comment --- .../Component/Form/Extension/Validator/ValidatorFormEvents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php b/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php index 9789512c65c15..0465dc2ca6635 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php +++ b/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php @@ -17,7 +17,7 @@ final class ValidatorFormEvents { /** - * This event is dispatched after validation completes. + * This event is dispatched before validation starts. * * @Event("Symfony\Component\Form\Extension\Validator\Event\PreValidateEvent") */ From d34d434fa1b16419dbddab2b8d10f7ced6094d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20ALFAIATE?= Date: Sun, 7 Aug 2022 21:45:09 +0700 Subject: [PATCH 3/4] Comments consistency --- .../Form/Extension/Validator/Event/PostValidateEvent.php | 2 +- .../Form/Extension/Validator/Event/PreValidateEvent.php | 2 +- .../Form/Extension/Validator/ValidatorFormEvents.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php b/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php index 3ded15749a99e..dc0823f239646 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php +++ b/src/Symfony/Component/Form/Extension/Validator/Event/PostValidateEvent.php @@ -14,7 +14,7 @@ use Symfony\Component\Form\FormEvent; /** - * This event is dispatched after the root form validation. + * This event is dispatched after (root form) validation completes. */ final class PostValidateEvent extends FormEvent { diff --git a/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php b/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php index 5678be78238ce..7c845b27d03ff 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php +++ b/src/Symfony/Component/Form/Extension/Validator/Event/PreValidateEvent.php @@ -14,7 +14,7 @@ use Symfony\Component\Form\FormEvent; /** - * This event is dispatched before the root form validation. + * This event is dispatched before (root form) validation starts. */ final class PreValidateEvent extends FormEvent { diff --git a/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php b/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php index 0465dc2ca6635..fba2883da3af5 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php +++ b/src/Symfony/Component/Form/Extension/Validator/ValidatorFormEvents.php @@ -17,14 +17,14 @@ final class ValidatorFormEvents { /** - * This event is dispatched before validation starts. + * This event is dispatched before (root form) validation starts. * * @Event("Symfony\Component\Form\Extension\Validator\Event\PreValidateEvent") */ public const PRE_VALIDATE = 'form.pre_validate'; /** - * This event is dispatched after validation completes. + * This event is dispatched after (root form) validation completes. * * @Event("Symfony\Component\Form\Extension\Validator\Event\PostValidateEvent") */ From e23b9d634ecce69824d8240ac7064a85b4ec9350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20ALFAIATE?= Date: Sat, 27 Aug 2022 23:39:24 +0700 Subject: [PATCH 4/4] Remove static property --- .../Validator/EventListener/ValidationListener.php | 10 +++++----- .../Validator/Type/FormTypeValidatorExtension.php | 9 +++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php index b144791c31a78..c77e4b1b28e74 100644 --- a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php @@ -29,7 +29,7 @@ class ValidationListener implements EventSubscriberInterface private ViolationMapperInterface $violationMapper; /** @var FormInterface[][] */ - private static array $dispatchEvents = []; + private array $dispatchEvents = []; public static function getSubscribedEvents(): array { @@ -49,7 +49,7 @@ public function validateForm(FormEvent $event) // Register events to dispatch during (root form) validation foreach (ValidatorFormEvents::ALIASES as $eventName) { if ($form->getConfig()->getEventDispatcher()->hasListeners($eventName)) { - self::$dispatchEvents[$eventName][] = $form; + $this->dispatchEvents[$eventName][] = $form; } } @@ -71,17 +71,17 @@ public function validateForm(FormEvent $event) private function dispatchEvents(string $eventName) { - if (!isset(self::$dispatchEvents[$eventName])) { + if (!isset($this->dispatchEvents[$eventName])) { return; } $event = array_flip(ValidatorFormEvents::ALIASES)[$eventName]; - foreach (self::$dispatchEvents[$eventName] as $form) { + foreach ($this->dispatchEvents[$eventName] as $form) { $event = new $event($form, $form->getData()); $form->getConfig()->getEventDispatcher()->dispatch($event, $eventName); } - unset(self::$dispatchEvents[$eventName]); + unset($this->dispatchEvents[$eventName]); } } diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php index 26653dc9985b0..ada6f901afe7d 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -27,19 +27,16 @@ */ class FormTypeValidatorExtension extends BaseValidatorExtension { - private ValidatorInterface $validator; - private ViolationMapper $violationMapper; - private bool $legacyErrorMessages; + private ValidationListener $validationListener; public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null) { - $this->validator = $validator; - $this->violationMapper = new ViolationMapper($formRenderer, $translator); + $this->validationListener = new ValidationListener($validator, new ViolationMapper($formRenderer, $translator)); } public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper)); + $builder->addEventSubscriber($this->validationListener); } public function configureOptions(OptionsResolver $resolver)