The recommended way of processing Symfony forms is to use a single action for both rendering the form and handling the form submit.
This is how it looks in practice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/ConferenceController.php
// ...
#[Route('/{id}/edit', name: 'conference_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Conference $conference): Response
{
$form = $this->createForm(ConferenceType::class, $conference);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// do something with the $conference object
// (e.g. persist it in the database)
return $this->redirectToRoute('conference_show', [
'id' => $conference->getId(),
]);
}
return $this->render('conference/edit.html.twig', [
'form' => $form->createView(),
]);
}
When using libraries such as Symfony UX Turbo this simple form handling is not enough and you have to follow the HTTP protocol strictly (e.g. if the form is submitted but invalid, the response must have a HTTP 422 status code).
In order to simplify the form handling in those cases, Symfony 5.3 adds a
new (optional) helper to render forms. This helper is defined in the
AbstractController base controller as a new method called renderForm()
.
This is how the last lines of the previous example should be written when using the new helper:
1 2 3 4 5 6 7
// src/Controller/ConferenceController.php
// ...
return $this->renderForm('conference/edit.html.twig', [
'form' => $form,
]);
}
The signature of the renderForm()
method is the same as for render()
:
1 2 3 4 5
renderForm(
string $view,
array $parameters = [],
Response $response = null
): Response
This method renders the given form (it calls $form->createView()
internally)
and sets the 422
status code when the form is submitted and invalid.
The $parameters
argument is the list of variables passed to the Twig
template and the optional $response
object allows you to configure certain
properties of the returned response (e.g. its cache options).
Another nice option would be to add Form::isInvalid() { $this->isSubmitted() && $this->isValid(); } — so we can then do return $this->render(..., $form->isInvalid() ? 422 : 200);
Or even better: Form::getHttpCode() which will return the recommended HTTP code of the response. 303 on success if method is post, 200 if not submitted, 422 if submitted with error... then just $this->render(..., $form->getHttpCode()); // :)
It's nice to improve form handling but IMHO this new way is actually taking more lines that previous one to do the same job
A step in the good direction!
I like this better than the old way of rendering forms, which my brain never managed to wrap around. In my mind the workflow is 1. render form, 2. handle submit and render response (either valid or invalid). The fact that "invalid response" looks generally very similar to the pristine form is of accidental nature and not core to the design of the API.
I would see as improvement anything that makes handling of invalid forms more explicit, such as the suggestions from Josef Kufner, or maybe adding to the signature of
handleForm
an optional callable $onIvalid, eg:handleForm(FormInterface $form, Request $request, callable $render, callable $onSuccess, callable $onInvalid = null)
I like Josef Kufner's solution, but I hink we can push it further:
protected function renderForm(string $template, FormInterface $form, array $params = []): Response { $code = $form->isSubmitted() && !$form->isValid() ? Response::HTTP_UNPROCESSABLE_ENTITY : Response::HTTP_OK;
}
or see this gist for a nicer view https://gist.github.com/garak/fbc48d46e5c6226fe0fe19597f5a5293
@Guillaume Sainthillier it's not more code if you handle HTTP status codes properly. Take a look at this example to see how cumbersome it is to send the proper status codes without this helper: https://github.com/symfony/ux/blob/0a6ebad4bc67f74ba3bbb52f6586085ddcd28ab1/src/Turbo/README.md#forms
@Massimiliano Arione we did something similar initially, but this triggers the validation logic two times, and that hurts performance. Also, it makes it harder to pass custom arguments to the view, and to hook custom logic.
Great functionality, thanks! If it is interesting, I developed a bundle which works with the same principle and which allows to decouple the logic after submission of the form in a handler instead of leaving it in the controller. So the controller only does its job: to receive a request and send back a response. See it here : https://github.com/Digivia/form-handler
This feature has been replaced by a new renderForm() method, see here for details:
https://github.com/symfony/symfony/pull/41178
The blog post has been updated to use the new renderForm() helper.
I think the last update introduced a cumbersome way to check for form view among parameters, with a foreach. I left a more detailed comment on PR: https://github.com/symfony/symfony/pull/41190/files#r631630952