Skip to content

[Form] Introduce validation events for forms #37641

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

Closed
wants to merge 1 commit into from

Conversation

alcaeus
Copy link
Contributor

@alcaeus alcaeus commented Jul 23, 2020

Q A
Branch? master
Bug fix? no
New feature? yes (needs docs changes)
Deprecations? yes (adds a new parameter to the ValidationListener constructor)
Tickets Related RFC: #37597
License MIT
Doc PR symfony/symfony-docs#... (yet to come)

This PR suggests the introduction of two new events related to form handling: preValidate and postValidate. These events are dispatched by the validation listener before starting validation and after validation has completed. They allow adding additional handling to the form submit process, e.g. writing changes from a DTO to a managed entity after validation succeeds. Consider the following DTO:

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Validator\FormValidationEvents;
use Symfony\Component\Form\FormEvents;

final class RenameCategory implements EventSubscriberInterface
{
    public ?string $name;

    private Category $category;

    private function __construct() {}

    public static function fromCategory(Category $category): self
    {
        $instance = new self();
        $instance->category = $category;

        return $instance;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            FormEvents::PRE_SET_DATA => 'initialize',
            FormValidationEvents::POST_VALIDATE => 'apply',
    }

    public function initialize()
    {
        $this->name = $this->category->getName();
    }

    public function apply()
    {
        $this->category->rename($this->name);
    }
}

Now, a controller can create the DTO, pass it to the form, then store the managed entity:

public function renameCategory(Request $request, Category $category)
{
    $renameCategory = RenameCategory::fromCategory($category);

    $form = $this->createForm(EditCategoryForm::class, $renameCategory);
    $form->getConfig()->getEventDispatcher()->addSubscriber($renameCategory);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $entityManager->persist($category);
        $entityManager->flush();
    }

    // ...
}

Of course, this process could be further automated to fit one's needs, but I believe that for the time being, the addition of these events makes is a shortcut towards a better handling of DTOs in forms.

@alcaeus alcaeus requested a review from xabbuh as a code owner July 23, 2020 12:22
* 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
Copy link
Contributor

Choose a reason for hiding this comment

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

just asking, why not

Suggested change
final class PostValidateEvent extends FormEvent
final class PostValidationEvent extends FormEvent

?
same for PreValidateEvent and PreValidationEvent

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You've got a point. I'm not entirely convinced by "validate" either, but I'm open for ideas. Any opinions or objections to the suggested wording?

Copy link
Contributor

Choose a reason for hiding this comment

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

first i see the listener is called validationListener, thus suggest using validationEvent
but then, i saw the method is called validateForm, so maybe the word validate is ok

(i have in mind the workflow event logic, where event are named after the method) :)

@yceruto yceruto added the Form label Jul 23, 2020
@yceruto yceruto added this to the next milestone Jul 23, 2020
@alcaeus alcaeus force-pushed the form-validate-events branch 2 times, most recently from a087bc6 to 6b7549d Compare July 24, 2020 10:06
@alcaeus
Copy link
Contributor Author

alcaeus commented Jul 24, 2020

To the folks more familiar with the event dispatcher than me, please let me know if creating event names is still appropriate, or if the Pre|PostValidateEvent event classes are all the system needs to know.

@alcaeus alcaeus force-pushed the form-validate-events branch from 6b7549d to 9112ef8 Compare July 24, 2020 10:08
Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

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

im tempted to move validation events after submit events, so we avoid POST_SUBMIT with prio > 0 running after PRE/POST_VALIDATE with prio 999 still. But that's a BC break also IIUC.

}

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

is there any reason for these events to be root-only?

Copy link
Contributor Author

@alcaeus alcaeus Jul 24, 2020

Choose a reason for hiding this comment

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

Validation is only triggered for root forms, hence the checks here. The validator then traverses the form and its data.

Copy link
Contributor

@ro0NL ro0NL Jul 24, 2020

Choose a reason for hiding this comment

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

given i have a custom form type listening to VALIDATE events

now, im nesting this form type somewhere else ...all of a sudden the listener isnt called anymore on the child type.

Im not sure that's expected, given validator service maps violations back to underlying form types, so each type has a PRE/POST validation knowledge imho.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the issue is that this is form validation. Form validation is only triggered on root forms. You always have to attach validation listeners to the form that is being validated.

