8000 feature #40575 [FrameworkBundle][HttpKernel][TwigBridge] Add an helpe… · symfony/symfony@3154509 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3154509

Browse files
committed
feature #40575 [FrameworkBundle][HttpKernel][TwigBridge] Add an helper to generate fragments URL (dunglas)
This PR was squashed before being merged into the 5.3-dev branch. Discussion ---------- [FrameworkBundle][HttpKernel][TwigBridge] Add an helper to generate fragments URL | 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 | Fix n/a | License | MIT | Doc PR | todo This PR adds a new helper to generate raw fragment URL. Fragments will be useful to generate lazy frames with Symfony UX Turbo (symfony/ux#64). This will also be convenient when using hinclude, ESI etc in case you want full control over the generated HTML. This is also more in sync with the new best practices we apply in the form component (generate the HTML by yourself instead of using Twig helpers hiding the HTML elements). Example: ```html <turbo-frame id="set_aside_tray" src="{{ 8000 fragment_uri(controller('Symfony\Bundle\FrameworkBundle\Controller', {template: "foo.html.twig"})) }}"> <img src="/icons/spinner.gif"> </turbo-frame> ``` Commits ------- 5d29d76 [FrameworkBundle][HttpKernel][TwigBridge] Add an helper to generate fragments URL
2 parents 7327919 + 5d29d76 commit 3154509

File tree

17 files changed

+214
-55
lines changed

17 files changed

+214
-55
lines changed

src/Symfony/Bridge/Twig/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 a new `markAsPublic` method on `NotificationEmail` to change the `importance` context option to null after creation
8+
* Add a new `fragment_uri()` helper to generate the URI of a fragment
89

910
5.3.0
1011
-----

src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function getFunctions(): array
3030
return [
3131
new TwigFunction('render', [HttpKernelRuntime::class, 'renderFragment'], ['is_safe' => ['html']]),
3232
new TwigFunction('render_*', [HttpKernelRuntime::class, 'renderFragmentStrategy'], ['is_safe' => ['html']]),
33+
F438 new TwigFunction('fragment_uri', [HttpKernelRuntime::class, 'generateFragmentUri']),
3334
new TwigFunction('controller', static::class.'::controller'),
3435
];
3536
}

src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\HttpKernel\Controller\ControllerReference;
1515
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
16+
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
1617

1718
/**
1819
* Provides integration with the HttpKernel component.
@@ -22,10 +23,12 @@
2223
final class HttpKernelRuntime
2324
{
2425
private $handler;
26+
private $fragmentUriGenerator;
2527

26-
public function __construct(FragmentHandler $handler)
28+
public function __construct(FragmentHandler $handler, FragmentUriGeneratorInterface $fragmentUriGenerator = null)
2729
{
2830
$this->handler = $handler;
31+
$this->fragmentUriGenerator = $fragmentUriGenerator;
2932
}
3033

3134
/**
@@ -54,4 +57,13 @@ public function renderFragmentStrategy(string $strategy, $uri, array $options =
5457
{
5558
return $this->handler->render($uri, $strategy, $options);
5659
}
60+
61+
public function generateFragmentUri(ControllerReference $controller, bool $absolute = false, bool $strict = true, bool $sign = true): string
62+
{
63+
if (null === $this->fragmentUriGenerator) {
64+
throw new \LogicException(sprintf('An instance of "%s" must be provided to use "%s()".', FragmentUriGeneratorInterface::class, __METHOD__));
65+
}
66+
67+
return $this->fragmentUriGenerator->generate($controller, null, $absolute, $strict, $sign);
68+
}
5769
}

src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
1616
use Symfony\Bridge\Twig\Extension\HttpKernelRuntime;
17+
use Symfony\Bundle\FrameworkBundle\Controller\TemplateController;
1718
use Symfony\Component\HttpFoundation\Request;
1819
use Symfony\Component\HttpFoundation\RequestStack;
1920
use Symfony\Component\HttpFoundation\Response;
2021
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
2122
use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface;
23+
use Symfony\Component\HttpKernel\Fragment\FragmentUriGenerator;
24+
use Symfony\Component\HttpKernel\UriSigner;
2225
use Twig\Environment;
2326
use Twig\Loader\ArrayLoader;
2427
use Twig\RuntimeLoader\RuntimeLoaderInterface;
@@ -53,6 +56,37 @@ public function testUnknownFragmentRenderer()
5356
$renderer->render('/foo');
5457
}
5558

59+
public function testGenerateFragmentUri()
60+
{
61+
if (!class_exists(FragmentUriGenerator::class)) {
62+
$this->markTestSkipped('HttpKernel 5.3+ is required');
63+
}
64+
65+
$requestStack = new Reques F987 tStack();
66+
$requestStack->push(Request::create('/'));
67+
68+
$fragmentHandler = new FragmentHandler($requestStack);
69+
$fragmentUriGenerator = new FragmentUriGenerator('/_fragment', new UriSigner('s3cr3t'), $requestStack);
70+
71+
$kernelRuntime = new HttpKernelRuntime($fragmentHandler, $fragmentUriGenerator);
72+
73+
$loader = new ArrayLoader([
74+
'index' => sprintf(<<<TWIG
75+
{{ fragment_uri(controller("%s::templateAction", {template: "foo.html.twig"})) }}
76+
TWIG
77+
, TemplateController::class), ]);
78+
$twig = new Environment($loader, ['debug' => true, 'cache' => false]);
79+
$twig->addExtension(new HttpKernelExtension());
80+
81+
$loader = $this->createMock(RuntimeLoaderInterface::class);
82+
$loader->expects($this->any())->method('load')->willReturnMap([
83+
[HttpKernelRuntime::class, $kernelRuntime],
84+
]);
85+
$twig->addRuntimeLoader($loader);
86+
87+
$this->assertSame('/_fragment?_hash=PP8%2FeEbn1pr27I9wmag%2FM6jYGVwUZ0l2h0vhh2OJ6CI%3D&amp;_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfonyBundleFrameworkBundleControllerTemplateController%253A%253AtemplateAction', $twig->render('index'));
88+
}
89+
5690
protected function getFragmentHandler($return)
5791
{
5892
$strategy = $this->createMock(FragmentRendererInterface::class);

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ CHANGELOG
1818
* Deprecate all other values than "none", "php_array" and "file" for `framework.annotation.cache`
1919
* Add `KernelTestCase::getContainer()` as t 1241 he best way to get a container in tests
2020
* Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests`
21+
* Add service `fragment.uri_generator` to generate the URI of a fragment
2122

2223
5.2.0
2324
-----

src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler;
1515
use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer;
16+
use Symfony\Component\HttpKernel\Fragment\FragmentUriGenerator;
17+
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
1618
use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer;
1719
use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer;
1820
use Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer;
@@ -31,6 +33,10 @@
3133
param('kernel.debug'),
3234
])
3335

36+
->set('fragment.uri_generator', FragmentUriGenerator::class)
37+
->args([param('fragment.path'), service('uri_signer')])
38+
->alias(FragmentUriGeneratorInterface::class, 'fragment.uri_generator')
39+
3440
->set('fragment.renderer.inline', InlineFragmentRenderer::class)
3541
->args([service('http_kernel'), service('event_dispatcher')])
3642
->call('setFragmentPath', [param('fragment.path')])

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Symfony\Component\HttpClient\ScopingHttpClient;
5151
use Symfony\Component\HttpFoundation\Session\SessionInterface;
5252
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
53+
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
5354
use Symfony\Component\Messenger\Transport\TransportFactory;
5455
use Symfony\Component\PropertyAccess\PropertyAccessor;
5556
use Symfony\Component\Security\Core\Security;
@@ -182,6 +183,8 @@ public function testEsiDisabled()
182183
public function testFragmentsAndHinclude()
183184
{
184185
$container = $this->createContainerFromFile('fragments_and_hinclude');
186+
$this->assertTrue($container->has('fragment.uri_generator'));
187+
$this->assertTrue($container->hasAlias(FragmentUriGeneratorInterface::class));
185188
$this->assertTrue($container->hasParameter('fragment.renderer.hinclude.global_template'));
186189
$this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template'));
187190
}

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
1616
use Symfony\Component\HttpFoundation\Request;
1717
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\Controller\ControllerReference;
19+
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
1820
use Twig\Environment;
1921

2022
class FragmentController implements ContainerAwareInterface
@@ -45,6 +47,11 @@ public function forwardLocaleAction(Request $request)
4547
{
4648
return new Response($request->getLocale());
4749
}
50+
51+
public function fragmentUriAction(Request $request, FragmentUriGeneratorInterface $fragmentUriGenerator)
52+
{
53+
return new Response($fragmentUriGenerator->generate(new ControllerReference(self::class.'::indexAction'), $request));
54+
}
4855
}
4956

5057
class Bar

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ fragment_inlined:
5757
path: /fragment_inlined
5858
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::inlinedAction }
5959

60+
fragment_uri:
61+
path: /fragment_uri
62+
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::fragmentUriAction }
63+
6064
array_controller:
6165
path: /array_controller
6266
defaults: { _controller: [ArrayController, someAction] }

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@ public function getConfigs()
4444
[true],
4545
];
4646
}
47+
48+
public function testGenerateFragmentUri()
49+
{
50+
$client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]);
51+
$client->request('GET', '/fragment_uri');
52+
53+
$this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent());
54+
}
4755
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
->set('twig.extension.httpkernel', HttpKernelExtension::class)
124124

125125
->set('twig.runtime.httpkernel', HttpKernelRuntime::class)
126-
->args([service('fragment.handler')])
126+
->args([service('fragment.handler'), service('fragment.uri_generator')->ignoreOnInvalid()])
127127

128128
->set('twig.extension.httpfoundation', HttpFoundationExtension::class)
129129
->args([service('url_helper')])

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CHANGELOG
1212
* Deprecate `HttpKernelInterface::MASTER_REQUEST` and add `HttpKernelInterface::MAIN_REQUEST` as replacement
1313
* Deprecate `KernelEvent::isMasterRequest()` and add `isMainRequest()` as replacement
1414
* Add `#[AsController]` attribute for declaring standalone controllers on PHP 8
15+
* Add `FragmentUriGeneratorInterface` and `FragmentUriGenerator` to generate the URI of a fragment
1516

1617
5.2.0
1718
-----

src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,7 @@ public function render($uri, Request $request, array $options = [])
8383

8484
private function generateSignedFragmentUri(ControllerReference $uri, Request $request): string
8585
{
86-
if (null === $this->signer) {
87-
throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.');
88-
}
89-
90-
// we need to sign the absolute URI, but want to return the path only.
91-
$fragmentUri = $this->signer->sign($this->generateFragmentUri($uri, $request, true));
92-
93-
return substr($fragmentUri, \strlen($request->getSchemeAndHttpHost()));
86+
return (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request);
9487
}
9588

9689
private function containsNonScalars(array $values): bool
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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\Component\HttpKernel\Fragment;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestStack;
16+
use Symfony\Component\HttpKernel\Controller\ControllerReference;
17+
use Symfony\Component\HttpKernel\UriSigner;
18+
19+
/**
20+
* Generates a fragment URI.
21+
*
22+
* @author Kévin Dunglas <kevin@dunglas.fr>
23+
* @author Fabien Potencier <fabien@symfony.com>
24+
*/
25+
final class FragmentUriGenerator implements FragmentUriGeneratorInterface
26+
{
27+
private $fragmentPath;
28+
private $signer;
29+
private $requestStack;
30+
31+
public function __construct(string $fragmentPath, UriSigner $signer = null, RequestStack $requestStack = null)
32+
{
33+
$this->fragmentPath = $fragmentPath;
34+
$this->signer = $signer;
35+
$this->requestStack = $requestStack;
36+
}
37+
38+
/**
39+
* {@inheritDoc}
40+
*/
41+
public function generate(ControllerReference $controller, Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string
42+
{
43+
if (null === $request && (null === $this->requestStack || null === $request = $this->requestStack->getCurrentRequest())) {
44+
throw new \LogicException('Generating a fragment URL can only be done when handling a Request.');
45+
}
46+
47+
if ($sign && null === $this->signer) {
48+
throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.');
49+
}
50+
51+
if ($strict) {
52+
$this->checkNonScalar($controller->attributes);
53+
}
54+
55+
// We need to forward the current _format and _locale values as we don't have
56+
// a proper routing pattern to do the job for us.
57+
// This makes things inconsistent if you switch from rendering a controller
58+
// to rendering a route if the route pattern does not contain the special
59+
// _format and _locale placeholders.
60+
if (!isset($controller->attributes['_format'])) {
61+
$controller->attributes['_format'] = $request->getRequestFormat();
62+
}
63+
if (!isset($controller->attributes['_locale'])) {
64+
$controller->attributes['_locale'] = $request->getLocale();
65+
}
66+
67+
$controller->attributes['_controller'] = $controller->controller;
68+
$controller->query['_path'] = http_build_query($controller->attributes, '', '&');
69+
$path = $this->fragmentPath.'?'.http_build_query($controller->query, '', '&');
70+
71+
// we need to sign the absolute URI, but want to return the path only.
72+
$fragmentUri = $sign || $absolute ? $request->getUriForPath($path) : $request->getBaseUrl().$path;
73+
74+
if (!$sign) {
75+
return $fragmentUri;
76+
}
77+
78+
$fragmentUri = $this->signer->sign($fragmentUri);
79+
80+
return $absolute ? $fragmentUri : substr($fragmentUri, \strlen($request->getSchemeAndHttpHost()));
81+
}
82+
83+
private function checkNonScalar(array $values): void
84+
{
85+
foreach ($values as $key => $value) {
86+
if (\is_array($value)) {
87+
$this->checkNonScalar($value);
88+
} elseif (!is_scalar($value) && null !== $value) {
89+
throw new \LogicException(sprintf('Controller attributes cannot contain non-scalar/non-null values (value for key "%s" is not a scalar or null).', $key));
90+
}
91+
}
92+
}
93+
}
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 <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\Component\HttpKernel\Fragment;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ControllerReference;
16+
17+
/**
18+
* Interface implemented by rendering strategies able to generate an URL for a fragment.
19+
*
20+
* @author Kévin Dunglas <kevin@dunglas.fr>
21+
*/
22+
interface FragmentUriGeneratorInterface
23+
{
24+
/**
25+
* Generates a fragment URI for a given controller.
26+
*
27+
* @param bool $absolute Whether to generate an absolute URL or not
28+
* @param bool $strict Whether to allow non-scalar attributes or not
29+
* @param bool $sign Whether to sign the URL or not
30+
*
31+
* @return string A fragment URI
32+
*/
33+
public function generate(ControllerReference $controller, Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string;
34+
}

src/Symfony/Component/HttpKernel/Fragment/HIncludeFragmentRenderer.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,7 @@ public function hasTemplating()
6262
public function render($uri, Request $request, array $options = [])
6363 8AA7
{
6464
if ($uri instanceof ControllerReference) {
65-
if (null === $this->signer) {
66-
throw new \LogicException('You must use a proper URI when using the Hinclude rendering strategy or set a URL signer.');
67-
}
68-
69-
// we need to sign the absolute URI, but want to return the path only.
70-
$uri = substr($this->signer->sign($this->generateFragmentUri($uri, $request, true)), \strlen($request->getSchemeAndHttpHost()));
65+
$uri = (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request);
7166
}
7267

7368
// We need to replace ampersands in the URI with the encoded form in order to return valid html/xml content.

0 commit comments

Comments
 (0)
0