Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.7k
[FrameworkBundle] Add AbstractController::handleForm() helper#40799
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
chalasr commentedApr 13, 2021
I think we should make the persistence part optional |
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
2a83b57 to89dff58Comparedunglas commentedApr 13, 2021
Here is a new closure-based proposal based on the feedback in this PR: #[Route('/{id}/edit', name:'conference_edit', methods: ['GET','POST'])]publicfunctionedit(Request$request,Conference$conference):Response {return$this->handleForm($request,$this->createForm(ConferenceType::class,$conference),function (FormInterface$form)use ($conference) {$this->getDoctrine()->getManager()->flush();return$this->redirectToRoute('conference_show', ['id' =>$conference->getId()], Response::HTTP_SEE_OTHER ); },function (FormInterface$form) {return$this->render('conference/edit.html.twig', ['form' =>$form->createView(), ]); } ); } |
b23fe89 toccb9694Compare
nicolas-grekas left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I like this iteration :)
ccb9694 toe25a637Comparezmitic commentedApr 13, 2021
Question: wouldn't it be better if Reasons:
$this->createForm(ConferenceType::class)// no $data Otherwise we have to write this: function (FormInterface$form) {/** @var Conference $conference */$conference =$form->getData();// 2 extra lines needed, with @var as tricking SA tools// ...}, instead of simpler: function (FormInterface$form,Conference$conference) {// ...},
Similar for |
plandolt commentedApr 13, 2021
Based on#40799 (comment) I'd like to suggest this changes: #[Route('/{id}/edit', name:'conference_edit', methods: ['GET','POST'])]publicfunctionedit(Request$request,Conference$conference):Response {return$this->handleForm( FormHandler::create($request) ->withFormType(ConferenceType::class) ->withFormData($conference) ->withHandleRequestCallback(function (FormInterface$form)use ($conference) {$this->getDoctrine()->getManager()->flush();return$this->redirectToRoute('conference_show', ['id' =>$conference->getId()], Response::HTTP_SEE_OTHER ); }) ->withCallback(function (FormInterface$form) {return$this->render('conference/edit.html.twig', ['form' =>$form->createView(), ]); }) ; ); }
WDYT? |
nicolas-grekas commentedApr 13, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@zmitic this preserves the type info and works already: @scuben I'd be 👎 on my side: this would require a pile of code for something that arguments already provide. |
zmitic commentedApr 13, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
It doesn't when Conference is created with Full example tocreate entity from data in form:https://symfony.com/doc/current/form/use_empty_data.html#option-2-provide-a-closure #[Route('/create', name:'conference_create', methods: ['GET','POST'])]publicfunctioncreate(Request$request):Response {return$this->handleForm($request,$this->createForm(ConferenceType::class),// null for $datafunction (FormInterface$form) {/** @var Conference $conference */$conference =$form->getData();// extra 2 lines, one used to trick static analysis// the rest },function (FormInterface$form) {return$this->render('conference/edit.html.twig', ['form' =>$form->createView(), ]); } ); } UPDATED: Most likely users will use one file for both function (FormInterface$form, ?Conference$conference) {return$this->render('conference/form.html.twig', ['form' =>$form->createView(),// changing page title via reusable ``form.html.twig``'page_title' =>$conference ?'Editing' .$conference->getName() :'Create new conference', ]);} |
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
nicolas-grekas commentedApr 14, 2021
Oh btw, I'd be fine also with@zmitic's proposal to pass the form's data as 2nd argument to the closures. |
bOmBeLq commentedApr 15, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Would service instead of method in controller class be considerable? could even get request form request stack in service or would it be bad practice? |
dunglas commentedApr 16, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I swapped the Regarding@zmitic's proposal, it will not be necessary anymore with#40783 (I described why here:#40783 (comment)) so I suggest keeping the code simple for now and to not add a new argument. Regarding the service, I don't think it is worth it yet. It's just a tiny helper method. I think that this PR is now ready to be merged. |
chalasr left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Once the corresponding changelog entry is updated.
zmitic commentedApr 16, 2021
@dunglas Is it possible to rediscuss this decision? Template for If there was a method like I already made stubs for almost entire form component,including FormInterface. Current usage that allows psalm to know the type: /** @extends AbstractType<User> */class UserTypeextends AbstractType{} Real example from my current project, relevant parts are selected: Even events are covered, example for PRE_SET_DATA: All of them depends on |
dunglas commentedApr 16, 2021
zmitic commentedApr 16, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
It won't for forms. Let me refer to this:
My argument wasn't about autocompletion but about static analysis. With LSP+psalm, I already have autocompletion using generics: Notice that there is no I use psalm on level 1 for almost two years so I am ready to help if needed. I am not skilled enough to contribute to Symfony core itself but for this idea, I am 99% sure. Let me know if you need more examples. |
dunglas commentedApr 16, 2021
But the data passed to the closure could be |
zmitic commentedApr 16, 2021
If the form is valid, it means that value willnot be null so users will typehint And Unlike Side note: However LSP works great. It does have few bugs but once you learn how to avoid them, it is absolute joy to work with generics and having autocomplete for them. |
zmitic commentedApr 16, 2021
Let me explain with example. The code in if ($submitted &&$form->isValid()) {return$onSuccess($form);} what I am proposing is this: if ($submitted &&$form->isValid() &&$data =$form->getData()) {return$onSuccess($form,$data);} with this change, we could write code like this: $this->handleForm( onSuccess:function (FormInterface$form,Conference$conference) {// we can render form, and we KNOW that $conference is an entity, NOT null }); but without that second parameter: $this->handleForm( onSuccess:function (FormInterface$form) {/** @var Conference $conference */$conference =$form->getData(); }); The second example requires 2 extra lines. And that I hope it is more clear now, if not, I will provide more. |
dunglas commentedApr 16, 2021
Here is what the interface says: AFAIU it's not guaranteed by the interface that a I've no strong opinion on this TBH. If we pass the data object as a second parameter, then we should also clarify the interface to make it clear that |
Neirda24 commentedApr 16, 2021
In my opinion it returns |
zmitic commentedApr 16, 2021
Exactly, my stubs cover FormInterface, AbstractType and others; I was super-careful to not makeany problems, took me lots of time to make them. Even had to annoy muglug many timesvimeo/psalm#4136 😄 Typehint
This is also one of the reasons why I ask for this change. If we could use $this->handleForm( onSuccess:function (FormInterface$form,Conference$conference) {// we can render form, and we KNOW that $conference is an entity, NOT null }); there would never be any confusion; at this point, weknow it is not null, no need for |
dunglas commentedApr 16, 2021
Ok got it. This makes sense. +1 for me. Thanks for taking the time to explain this in depth@zmitic! |
zmitic commentedApr 16, 2021
@dunglas Glad I could help and sorry if I didn't explain it simpler. Once merged, I will add stubs for Example would be using |
yceruto left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
LGTM, thanks!
3db80ca to5228546Comparenicolas-grekas commentedApr 16, 2021
Thank you@dunglas. |



Uh oh!
There was an error while loading.Please reload this page.
Some libraries such as Turbo require to strictly follow the HTTP specification (and especially to use proper status codes) to deal with forms.
In#39843, I introduced a new
renderForm()helper for this purpose. But I'm not very satisfied by it. The current approach has several problems:$form->isValid()two times, which may hurt performanceThis PR proposes an alternative helper,
handleForm(), which handles automatically 80% of the use cases, provide an extension point for the other 20%, and can also serve as a quick example for users to handle form in a custom way (by control-clicking on the function to see the code and copy/paste/adapt it).Before (standard case):
With the new helper:
Before (more advanced use case):
With the new helper (more advanced case):
This also works without named parameters. I also considered passing a callback to be executed on success, but I'm happier with the current solution.
WDYT?
TODO: