Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Form] Introduce validation events #47210

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 4 commits 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
1 change: 1 addition & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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'];
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the goal of this new event but the dispatching moment is a bit confusing to me. If I create a listener in a nested form type, I expect the event to be received just before the current form is validated in FormValidator and not before the whole validation process of all forms in the tree is started.

The meaning is different in this case from other events.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistant with POST_VALIDATE event, I decided to dispatch it before the validation of the entire form start.
(see my other comment to know why POST_VALIDATE is dispatched after all form items are validated)


// 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
Expand All @@ -50,6 +64,24 @@ public function validateForm(FormEvent $event)

$this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized);
}

$this->dispatchEvents(ValidatorFormEvents::POST_VALIDATE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that child forms can check easily whether their parent is valid or not, but I have the same confusion about the right moment this event is being dispatched. They are not done just after the current form is validated but when the whole validation process is finished for all forms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main purpose of this PR, to be able to know if the entire form is valid or not.
That's why the POST_VALIDATE event must be dispatched here.
If we dispatch the event just after the validation of the current item, we will only be able to know if the current item (and his parents) are valid.

Maybe we can introduce a 3rd event "VALIDATE" that could be dispatched just after the current form item is validated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or rename PRE/POST events by START/FINISH to avoid confusion?

}
}

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]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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()
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down