Skip to content
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

[FrameworkBundle] Introduce AbstractController::renderForm() instead of handleForm() #41178

Merged
merged 1 commit into from
May 12, 2021

Conversation

lyrixx
Copy link
Member

@lyrixx lyrixx commented May 11, 2021

Q A
Branch? 5.4
Bug fix? no
New feature? yes
Deprecations? yes
Tickets
License MIT
Doc PR -

I'm know I'm a bit late on this once, but I don't really like the handleForm() method:

  1. It uses callable and PHP does not support type hint on callable so it's error prone. While trying the feature I forgot to return a response and I got a fatal error "cannot call getStatusCode() on null". Not really user friendly;
  2. callables receive mixed $data: it's too generic. Static analysis could not work properly and so autocompletion does not work;
  3. This is a new syntax to learn;
  4. All documentation, blog post, etc should be updated, and it's not fixable with sed or similar tool;
  5. This is not really flexible. We are going to lock people with this flow, and they will hesitate to use the "old" syntax when they need more flexibility;

That's why I propose this alternative, which is more simple I guess and addresses issues I leveraged.

I read somewhere that calling isValid() trigger twice the validation logic: This is wrong. The validation occurs during form submitting via an event listener. calling isValid() only check if there is some errors attached to the form.


Usage:

     #[Route('/new', name: 'thing_new', methods: ['GET', 'POST'])]
     public function new(Request $request): Response
     {
         $thing = new Thing();
         $form = $this->createForm(ThingType::class, $thing);
 
         $form->handleRequest($request);
         if ($form->isSubmitted() && $form->isValid()) {
             $entityManager = $this->getDoctrine()->getManager();
             $entityManager->persist($thing);
             $entityManager->flush();
 
             return $this->redirectToRoute('thing_index');
         }
 
-        return $this->render('thing/new.html.twig', [
+        return $this->renderForm('thing/new.html.twig', $form, [
             'thing' => $thing,
             'form' => $form->createView(),
         ]);
     }

@lyrixx lyrixx requested a review from dunglas May 11, 2021 19:28
@carsonbot carsonbot changed the title [FrameworkBundle] Introduce AbstractController::renderForm() [Form][FrameworkBundle] Introduce AbstractController::renderForm() May 11, 2021
@lyrixx lyrixx changed the title [Form][FrameworkBundle] Introduce AbstractController::renderForm() [Form][FrameworkBundle] Introduce AbstractController::renderForm() May 11, 2021
UPGRADE-5.4.md Outdated Show resolved Hide resolved
@nicolas-grekas
Copy link
Member

While trying the feature I forgot to return a response and I got a fatal error "cannot call getStatusCode() on null".

This can be improved. Would you mind sending a PR?

callables receive mixed $data: it's too generic. Static analysis could not work properly;

Use the entity-class as type hint and all works seamslessly.

This is a new syntax to learn;
All documentation, blog post, etc should be updated, and it's not fixable with sed or similar tool;

As everything, this very PR is no exception :)

this is not really flexible. We are going to lock people with this flow

How is that not flexible? Which flow is not covered? Can you give some examples please?

About the proposal in this PR itself, it tights twig and forms together, which means it's relies on having only one form per controller. That is less flexible to me.

handleForm() more flexible to me, and requires less logic. That's why I think it's the correct approach.

@nicolas-grekas
Copy link
Member

I submitted #41181 to improve handleForm()!

@OskarStark
Copy link
Contributor

@lyrixx do we need the $form twice?

-        return $this->render('thing/new.html.twig', [
+        return $this->renderForm('thing/new.html.twig', $form, [
             'thing' => $thing,
             'form' => $form->createView(),
         ]);
     }

I mean 'form' => $form->createView(), .... 🤔

@lyrixx
Copy link
Member Author

lyrixx commented May 12, 2021

@nicolas-grekas

This is a new syntax to learn;
All documentation, blog post, etc should be updated, and it's not fixable with sed or similar tool;

As everything, this very PR is no exception :)

Not really, handleForm() is a new syntax (or a flow) to learn: IE the flow is different, and updating application will be painful! (I hope to see a rector for that if it's kept). renderForm() is instead a new method that is easy to update to. Updating application could be done in minutes with a simple sed command (or similar). It's the same for documentation.

this is not really flexible. We are going to lock people with this flow

How is that not flexible? Which flow is not covered? Can you give some examples please?

Sure. I searched for handleRequest() in some application I have on my computer where the flow is special. So there is client code that I can not comment about, obviously.

some examples

The data is not validated (1) (I don't know why):

$form = $this->formFactory->create(WhitePaperAccessType::class, $whitePaperAccess);
$form->handleRequest($request);

try {
    $whitePaperAccess->setToken($this->tokenGenerator->generateToken());
    $this->entityManager->persist($whitePaperAccess);
    $this->entityManager->flush();
} catch (ORMException $exception) {
    // Ignore and hide.
}

// [...]
return new RedirectResponse(
    $this->router->generate($route, $routeParams)
);

There is a difference between 1/ new form 2/ form submitted 3/ form invalid (looks like XHR handling)

$form = $this->createForm(ContactType::class, $contact)->handleRequest($request);

if (!$form->isSubmitted()) {
    return $this->render('account/_contact_agent.html.twig', [
        'form' => $form->createView(),
        'agent' => $agent,
    ]);
}

if (!$form->isValid()) {
    return $this->json([
        'success' => false,
        'data' => $this->renderView('account/_contact_agent.html.twig', [
            'form' => $form->createView(),
            'agent' => $agent,
        ]),
    ]);
}

//[...]

return $this->json([
    'success' => true,
    'data' => $this->renderView('account/_contact_agent.html.twig', [
        'form' => false,
        'agent' => $agent,
    ]),
]);

There is many form (the proposed solution won't work either but it is easier to adapt)

$monthlyPaymentsForm = $this->createForm(SimulatorMonthlyPaymentsType::class, $monthlyPaymentDto);
$capacityForm = $this->createForm(SimulatorCapacityType::class, $capacityDto);

$monthlyPaymentsForm->handleRequest($request);
$capacityForm->handleRequest($request);

if ($monthlyPaymentsForm->isSubmitted() && $monthlyPaymentsForm->isValid()) {
    $monthlyPayments = $calculator->computeMonthlyPayments($monthlyPaymentDto);
    $amortizationTable = $calculator->computeAmortizationTable($monthlyPaymentDto);
}

if ($capacityForm->isSubmitted() && $capacityForm->isValid()) {
    $capacity = $calculator->computeCapacity($capacityDto);
    $amortizationTable = $calculator->computeAmortizationTable($capacityDto);
}

return $this->render('tools/investment_simulator.html.twig', [
    'monthlyPaymentsForm' => $monthlyPaymentsForm->createView(),
    'capacityForm' => $capacityForm->createView(),

The data is not validated because it's a search:

$originDestinationSearch = new OriginDestinationSearch();
$form = $this->createForm(OriginDestinationType::class, $originDestinationSearch, [
    'validation_groups' => 'None',
    'entry_type' => CitySelectorType::class,
]);

$form->handleRequest($request);

$parameters = $request->query->all();
$parameters['locale'] = $request->getLocale();

if ($originDestinationSearch->isFulfilled() && !$request->isXmlHttpRequest()) {
    return $this->deeplinkAction($request);
} elseif ($originDestinationSearch->isEmpty()) {
    // $data = [...]
} else {
    // $data = [...]
}

$viewData = [
    'form' => $form->createView(),

use submit() instead of handleRequest()

$formRequest = $this->createForm(ReferrerRequestType::class, $referralProgramRequest);

// We don't use $form->handleRequest because we want to always submit the value, whatever the method is
if (!$formRequest->submit($request->query->all())->isValid()) {
    return $this->redirectToRoute('referral_landing');
}

About the proposal in this PR itself, it tights twig and forms together, which means it's relies on having only one form per controller. That is less flexible to me.

I already talk about that in the examples ☝ but how could you use handleForm() with 2 forms? I fail to see :/. Could you show me please? Because according to the doc and the code, handleForm() must return a response, so which response are you supposed to return? How could you do something only if one form is valid and not the other one ? Or both of same? You can say it's the same for renderForm(), and indeed renderForm() could not work with 2 forms, but it's super easy to mimic what is done in the framework to the application and keep a full control of the flow.

handleForm() more flexible to me, and requires less logic. That's why I think it's the correct approach.

I agree it requires less logic but this logic is well know but I did think we can do better but not this way.

I highly disagree handleForm() it's more flexible. Since you force the flow, I fail to see how it could be flexible. Anyway I posted many example where handleForm() could not work whereas renderForm() could. (I could post much more, but I think it's useless :) )

Finally, @dunglas created the initial PR in order to get a "good" status code when the form is submitted and not valid. And we end up with a totally new syntax for handling a form. We are kinda breaking things here. I don't want to be conservative, but (experience from the field) people don't really like when we change things. And it the case: it solves nothing more than setting the right status code.

I'm sorry if I'm pushing hard against handleForm(). If we decide to close this PR I'll be fine, don't worry. But I think it would be a shame.


@OskarStark

@lyrixx do we need the $form twice?

I thought about that too. I saw quite often in application that the form variable passed to twig is not named form.
We could indeed create the view for the user, and give it to the view but if people want to change the var name ?

maybe something like this :

return $this->renderForm('thing/new.html.twig', $form); // a "form" var is passed to the view
return $this->renderForm('thing/new.html.twig', $form, "another_variable_name"); // a "another_variable_name" var is passed to the view

WDYT?

nicolas-grekas added a commit that referenced this pull request May 12, 2021
…() (nicolas-grekas)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[FrameworkBundle] improve AbstractController::handleForm()

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | (
| License       | MIT
| Doc PR        | -

Related to #41178

Commits
-------

7c69682 [FrameworkBundle] improve AbstractController::handleForm()
@nicolas-grekas
Copy link
Member

how could you use handleForm() with 2 forms [...] handleForm() must return a response

🤔 you're right! #41184 might help :)

@dunglas created the initial PR in order to get a "good" status code when the form is submitted and not valid

I agree that renderForm() solves that and reduces the diff.

Thanks for the examples. While not covering 100% of use cases, handleForm() has more use cases than renderForm() to me, especially after #41184! (this PR already triggered some changes, so thanks for it,whatever the outcome).

But I get your arguments. I'd be happy to see what others think about this proposal.

@nicolas-grekas
Copy link
Member

(my progress of thoughts: if handleForm() has value but renderForm() provides a smoother transition, we might replace handleForm() by renderForm() in 5.3, and reconsider handleForm() or a variant of it for 5.4)

@lyrixx
Copy link
Member Author

lyrixx commented May 12, 2021

I have removed handleForm() for now. CF previous comment. (thanks @nicolas-grekas)

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

This PR basically reverts #40799, which had some arguments that you answered I think.
The patch basically restores #39843, which we could borrow some more code from?

@nicolas-grekas nicolas-grekas changed the title [Form][FrameworkBundle] Introduce AbstractController::renderForm() [Form][FrameworkBundle] Introduce AbstractController::renderForm() instead of handleForm() May 12, 2021
@carsonbot carsonbot changed the title [Form][FrameworkBundle] Introduce AbstractController::renderForm() instead of handleForm() [FrameworkBundle] Introduce AbstractController::renderForm() instead of handleForm() May 12, 2021
@wouterj
Copy link
Member

wouterj commented May 12, 2021

But I get your arguments. I'd be happy to see what others think about this proposal.

Disclaimer: I have very limited knowledge about Symfony forms, from both personal experience and general user experience.

To me, the limited diff of this proposal between "using forms in controllers that don't extend AbstractController" vs "using the AbstractController form helper functions" is a big win over #40799 (users will have to learn less to be able to write complex or default form handling).

@wouterj
Copy link
Member

wouterj commented May 12, 2021

What we should think about is the number of alternatives in 5.3. Especially in documentation, you don't want to have choices without clear pros/cons. So if we keep both renderForm(), handleForm() and the pre-5.3 way, we should have very clear pros/cons on when to use each of them imho (otherwise, it's probably wise to remove/deprecate on of them).

edit: I just saw that this PR removes handleForm(). The previously merged changes to that method made me believe we kept both. All OK :)

Copy link
Member

@fabpot fabpot left a comment

Choose a reason for hiding this comment

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

Thank you for the discussion @lyrixx. I agree with most of the things you say and I think you're right about having a simpler helper method. Even if it's late in the process, I'm happy to merge it now and give us more time to revisit this issue for 5.4.

@fabpot
Copy link
Member

fabpot commented May 12, 2021

Thank you @lyrixx.

@nicolas-grekas
Copy link
Member

The blogpost needs an update :)
https://symfony.com/blog/new-in-symfony-5-3-form-handler-helper
/cc @javiereguiluz

@Warxcell
Copy link
Contributor

Warxcell commented May 12, 2021

What if have 2 forms? Isn't better like that:

return $this->renderForm('conference/edit.html.twig', ['form' => $form]);
That will simplify the signature because string $formVar = 'form' will be removed, and it will make more obvious the var name of form?

@nicolas-grekas
Copy link
Member

nicolas-grekas commented May 12, 2021

@Warxcell but the code needs to get access to the $form object in the body of the method. How would it cherry-pick it in the array of $parameters?

@Warxcell
Copy link
Contributor

@Warxcell but the code needs to get access to the $form object in the body of the method. How would it cherry-pick it in the array of $parameters?

Parameters is the 3rd parameter. 2nd parameter will be array of forms only.

@lyrixx
Copy link
Member Author

lyrixx commented May 12, 2021

IMHO, if you have N form (not so commun), you should fallback to the previous method and write the code by hand.

@Warxcell
Copy link
Contributor

Warxcell commented May 12, 2021

Typical use-case for multiple form is "Profile" section, where you have 1 form for name and other simple fields and 1 form just for password changing, 1 form for phone changing, etc.
Even Symfony website uses that use-case:
image

@nicolas-grekas
Copy link
Member

WDYT of #41190?

@lyrixx
Copy link
Member Author

lyrixx commented May 12, 2021

@Warxcell haha, it's funny you quote SymfonyConnect because I this feature! And as far as I remember, I did not use many forms in the same action, instead I used many render(controller()) (or ESI) 👼🏼

nicolas-grekas added a commit that referenced this pull request May 12, 2021
…() (nicolas-grekas)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[FrameworkBundle] improve AbstractController::renderForm()

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Even better than #41178, this requires a simple change on apps, and is compatible with multiple forms.

Usage:
```diff
-        return $this->render('thing/new.html.twig', [
+        return $this->renderForm('thing/new.html.twig', [
             'thing' => $thing,
-            'form' => $form->createView(),
+            'form' => $form,
         ]);
```

In 5.4, we could even deprecate passing a FormView to render() so that we can always set the 422.

Commits
-------

e244d31 [FrameworkBundle] improve AbstractController::renderForm()
@fabpot fabpot mentioned this pull request May 12, 2021
@zmitic
Copy link

zmitic commented May 12, 2021

Wouldn't this trigger validation twice?
Once in AbstractController::renderForm and once in controller itself:

$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {  // first validation

    // do something but DO NOT redirect      
}

// always render the form; triggers validation again
return $this->renderForm('thing/new.html.twig', $form, [
]);

The above is from real use-case I have in turbo+stimulus app. Users can edit some form, but when valid, they get Bootstrap alert and stay on the same page.


Updated

My mistake; I do redirect but to same page. Sorry.

@jvasseur
Copy link
Contributor

jvasseur commented May 12, 2021

@zmitic it's in the PR description:

I read somewhere that calling isValid() trigger twice the validation logic: This is wrong. The validation occurs during form submitting via an event listener. calling isValid() only check if there is some errors attached to the form.

The validation is donne in handleRequest, not in the isValid method.

@lyrixx
Copy link
Member Author

lyrixx commented May 12, 2021

@jvasseur That's what I said, yes 👍🏼 (don't forget that when you comment on a PR, everyone that participate to it receive a notification - And yes, I spamming everyone too :( sorry)

@TomasVotruba
Copy link
Contributor

Oh, I was about to use handleForm() in my projects... now I'm bit confused it got promoted as done feature and now removed. What other promised features might be gone in 5.3?

Nette is using similar callable approach since ~2010 and it saves so much repeatead code that we have to obey in Symfony. I hoped Symfony can finally get rid of that boiler plate and lower risk for bugs while improving DX.

@lyrixx
Copy link
Member Author

lyrixx commented May 17, 2021

@TomasVotruba I'm sorry about this very late change. The renderForm() method will save some bit of code, and we might find something event better for Symfony 5.4.

What other promised features might be gone in 5.3?

This change was exceptional, and not other promised feature will be gone in 5.3.

I hope you understand

@TomasVotruba
Copy link
Contributor

I see.

I wish this would happen on internal level before communicating to the public.
Now it seems there are 2 core Symfony devs who remove mutual features, both good and useful.

@nicolas-grekas
Copy link
Member

But nothing is internal in the dev process of Symfony.
That's because we get feedback from the community that things improve. Even if the feedback comes as a comment on a blog post.

@TomasVotruba
Copy link
Contributor

TomasVotruba commented May 17, 2021

Could you link the feedback? I wish I was there too to avoid this :D

@wouterj
Copy link
Member

wouterj commented May 17, 2021

@TomasVotruba part of the feedback is this PR (note that this PR, like all non-CVE PRs, is not created by "the core team" but by @lyrixx as "Symfony contributor").

Some other feedback can be found in the comments on this article: https://symfony.com/blog/new-in-symfony-5-3-form-handler-helper

@TomasVotruba
Copy link
Contributor

@wouterj Thanks 👍
I've read this post and remember positive reactions. Now I can see only "pushing this forward" and "right direction" feedback. Not removing the method completely, that's why I'm surprised it get removed completely.

I'll try to write positive feedback on posts I like, maybe it helps next time :)

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.