Skip to content

Commit

Permalink
feature #39843 [FrameworkBundle] Add renderForm() helper setting the …
Browse files Browse the repository at this point in the history
…appropriate HTTP status code (dunglas)

This PR was squashed before being merged into the 5.3-dev branch.

Discussion
----------

[FrameworkBundle] Add renderForm() helper setting the appropriate HTTP status code

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | n/a
| License       | MIT
| Doc PR        | todo

A 422 HTTP status code should be returned after the submission of an invalid form. Some libraries including [Turbo](hotwired/turbo#39) rely on this behavior and will not display the updated form (containing errors) unless this status code is present.

Rails also [recently switched to this behavior ](rails/rails#41026) by default for the same reason.

I propose to introduce a new helper method rendering the form and setting the appropriate status code. It makes the code cleaner:

```php
// src/Controller/TaskController.php

// ...
use Symfony\Component\HttpFoundation\Request;

class TaskController extends AbstractController
{
    public function new(Request $request): Response
    {
        $task = new Task();
        $form = $this->createForm(TaskType::class, $task);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $task = $form->getData();
            // ...

            return $this->redirectToRoute('task_success');
        }

        return $this->renderForm('task/new.html.twig', $form);
    }
}
```

Commits
-------

4c77e50 [FrameworkBundle] Add renderForm() helper setting the appropriate HTTP status code
  • Loading branch information
fabpot committed Jan 17, 2021
2 parents 93e853d + 4c77e50 commit fa87194
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
5.3
---

* Added `AbstractController::renderForm()` to render a form and set the appropriate HTTP status code
* Added support for configuring PHP error level to log levels
* Added the `dispatcher` option to `debug:event-dispatcher`
* Added the `event_dispatcher.dispatcher` tag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
Expand Down Expand Up @@ -289,6 +290,22 @@ protected function stream(string $view, array $parameters = [], StreamedResponse
return $response;
}

/**
* Renders a form.
*
* The FormView instance is passed to the template in a variable named "form".
* If the form is invalid, a 422 status code is returned.
*/
public function renderForm(string $view, FormInterface $form, array $parameters = [], Response $response = null): Response
{
$response = $this->render($view, ['form' => $form->createView()] + $parameters, $response);
if ($form->isSubmitted() && !$form->isValid()) {
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
}

return $response;
}

/**
* Returns a NotFoundHttpException.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -31,6 +33,7 @@
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\WebLink\Link;
use Twig\Environment;

class AbstractControllerTest extends TestCase
{
Expand Down Expand Up @@ -371,7 +374,7 @@ public function testdenyAccessUnlessGranted()

public function testRenderViewTwig()
{
$twig = $this->getMockBuilder(\Twig\Environment::class)->disableOriginalConstructor()->getMock();
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig->expects($this->once())->method('render')->willReturn('bar');

$container = new Container();
Expand All @@ -385,7 +388,7 @@ public function testRenderViewTwig()

public function testRenderTwig()
{
$twig = $this->getMockBuilder(\Twig\Environment::class)->disableOriginalConstructor()->getMock();
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig->expects($this->once())->method('render')->willReturn('bar');

$container = new Container();
Expand All @@ -399,7 +402,7 @@ public function testRenderTwig()

public function testStreamTwig()
{
$twig = $this->getMockBuilder(\Twig\Environment::class)->disableOriginalConstructor()->getMock();
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();

$container = new Container();
$container->set('twig', $twig);
Expand All @@ -410,6 +413,52 @@ public function testStreamTwig()
$this->assertInstanceOf(\Symfony\Component\HttpFoundation\StreamedResponse::class, $controller->stream('foo'));
}

public function testRenderFormTwig()
{
$formView = new FormView();

$form = $this->getMockBuilder(FormInterface::class)->getMock();
$form->expects($this->once())->method('createView')->willReturn($formView);

$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig->expects($this->once())->method('render')->with('foo', ['form' => $formView, 'bar' => 'bar'])->willReturn('bar');

$container = new Container();
$container->set('twig', $twig);

$controller = $this->createController();
$controller->setContainer($container);

$response = $controller->renderForm('foo', $form, ['bar' => 'bar']);

$this->assertTrue($response->isSuccessful());
$this->assertSame('bar', $response->getContent());
}

public function testRenderInvalidFormTwig()
{
$formView = new FormView();

$form = $this->getMockBuilder(FormInterface::class)->getMock();
$form->expects($this->once())->method('createView')->willReturn($formView);
$form->expects($this->once())->method('isSubmitted')->willReturn(true);
$form->expects($this->once())->method('isValid')->willReturn(false);

$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
$twig->expects($this->once())->method('render')->with('foo', ['form' => $formView, 'bar' => 'bar'])->willReturn('bar');

$container = new Container();
$container->set('twig', $twig);

$controller = $this->createController();
$controller->setContainer($container);

$response = $controller->renderForm('foo', $form, ['bar' => 'bar']);

$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
$this->assertSame('bar', $response->getContent());
}

public function testRedirectToRoute()
{
$router = $this->getMockBuilder(\Symfony\Component\Routing\RouterInterface::class)->getMock();
Expand Down

0 comments on commit fa87194

Please sign in to comment.