Skip to content

Commit b15f8ae

Browse files
[TwigBridge] Add #[Template()] to describe how to render arrays returned by controllers
1 parent 28715c0 commit b15f8ae

File tree

6 files changed

+215
-2
lines changed

6 files changed

+215
-2
lines changed

Attribute/Template.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
15+
class Template
16+
{
17+
public function __construct(
18+
/**
19+
* The name of the template to render.
20+
*/
21+
public string $template,
22+
23+
/**
24+
* The controller method arguments to pass to the template.
25+
*/
26+
public ?array $vars = null,
27+
28+
/**
29+
* Enables streaming the template.
30+
*/
31+
public bool $stream = false,
32+
) {
33+
}
34+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `form_label_content` and `form_help_content` block to form themes
8+
* Add `#[Template()]` to describe how to render arrays returned by controllers
89

910
6.1
1011
---
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\EventListener;
13+
14+
use Symfony\Bridge\Twig\Attribute\Template;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\Form\FormInterface;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpFoundation\StreamedResponse;
19+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
20+
use Symfony\Component\HttpKernel\Event\ViewEvent;
21+
use Symfony\Component\HttpKernel\KernelEvents;
22+
use Twig\Environment;
23+
24+
class TemplateAttributeListener implements EventSubscriberInterface
25+
{
26+
public function __construct(
27+
private Environment $twig,
28+
) {
29+
}
30+
31+
public function onKernelView(ViewEvent $event)
32+
{
33+
$parameters = $event->getControllerResult();
34+
35+
if (!\is_array($parameters ?? [])) {
36+
return;
37+
}
38+
$attribute = $event->getRequest()->attributes->get('_template');
39+
40+
if (!$attribute instanceof Template && !$attribute = $event->controllerArgumentsEvent?->getAttributes()[Template::class][0] ?? null) {
41+
return;
42+
}
43+
44+
$parameters ??= $this->resolveParameters($event->controllerArgumentsEvent, $attribute->vars);
45+
$status = 200;
46+
47+
foreach ($parameters as $k => $v) {
48+
if (!$v instanceof FormInterface) {
49+
continue;
50+
}
51+
if ($v->isSubmitted() && !$v->isValid()) {
52+
$status = 422;
53+
}
54+
$parameters[$k] = $v->createView();
55+
}
56+
57+
$event->setResponse($attribute->stream
58+
? new StreamedResponse(fn () => $this->twig->display($attribute->template, $parameters), $status)
59+
: new Response($this->twig->render($attribute->template, $parameters), $status)
60+
);
61+
}
62+
63+
public static function getSubscribedEvents(): array
64+
{
65+
return [
66+
KernelEvents::VIEW => ['onKernelView', -128],
67+
];
68+
}
69+
70+
private function resolveParameters(ControllerArgumentsEvent $event, ?array $vars): array
71+
{
72+
if ([] === $vars) {
73+
return [];
74+
}
75+
76+
$parameters = $event->getNamedArguments();
77+
78+
if (null !== $vars) {
79+
$parameters = array_intersect_key($parameters, array_flip($vars));
80+
}
81+
82+
return $parameters;
83+
}
84+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Tests\EventListener;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\Twig\EventListener\TemplateAttributeListener;
16+
use Symfony\Bridge\Twig\Tests\Fixtures\TemplateAttributeController;
17+
use Symfony\Component\Form\FormInterface;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
20+
use Symfony\Component\HttpKernel\Event\ViewEvent;
21+
use Symfony\Component\HttpKernel\HttpKernelInterface;
22+
use Twig\Environment;
23+
24+
class TemplateAttributeListenerTest extends TestCase
25+
{
26+
public function testAttribute()
27+
{
28+
$twig = $this->createMock(Environment::class);
29+
$twig->expects($this->exactly(2))
30+
->method('render')
31+
->withConsecutive(
32+
['templates/foo.html.twig', ['foo' => 'bar']],
33+
['templates/foo.html.twig', ['bar' => 'Bar', 'buz' => 'def']]
34+
)
35+
->willReturn('Bar');
36+
37+
$request = new Request();
38+
$kernel = $this->createMock(HttpKernelInterface::class);
39+
$controllerArgumentsEvent = new ControllerArgumentsEvent($kernel, [new TemplateAttributeController(), 'foo'], ['Bar'], $request, null);
40+
$listener = new TemplateAttributeListener($twig);
41+
42+
$event = new ViewEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, ['foo' => 'bar'], $controllerArgumentsEvent);
43+
$listener->onKernelView($event);
44+
$this->assertSame('Bar', $event->getResponse()->getContent());
45+
46+
$event = new ViewEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, null, $controllerArgumentsEvent);
47+
$listener->onKernelView($event);
48+
$this->assertSame('Bar', $event->getResponse()->getContent());
49+
50+
$event = new ViewEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, null);
51+
$listener->onKernelView($event);
52+
$this->assertNull($event->getResponse());
53+
}
54+
55+
public function testForm()
56+
{
57+
$request = new Request();
58+
$kernel = $this->createMock(HttpKernelInterface::class);
59+
$controllerArgumentsEvent = new ControllerArgumentsEvent($kernel, [new TemplateAttributeController(), 'foo'], [], $request, null);
60+
$listener = new TemplateAttributeListener($this->createMock(Environment::class));
61+
62+
$form = $this->createMock(FormInterface::class);
63+
$form->expects($this->once())->method('createView');
64+
$form->expects($this->once())->method('isSubmitted')->willReturn(true);
65+
$form->expects($this->once())->method('isValid')->willReturn(false);
66+
67+
$event = new ViewEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, ['bar' => $form], $controllerArgumentsEvent);
68+
$listener->onKernelView($event);
69+
70+
$this->assertSame(422, $event->getResponse()->getStatusCode());
71+
}
72+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Tests\Fixtures;
13+
14+
use Symfony\Bridge\Twig\Attribute\Template;
15+
16+
class TemplateAttributeController
17+
{
18+
#[Template('templates/foo.html.twig', vars: ['bar', 'buz'])]
19+
public function foo($bar, $baz = 'abc', $buz = 'def')
20+
{
21+
}
22+
}

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"symfony/form": "^6.1",
3131
"symfony/html-sanitizer": "^6.1",
3232
"symfony/http-foundation": "^5.4|^6.0",
33-
"symfony/http-kernel": "^5.4|^6.0",
33+
"symfony/http-kernel": "^6.2",
3434
"symfony/intl": "^5.4|^6.0",
3535
"symfony/mime": "^5.4|^6.0",
3636
"symfony/polyfill-intl-icu": "~1.0",
@@ -58,7 +58,7 @@
5858
"symfony/console": "<5.4",
5959
"symfony/form": "<6.1",
6060
"symfony/http-foundation": "<5.4",
61-
"symfony/http-kernel": "<5.4",
61+
"symfony/http-kernel": "<6.2",
6262
"symfony/translation": "<5.4",
6363
"symfony/workflow": "<5.4"
6464
},

0 commit comments

Comments
 (0)