SymfonyWorld Online 2021 Winter Edition December 9 – 10, 2021 100% Online +20 talks and workshops

New in Symfony 5.3: Form Handler Helper

Symfony 5.3 is backed by JoliCode. JoliCode is a team of passionate developers and open-source lovers, with a strong expertise in PHP & Symfony technologies. They can help you build your projects using state-of-the-art practices.

Contributed by
Grégoire Pineau and Nicolas Grekas in #41178 and #41190.

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).

Help the Symfony project!

As with any Open-Source project, contributing code or documentation is the most common way to help, but we also have a wide range of sponsoring opportunities.

Comments

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;

return $this->render($template, array_merge($params, ['form' => $form->createView()]), new Response(null, $code));
}

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

Comments are closed.

To ensure that comments stay relevant, they are closed for old posts.