Copy link
Contributor

Choose a reason for hiding this comment

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

form validation bubbles down.

put different, why shouldnt i be able to listen to POST_VALIDATE in a child form to check for some synchronized errors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Form validation does not bubble down. Validation does, in that the form validation metadata contains a Traverse constraint, which triggers validation for fields, child forms, and the entity. The POST_SUBMIT event for any non-root forms does not trigger validation, which is why there should be no form.(pre|post)_validate event

Copy link
Contributor

@ro0NL ro0NL Jul 25, 2020

Choose a reason for hiding this comment

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

hm yes, i agree. Im a bit worried the difference is too subtle, compared to current post-submit with prio >0.

Is there any reason to prefer the new events actually? other then cosmetics.

Choose a reason for hiding this comment

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

I am kind of interested in this discussion - because my Bundle does exactly this. It works quite nicely.

The most important parts for me - and my project - were - it has to bubble down - because I want to be able to run code inside of child forms whenever the whole form was successfully validated.

And inside of the $event variable, you have to be able to access the current form (current child) and the root form. Otherwise, it would not be possible to detect what data to use for some action like uploading an image or persisting an entity etc.

Like I said. My bundle works like a charm. I like to see this inside the Symfony core (form component).
But if these success metrics are not given, I will stick to my bundle ;).

Anyways. I like to see that you guys put effort into this.

@alcaeus
Copy link
Contributor Author

alcaeus commented Jul 24, 2020

As others have suggested on Slack, the fact that pre/post validation events are triggered during post_submit seems weird. However, any changes to this logic would already constitute a BC break:

  • Registering a post_submit handler with priority < 0 makes it a pre_validate event
  • Validation happens as a post_submit handler with priority = 0
  • Registering a post_submit handler with priority > 0 makes it a post_validate event

Triggering validation in a separate step either before or after post_submit makes it impossible for users to rely on validation being done/not being done when their listener is invoked. Thus, we now give people two additional events to listen to validation without needing to know when during the submit step it is done. If you have other suggestions on how to avoid BC breaks, please let me know.

@ro0NL
Copy link
Contributor

ro0NL commented Jul 24, 2020

we could migrate all current post-submit listeners with prio >0 to post_validate, along with a deprecation it should be configured as such.

then in 6.0 post-submit comes free, and as of then it's equivalent to pre_validate instead. Which raises the question if we really need pre-validate.

@fabpot
Copy link
Member

fabpot commented Sep 19, 2020

@xabbuh Do you time to review this PR?

