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..dc0823f239646 --- /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 (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 new file mode 100644 index 0000000000000..7c845b27d03ff --- /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 (root form) validation starts. + */ +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..c77e4b1b28e74 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 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)) { + $this->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($this->dispatchEvents[$eventName])) { + return; } + + $event = array_flip(ValidatorFormEvents::ALIASES)[$eventName]; + + foreach ($this->dispatchEvents[$eventName] as $form) { + $event = new $event($form, $form->getData()); + $form->getConfig()->getEventDispatcher()->dispatch($event, $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) 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..fba2883da3af5 --- /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 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 (root form) 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