Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c252606

Browse filesBrowse files
[TwigBridge] Add #[Template()] to describe how to render arrays returned by controllers
1 parent 3edca67 commit c252606
Copy full SHA for c252606

File tree

12 files changed

+262
-27
lines changed
Filter options

12 files changed

+262
-27
lines changed
+34Lines 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 <fabien@symfony.com>
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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/CHANGELOG.md
+1Lines 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
---
+84Lines 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 <fabien@symfony.com>
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+
}
+72Lines 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 <fabien@symfony.com>
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+
}
+22Lines 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 <fabien@symfony.com>
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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/composer.json
+2-2Lines 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
},

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
+5Lines changed: 5 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/TwigBundle/composer.json
+4-5Lines changed: 4 additions & 5 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Event/ControllerArgumentsEvent.php
+28Lines changed: 28 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Event/ViewEvent.php
+3-1Lines changed: 3 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/HttpKernel.php
+2-2Lines changed: 2 additions & 2 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.