Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FrameworkBundle][HttpKernel][TwigBridge] Add an helper to generate fragments URL #40575

Merged
merged 1 commit into from Mar 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bridge/Twig/CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* Add a new `markAsPublic` method on `NotificationEmail` to change the `importance` context option to null after creation
* Add a new `fragment_uri()` helper to generate the URI of a fragment

5.3.0
-----
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php
Expand Up @@ -30,6 +30,7 @@ public function getFunctions(): array
return [
new TwigFunction('render', [HttpKernelRuntime::class, 'renderFragment'], ['is_safe' => ['html']]),
new TwigFunction('render_*', [HttpKernelRuntime::class, 'renderFragmentStrategy'], ['is_safe' => ['html']]),
new TwigFunction('fragment_uri', [HttpKernelRuntime::class, 'generateFragmentUri']),
new TwigFunction('controller', static::class.'::controller'),
];
}
Expand Down
14 changes: 13 additions & 1 deletion src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;

/**
* Provides integration with the HttpKernel component.
Expand All @@ -22,10 +23,12 @@
final class HttpKernelRuntime
{
private $handler;
private $fragmentUriGenerator;

public function __construct(FragmentHandler $handler)
public function __construct(FragmentHandler $handler, FragmentUriGeneratorInterface $fragmentUriGenerator = null)
{
$this->handler = $handler;
$this->fragmentUriGenerator = $fragmentUriGenerator;
}

/**
Expand Down Expand Up @@ -54,4 +57,13 @@ public function renderFragmentStrategy(string $strategy, $uri, array $options =
{
return $this->handler->render($uri, $strategy, $options);
}

public function generateFragmentUri(ControllerReference $controller, bool $absolute = false, bool $strict = true, bool $sign = true): string
{
if (null === $this->fragmentUriGenerator) {
throw new \LogicException(sprintf('An instance of "%s" must be provided to use "%s()".', FragmentUriGeneratorInterface::class, __METHOD__));
}

return $this->fragmentUriGenerator->generate($controller, null, $absolute, $strict, $sign);
}
}
Expand Up @@ -14,11 +14,14 @@
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
use Symfony\Bridge\Twig\Extension\HttpKernelRuntime;
use Symfony\Bundle\FrameworkBundle\Controller\TemplateController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGenerator;
use Symfony\Component\HttpKernel\UriSigner;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
Expand Down Expand Up @@ -53,6 +56,37 @@ public function testUnknownFragmentRenderer()
$renderer->render('/foo');
}

public function testGenerateFragmentUri()
{
if (!class_exists(FragmentUriGenerator::class)) {
$this->markTestSkipped('HttpKernel 5.3+ is required');
}

$requestStack = new RequestStack();
$requestStack->push(Request::create('/'));

$fragmentHandler = new FragmentHandler($requestStack);
$fragmentUriGenerator = new FragmentUriGenerator('/_fragment', new UriSigner('s3cr3t'), $requestStack);

$kernelRuntime = new HttpKernelRuntime($fragmentHandler, $fragmentUriGenerator);

$loader = new ArrayLoader([
'index' => sprintf(<<<TWIG
{{ fragment_uri(controller("%s::templateAction", {template: "foo.html.twig"})) }}
TWIG
, TemplateController::class), ]);
$twig = new Environment($loader, ['debug' => true, 'cache' => false]);
$twig->addExtension(new HttpKernelExtension());

$loader = $this->createMock(RuntimeLoaderInterface::class);
$loader->expects($this->any())->method('load')->willReturnMap([
[HttpKernelRuntime::class, $kernelRuntime],
]);
$twig->addRuntimeLoader($loader);

$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'));
}

protected function getFragmentHandler($return)
{
$strategy = $this->createMock(FragmentRendererInterface::class);
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Expand Up @@ -18,6 +18,7 @@ CHANGELOG
* Deprecate all other values than "none", "php_array" and "file" for `framework.annotation.cache`
* Add `KernelTestCase::getContainer()` as the best way to get a container in tests
* Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests`
* Add service `fragment.uri_generator` to generate the URI of a fragment

5.2.0
-----
Expand Down
Expand Up @@ -13,6 +13,8 @@

use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler;
use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGenerator;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer;
use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer;
use Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer;
Expand All @@ -31,6 +33,10 @@
param('kernel.debug'),
])

->set('fragment.uri_generator', FragmentUriGenerator::class)
->args([param('fragment.path'), service('uri_signer')])
->alias(FragmentUriGeneratorInterface::class, 'fragment.uri_generator')

->set('fragment.renderer.inline', InlineFragmentRenderer::class)
->args([service('http_kernel'), service('event_dispatcher')])
->call('setFragmentPath', [param('fragment.path')])
Expand Down
Expand Up @@ -49,6 +49,7 @@
use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
use Symfony\Component\Messenger\Transport\TransportFactory;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Security\Core\Security;
Expand Down Expand Up @@ -181,6 +182,8 @@ public function testEsiDisabled()
public function testFragmentsAndHinclude()
{
$container = $this->createContainerFromFile('fragments_and_hinclude');
$this->assertTrue($container->has('fragment.uri_generator'));
$this->assertTrue($container->hasAlias(FragmentUriGeneratorInterface::class));
$this->assertTrue($container->hasParameter('fragment.renderer.hinclude.global_template'));
$this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template'));
}
Expand Down
Expand Up @@ -15,6 +15,8 @@
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
use Twig\Environment;

class FragmentController implements ContainerAwareInterface
Expand Down Expand Up @@ -45,6 +47,11 @@ public function forwardLocaleAction(Request $request)
{
return new Response($request->getLocale());
}

public function fragmentUriAction(Request $request, FragmentUriGeneratorInterface $fragmentUriGenerator)
{
return new Response($fragmentUriGenerator->generate(new ControllerReference(self::class.'::indexAction'), $request));
}
}

class Bar
Expand Down
Expand Up @@ -57,6 +57,10 @@ fragment_inlined:
path: /fragment_inlined
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::inlinedAction }

fragment_uri:
path: /fragment_uri
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::fragmentUriAction }

array_controller:
path: /array_controller
defaults: { _controller: [ArrayController, someAction] }
Expand Down
Expand Up @@ -44,4 +44,12 @@ public function getConfigs()
[true],
];
}

public function testGenerateFragmentUri()
{
$client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]);
$client->request('GET', '/fragment_uri');

$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());
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/TwigBundle/Resources/config/twig.php
Expand Up @@ -123,7 +123,7 @@
->set('twig.extension.httpkernel', HttpKernelExtension::class)

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

->set('twig.extension.httpfoundation', HttpFoundationExtension::class)
->args([service('url_helper')])
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ CHANGELOG
* Deprecate `HttpKernelInterface::MASTER_REQUEST` and add `HttpKernelInterface::MAIN_REQUEST` as replacement
* Deprecate `KernelEvent::isMasterRequest()` and add `isMainRequest()` as replacement
* Add `#[AsController]` attribute for declaring standalone controllers on PHP 8
* Add `FragmentUriGeneratorInterface` and `FragmentUriGenerator` to generate the URI of a fragment

5.2.0
-----
Expand Down
Expand Up @@ -83,14 +83,7 @@ public function render($uri, Request $request, array $options = [])

private function generateSignedFragmentUri(ControllerReference $uri, Request $request): string
{
if (null === $this->signer) {
throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.');
}

// we need to sign the absolute URI, but want to return the path only.
$fragmentUri = $this->signer->sign($this->generateFragmentUri($uri, $request, true));

return substr($fragmentUri, \strlen($request->getSchemeAndHttpHost()));
return (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request);
}

private function containsNonScalars(array $values): bool
Expand Down
93 changes: 93 additions & 0 deletions src/Symfony/Component/HttpKernel/Fragment/FragmentUriGenerator.php
@@ -0,0 +1,93 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Fragment;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\UriSigner;

/**
* Generates a fragment URI.
*
* @author Kévin Dunglas <kevin@dunglas.fr>
* @author Fabien Potencier <fabien@symfony.com>
*/
final class FragmentUriGenerator implements FragmentUriGeneratorInterface
{
private $fragmentPath;
private $signer;
private $requestStack;

public function __construct(string $fragmentPath, UriSigner $signer = null, RequestStack $requestStack = null)
{
$this->fragmentPath = $fragmentPath;
$this->signer = $signer;
$this->requestStack = $requestStack;
}

/**
* {@inheritDoc}
*/
public function generate(ControllerReference $controller, Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string
{
if (null === $request && (null === $this->requestStack || null === $request = $this->requestStack->getCurrentRequest())) {
throw new \LogicException('Generating a fragment URL can only be done when handling a Request.');
}

if ($sign && null === $this->signer) {
throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.');
}

if ($strict) {
$this->checkNonScalar($controller->attributes);
dunglas marked this conversation as resolved.
Show resolved Hide resolved
}

// We need to forward the current _format and _locale values as we don't have
// a proper routing pattern to do the job for us.
// This makes things inconsistent if you switch from rendering a controller
// to rendering a route if the route pattern does not contain the special
// _format and _locale placeholders.
if (!isset($controller->attributes['_format'])) {
$controller->attributes['_format'] = $request->getRequestFormat();
}
if (!isset($controller->attributes['_locale'])) {
$controller->attributes['_locale'] = $request->getLocale();
}

$controller->attributes['_controller'] = $controller->controller;
$controller->query['_path'] = http_build_query($controller->attributes, '', '&');
$path = $this->fragmentPath.'?'.http_build_query($controller->query, '', '&');

// we need to sign the absolute URI, but want to return the path only.
$fragmentUri = $sign || $absolute ? $request->getUriForPath($path) : $request->getBaseUrl().$path;

if (!$sign) {
return $fragmentUri;
}

$fragmentUri = $this->signer->sign($fragmentUri);

return $absolute ? $fragmentUri : substr($fragmentUri, \strlen($request->getSchemeAndHttpHost()));
}

private function checkNonScalar(array $values): void
{
foreach ($values as $key => $value) {
if (\is_array($value)) {
$this->checkNonScalar($value);
} elseif (!is_scalar($value) && null !== $value) {
throw new \LogicException(sprintf('Controller attributes cannot contain non-scalar/non-null values (value for key "%s" is not a scalar or null).', $key));
}
}
}
}
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Fragment;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerReference;

/**
* Interface implemented by rendering strategies able to generate an URL for a fragment.
*
* @author Kévin Dunglas <kevin@dunglas.fr>
*/
interface FragmentUriGeneratorInterface
{
/**
* Generates a fragment URI for a given controller.
*
* @param bool $absolute Whether to generate an absolute URL or not
* @param bool $strict Whether to allow non-scalar attributes or not
* @param bool $sign Whether to sign the URL or not
*
* @return string A fragment URI
*/
public function generate(ControllerReference $controller, Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string;
}
Expand Up @@ -62,12 +62,7 @@ public function hasTemplating()
public function render($uri, Request $request, array $options = [])
{
if ($uri instanceof ControllerReference) {
if (null === $this->signer) {
throw new \LogicException('You must use a proper URI when using the Hinclude rendering strategy or set a URL signer.');
}

// we need to sign the absolute URI, but want to return the path only.
$uri = substr($this->signer->sign($this->generateFragmentUri($uri, $request, true)), \strlen($request->getSchemeAndHttpHost()));
$uri = (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request);
}

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