-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FrameworkBundle] Add AbstractController::handleForm() helper #40799
Conversation
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
I think we should make the persistence part optional |
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
Outdated
Show resolved
Hide resolved
2a83b57
to
89dff58
Compare
Here is a new closure-based proposal based on the feedback in this PR: #[Route('/{id}/edit', name: 'conference_edit', methods: ['GET', 'POST'])]
public function edit(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
to
ccb9694
Compare
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
to
e25a637
Compare
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 |
Based on #40799 (comment) I'd like to suggest this changes: #[Route('/{id}/edit', name: 'conference_edit', methods: ['GET', 'POST'])]
public function edit(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? |
@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. |
It doesn't when Conference is created with Full example to create 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'])]
public function create(Request $request): Response
{
return $this->handleForm(
$request,
$this->createForm(ConferenceType::class), // null for $data
function (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
Outdated
Show resolved
Hide resolved
Oh btw, I'd be fine also with @zmitic's proposal to pass the form's data as 2nd argument to the closures. |
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? |
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. |
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.
@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 UserType extends 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 |
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. |
But the data passed to the closure could be |
If the form is valid, it means that value will not 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. |
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. |
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 |
In my opinion it returns |
Exactly, my stubs cover FormInterface, AbstractType and others; I was super-careful to not make any problems, took me lots of time to make them. Even had to annoy muglug many times vimeo/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, we know it is not null, no need for |
Ok got it. This makes sense. +1 for me. Thanks for taking the time to explain this in depth @zmitic! |
@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 |
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
to
5228546
Compare
Thank you @dunglas. |
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: