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 98c1cfa

Browse filesBrowse files
[TwigBridge] Add #[Template()] to describe how to render arrays returned by controllers
1 parent 7c194b9 commit 98c1cfa
Copy full SHA for 98c1cfa

File tree

10 files changed

+242
-10
lines changed
Filter options

10 files changed

+242
-10
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_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
---
+97Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
if (!$attribute = ($event->controllerArgumentsEvent?->getAttributes()[Template::class][0] ?? null)) {
39+
return;
40+
}
41+
42+
$parameters ??= $this->resolveParameters($event->controllerArgumentsEvent, $attribute->vars);
43+
$status = 200;
44+
45+
foreach ($parameters as $k => $v) {
46+
if (!$v instanceof FormInterface) {
47+
continue;
48+
}
49+
if ($v->isSubmitted() && !$v->isValid()) {
50+
$status = 422;
51+
}
52+
$parameters[$k] = $v->createView();
53+
}
54+
55+
$event->setResponse($attribute->stream
56+
? new StreamedResponse(fn () => $this->twig->display($attribute->template, $parameters), $status)
57+
: new Response($this->twig->render($attribute->template, $parameters), $status)
58+
);
59+
}
60+
61+
public static function getSubscribedEvents(): array
62+
{
63+
return [
64+
KernelEvents::VIEW => ['onKernelView', -128],
65+
];
66+
}
67+
68+
private function resolveParameters(ControllerArgumentsEvent $event, ?array $vars)
69+
{
70+
if ([] === $vars) {
71+
return [];
72+
}
73+
74+
$arguments = $event->getArguments();
75+
$parameters = [];
76+
77+
$r = $event->getRequest()->attributes->get('_controller_reflectors')[1] ?? new \ReflectionFunction($event->getController());
78+
79+
foreach ($r->getParameters() as $i => $param) {
80+
if ($param->isVariadic()) {
81+
$parameters[$param->name] = \array_slice($arguments, $i);
82+
break;
83+
}
84+
if (\array_key_exists($i, $arguments)) {
85+
$parameters[$param->name] = $arguments[$i];
86+
} elseif ($param->isDefaultValueAvailable()) {
87+
$parameters[$param->name] = $param->getDefaultValue();
88+
}
89+
}
90+
91+
if (null !== $vars) {
92+
$parameters = array_intersect_key($parameters, array_flip($vars));
93+
}
94+
95+
return $parameters;
96+
}
97+
}
+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/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.