Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 378902e

Browse files
feature #46906 [TwigBridge] Add #[Template()] to describe how to render arrays returned by controllers (nicolas-grekas)
This PR was merged into the 6.2 branch. Discussion ---------- [TwigBridge] Add `#[Template()]` to describe how to render arrays returned by controllers | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Part of #44705 | License | MIT | Doc PR | - Extracted from #45415 (and modernized a lot). Unlike the``@Template`` annotation, this attribute mandates a template name as argument. This removes the need for any template-guesser logic, making things explicit. Using the attribute also turns `FormInterface` instances into `FormView`, adjusting the status code to 422 when a non-valid form is found in the parameters, similarly to what `AbstractController::render()` does. Commits ------- c252606 [TwigBridge] Add `#[Template()]` to describe how to render arrays returned by controllers
2 parents 3edca67 + c252606 commit 378902e

File tree

12 files changed

+262
-27
lines changed

12 files changed

+262
-27
lines changed
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+
}

src/Symfony/Bridge/Twig/CHANGELOG.md

+1
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
---
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+
}
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+
}
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+
}

src/Symfony/Bridge/Twig/composer.json

+2-2
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
},

src/Symfony/Bundle/TwigBundle/Resources/config/twig.php

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Bridge\Twig\AppVariable;
1616
use Symfony\Bridge\Twig\DataCollector\TwigDataCollector;
1717
use Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer;
18+
use Symfony\Bridge\Twig\EventListener\TemplateAttributeListener;
1819
use Symfony\Bridge\Twig\Extension\AssetExtension;
1920
use Symfony\Bridge\Twig\Extension\CodeExtension;
2021
use Symfony\Bridge\Twig\Extension\ExpressionExtension;
@@ -169,5 +170,9 @@
169170
->args([service('serializer')])
170171

171172
->set('twig.extension.serializer', SerializerExtension::class)
173+
174+
->set('controller.template_attribute_listener', TemplateAttributeListener::class)
175+
->args([service('twig')])
176+
->tag('kernel.event_subscriber')
172177
;
173178
};

src/Symfony/Bundle/TwigBundle/composer.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@
1818
"require": {
1919
"php": ">=8.1",
2020
"composer-runtime-api": ">=2.1",
21-
"symfony/config": "^5.4|^6.0",
22-
"symfony/dependency-injection": "^5.4|^6.0",
23-
"symfony/twig-bridge": "^5.4|^6.0",
21+
"symfony/config": "^6.1",
22+
"symfony/dependency-injection": "^6.1",
23+
"symfony/twig-bridge": "^6.2",
2424
"symfony/http-foundation": "^5.4|^6.0",
25-
"symfony/http-kernel": "^5.4|^6.0",
26-
"symfony/polyfill-ctype": "~1.8",
25+
"symfony/http-kernel": "^6.2",
2726
"twig/twig": "^2.13|^3.0.4"
2827
},
2928
"require-dev": {

src/Symfony/Component/HttpKernel/Event/ControllerArgumentsEvent.php

+28
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class ControllerArgumentsEvent extends KernelEvent
3030
{
3131
private ControllerEvent $controllerEvent;
3232
private array $arguments;
33+
private array $namedArguments;
3334

3435
public function __construct(HttpKernelInterface $kernel, callable|ControllerEvent $controller, array $arguments, Request $request, ?int $requestType)
3536
{
@@ -54,6 +55,7 @@ public function getController(): callable
5455
public function setController(callable $controller, array $attributes = null): void
5556
{
5657
$this->controllerEvent->setController($controller, $attributes);
58+
unset($this->namedArguments);
5759
}
5860

5961
public function getArguments(): array
@@ -64,6 +66,32 @@ public function getArguments(): array
6466
public function setArguments(array $arguments)
6567
{
6668
$this->arguments = $arguments;
69+
unset($this->namedArguments);
70+
}
71+
72+
public function getNamedArguments(): array
73+
{
74+
if (isset($this->namedArguments)) {
75+
return $this->namedArguments;
76+
}
77+
78+
$namedArguments = [];
79+
$arguments = $this->arguments;
80+
$r = $this->getRequest()->attributes->get('_controller_reflectors')[1] ?? new \ReflectionFunction($this->controllerEvent->getController());
81+
82+
foreach ($r->getParameters() as $i => $param) {
83+
if ($param->isVariadic()) {
84+
$namedArguments[$param->name] = \array_slice($arguments, $i);
85+
break;
86+
}
87+
if (\array_key_exists($i, $arguments)) {
88+
$namedArguments[$param->name] = $arguments[$i];
89+
} elseif ($param->isDefaultvalueAvailable()) {
90+
$namedArguments[$param->name] = $param->getDefaultValue();
91+
}
92+
}
93+
94+
return $this->namedArguments = $namedArguments;
6795
}
6896

6997
/**

src/Symfony/Component/HttpKernel/Event/ViewEvent.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
*/
2626
final class ViewEvent extends RequestEvent
2727
{
28+
public readonly ?ControllerArgumentsEvent $controllerArgumentsEvent;
2829
private mixed $controllerResult;
2930

30-
public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, mixed $controllerResult)
31+
public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, mixed $controllerResult, ControllerArgumentsEvent $controllerArgumentsEvent = null)
3132
{
3233
parent::__construct($kernel, $request, $requestType);
3334

3435
$this->controllerResult = $controllerResult;
36+
$this->controllerArgumentsEvent = $controllerArgumentsEvent;
3537
}
3638

3739
public function getControllerResult(): mixed

src/Symfony/Component/HttpKernel/HttpKernel.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,13 @@ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Re
156156
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
157157
$controller = $event->getController();
158158
$arguments = $event->getArguments();
159-
$request->attributes->remove('_controller_reflectors');
160159

161160
// call controller
162161
$response = $controller(...$arguments);
163162

164163
// view
165164
if (!$response instanceof Response) {
166-
$event = new ViewEvent($this, $request, $type, $response);
165+
$event = new ViewEvent($this, $request, $type, $response, $event);
167166
$this->dispatcher->dispatch($event, KernelEvents::VIEW);
168167

169168
if ($event->hasResponse()) {
@@ -179,6 +178,7 @@ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Re
179178
throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17);
180179
}
181180
}
181+
$request->attributes->remove('_controller_reflectors');
182182

183183
return $this->filterResponse($response, $request, $type);
184184
}

0 commit comments

Comments
 (0)