{
$this->validator = $validator;
$this->violationMapper = $violationMapper;

if (!$this->eventDispatcher) {
Copy link

Choose a reason for hiding this comment

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

Shouldn't this be the following?

Suggested change
if (!$this->eventDispatcher) {
if (!$eventDispatcher) {

$this->eventDispatcher is never set at this point.

@d-ph
Copy link

d-ph commented Oct 2, 2020

Gentlemen,

I'd like to discuss another use case for these events -- a use case that is not catered for by this PR, while I think it should be. I'm happy to move it to a new feature request ticket if necessary, however I believe it's best discussed in the context of this PR to avoid potential "change on a recent change".

I second what @ro0NL said in his review here: https://github.com/symfony/symfony/pull/37641/files#r460019018. In other words, the POST_VALIDATE has to be dispatched on all children forms recursively, to be useful and not to introduce confusion that @ro0NL mentioned.

I realise that the validation is triggered on the root form only (which then recursively validates the entire form tree -- this is done for a very good reason), however that's not a reason for not dispatching the events on the entire form tree.

To reiterate what @ro0NL said: given 2 FormTypes: FooType and FooChildType, where FooChildType is one of the child fields on FooType, developers should be able to listen on FormEvent::PRE_VALIDATE and FormEvent::POST_VALIDATE on both of these FormTypes and expect that these events are called on the deepest FormType-first (i.e. FooChildType first and then FooType second) at appropriate times, so that:

  1. Developers can store extra bespoke routines/validation relevant to a given FormType in that FormType.
  2. Developers may run extra bespoke routines/validation on a FormType irrespectively of whether that FormType is being submitted as a root form or not. Example: 2 api REST POST endpoints for a /blog-post and /blog-post/comments. The first endpoint would be submitting a BlogPostType as a root form (with BlogPostCommentType as a child field), and the second endpoint would be submitting a BlogPostCommentType as a root form.

Currently, there's no way to achieve the above (without creating new Constraints/ConstraintValidators) because form validation is initiated only on the root form (again, for a good reason).

What I propose is that instead of doing the following:

// ValidationListener.php

public function validateForm(FormEvent $event)
{
    $form = $event->getForm();

    if ($form->isRoot()) {
        /* ... */

        if ($this->eventDispatcher) {
            $this->eventDispatcher->dispatch(new PostValidateEvent($event->getForm(), $event->getData()), FormValidationEvents::POST_VALIDATE);
        }
    }
}

the following is done:

// ValidationListener.php

public function validateForm(FormEvent $event)
{
    $form = $event->getForm();

    if ($form->isRoot()) {
        /* ... */

        /*
         * Proof-of-concept code. I didn't run and test it. Just trying to get my point across.
         */
        $recursiveFormIterator = new RecursiveIteratorIterator($form, RecursiveIteratorIterator::CHILD_FIRST);
        foreach ($recursiveFormIterator as $iteratedForm) {
            $iteratedForm->getConfig()->getEventDispatcher()->dispatch(new PostValidateEvent($event->getForm(), $event->getData());
        }
    }
}

Benefits of the solution above over the one in the current PR:

  1. No deprecation: event dispatchers are accessed on the iterated form instances.
  2. No confusion: the new form events are run on all forms in the tree. Easier to explain in the Symfony docs, once merged.
  3. The new class FormValidationEvents is redundant. Instead, then new events should sit in the already existing FormEvents class.
  4. No reliance that the ValidatorListener class is using the exact same instance of EventDispatcher as the root Form being validated. I personally find this reliance very dodgy and implementation-dependent.

@alcaeus, please let me know what you think of it when you have time. The solution does solve your use case as well.

Thank you.

@stof
Copy link
Member

stof commented Oct 2, 2020

this will make it even more confusing when explaining the order of post_submit < 0, post_submit > 0 and pre/post_validate, as the order will be different for the root form and for child forms in your proposal...

@d-ph
Copy link

d-ph commented Oct 3, 2020

I agree, stof. In my opinion it is a necessary evil and a natural consequence of the fact that validation is initiated from the root form only. Plus, it is already confusing when a dev discovers that their child form is still not validated when they're attempting to do additional things in a post_submit event (with priority less than 0). I had a solid wtf-moment when I discovered it and I'm sure any dev that attempted to do anything more involved with Symfony Forms can relate to this. By introducing the pre/post-validate events, the framework explicitly says "here's how the things are, stick to these rules and you'll be just fine".

To me it's a matter of including proper explanation in the docs that the order of events in the root form is different than in children forms (root: pre-validate, post-submit (at priority 0), post-validate; children: post-submit (at any priority), pre-validate, post-validate), rather than binning the entire feature because it has rough edges.

Additionally, I'd say that devs will be more likely to use the post-validate event alone, once it introduced, rather than the post-submit because I personally can't think of any other use case a dev might have to do in a post-submit-like event other than "I want to run this additional, complex checks after individual fields have been validated by the framework".

Please re-consider.

@fabpot
Copy link
Member

fabpot commented Oct 7, 2020

@alcaeus @stof What's the next step?

@fabpot fabpot closed this Oct 7, 2020
@fabpot
Copy link
Member

fabpot commented Oct 7, 2020

We've just moved away from master as the main branch to use 5.x instead. Unfortunately, I cannot reopen the PR and change the target branch to 5.x. Can you open a new PR referencing this one to not loose the discussion? Thank you for your understanding and for your help.

@alcaeus
Copy link
Contributor Author

alcaeus commented Oct 8, 2020

Thanks @fabpot! I've recreated this as #38214

@ro0NL and @d-ph, please raise your questions again in the new PR if they persist so we can continue discussing this PR in one spot.

What's the next step?

The new PR is missing a review from maintainers, but good to go otherwise. If the Symfony Team is open to including these events, I'll update the new PR with the rest of the required changes (changelog, deprecations, documentation)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants