Skip to content

[Routing] UrlHelper to get absolute URL for a path #30862

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

Merged
merged 1 commit into from
Apr 7, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions UPGRADE-4.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ Security
}
```

TwigBridge
==========

* deprecated the `$requestStack` and `$requestContext` arguments of the
`HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper`
instance as the only argument instead

Workflow
--------

Expand Down
7 changes: 7 additions & 0 deletions UPGRADE-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ TwigBundle
* The default value (`false`) of the `twig.strict_variables` configuration option has been changed to `%kernel.debug%`.
* The `transchoice` tag and filter have been removed, use the `trans` ones instead with a `%count%` parameter.
* Removed support for legacy templates directories `src/Resources/views/` and `src/Resources/<BundleName>/views/`, use `templates/` and `templates/bundles/<BundleName>/` instead.

TwigBridge
----------

* removed the `$requestStack` and `$requestContext` arguments of the
`HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper`
instance as the only argument instead

Validator
--------
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Bridge/Twig/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ CHANGELOG

* added the `form_parent()` function that allows to reliably retrieve the parent form in Twig templates
* added the `workflow_transition_blockers()` function
* deprecated the `$requestStack` and `$requestContext` arguments of the
`HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper`
instance as the only argument instead

4.2.0
-----
Expand Down
92 changes: 29 additions & 63 deletions src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\UrlHelper;
use Symfony\Component\Routing\RequestContext;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
Expand All @@ -24,13 +25,34 @@
*/
class HttpFoundationExtension extends AbstractExtension
{
private $requestStack;
private $requestContext;
private $urlHelper;

public function __construct(RequestStack $requestStack, RequestContext $requestContext = null)
/**
* @param UrlHelper $urlHelper
*/
public function __construct($urlHelper)
{
$this->requestStack = $requestStack;
$this->requestContext = $requestContext;
if ($urlHelper instanceof UrlHelper) {
$this->urlHelper = $urlHelper;

return;
}

if (!$urlHelper instanceof RequestStack) {
throw new \TypeError(sprintf('The first argument must be an instance of "%s" or an instance of "%s".', UrlHelper::class, RequestStack::class));
}

@trigger_error(sprintf('Passing a "%s" instance as the first argument to the "%s" constructor is deprecated since Symfony 4.3, pass a "%s" instance instead.', RequestStack::class, __CLASS__, UrlHelper::class), E_USER_DEPRECATED);

$requestContext = null;
if (2 === \func_num_args()) {
$requestContext = \func_get_arg(1);
if (!$requestContext instanceof RequestContext) {
throw new \TypeError(sprintf('The second argument must be an instance of "%s".', RequestContext::class));
}
}

$this->urlHelper = new UrlHelper($urlHelper, $requestContext);
}

/**
Expand All @@ -57,55 +79,7 @@ public function getFunctions()
*/
public function generateAbsoluteUrl($path)
{
if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) {
return $path;
}

if (!$request = $this->requestStack->getMasterRequest()) {
if (null !== $this->requestContext && '' !== $host = $this->requestContext->getHost()) {
$scheme = $this->requestContext->getScheme();
$port = '';

if ('http' === $scheme && 80 != $this->requestContext->getHttpPort()) {
$port = ':'.$this->requestContext->getHttpPort();
} elseif ('https' === $scheme && 443 != $this->requestContext->getHttpsPort()) {
$port = ':'.$this->requestContext->getHttpsPort();
}

if ('#' === $path[0]) {
$queryString = $this->requestContext->getQueryString();
$path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path;
} elseif ('?' === $path[0]) {
$path = $this->requestContext->getPathInfo().$path;
}

if ('/' !== $path[0]) {
$path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path;
}

return $scheme.'://'.$host.$port.$path;
}

return $path;
}

if ('#' === $path[0]) {
$path = $request->getRequestUri().$path;
} elseif ('?' === $path[0]) {
$path = $request->getPathInfo().$path;
}

if (!$path || '/' !== $path[0]) {
$prefix = $request->getPathInfo();
$last = \strlen($prefix) - 1;
if ($last !== $pos = strrpos($prefix, '/')) {
$prefix = substr($prefix, 0, $pos).'/';
}

return $request->getUriForPath($prefix.$path);
}

return $request->getSchemeAndHttpHost().$path;
return $this->urlHelper->getAbsoluteUrl($path);
}

/**
Expand All @@ -121,15 +95,7 @@ public function generateAbsoluteUrl($path)
*/
public function generateRelativePath($path)
{
if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) {
return $path;
}

if (!$request = $this->requestStack->getMasterRequest()) {
return $path;
}

return $request->getRelativeUriForPath($path);
return $this->urlHelper->getRelativePath($path);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RequestContext;

/**
* @group legacy
*/
class HttpFoundationExtensionTest extends TestCase
{
/**
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Bridge/Twig/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/finder": "~3.4|~4.0",
"symfony/form": "^4.3",
"symfony/http-foundation": "~3.4|~4.0",
"symfony/http-foundation": "~4.3",
"symfony/http-kernel": "~3.4|~4.0",
"symfony/mime": "~4.3",
"symfony/polyfill-intl-icu": "~1.0",
Expand All @@ -46,6 +46,7 @@
"conflict": {
"symfony/console": "<3.4",
"symfony/form": "<4.3",
"symfony/http-foundation": "<4.3",
"symfony/translation": "<4.2",
"symfony/workflow": "<4.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
<service id="request_stack" class="Symfony\Component\HttpFoundation\RequestStack" public="true" />
<service id="Symfony\Component\HttpFoundation\RequestStack" alias="request_stack" />

<service id="url_helper" class="Symfony\Component\HttpFoundation\UrlHelper">
<argument type="service" id="request_stack" />
<argument type="service" id="router.request_context" on-invalid="ignore" />
</service>
<service id="Symfony\Component\HttpFoundation\UrlHelper" alias="url_helper" />

<service id="cache_warmer" class="Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate" public="true">
<argument type="tagged" tag="kernel.cache_warmer" />
<argument>%kernel.debug%</argument>
Expand Down
3 changes: 1 addition & 2 deletions src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@
</service>

<service id="twig.extension.httpfoundation" class="Symfony\Bridge\Twig\Extension\HttpFoundationExtension">
<argument type="service" id="request_stack" />
<argument type="service" id="router.request_context" on-invalid="ignore" />
<argument type="service" id="url_helper" />
</service>

<service id="twig.extension.debug" class="Twig\Extension\DebugExtension" />
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/TwigBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"require": {
"php": "^7.1.3",
"symfony/config": "~4.2",
"symfony/twig-bridge": "^4.2",
"symfony/http-foundation": "~4.1",
"symfony/twig-bridge": "^4.3",
"symfony/http-foundation": "~4.3",
"symfony/http-kernel": "~4.1",
"symfony/polyfill-ctype": "~1.8",
"twig/twig": "~1.34|~2.4"
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CHANGELOG
* deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`.
* deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`.
* deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`.
* added `UrlHelper` that allows to get an absolute URL and a relative path for a given path

4.2.0
-----
Expand Down
143 changes: 143 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?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\HttpFoundation\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\UrlHelper;
use Symfony\Component\Routing\RequestContext;

class UrlHelperTest extends TestCase
{
/**
* @dataProvider getGenerateAbsoluteUrlData()
*/
public function testGenerateAbsoluteUrl($expected, $path, $pathinfo)
{
$stack = new RequestStack();
$stack->push(Request::create($pathinfo));
$helper = new UrlHelper($stack);

$this->assertEquals($expected, $helper->getAbsoluteUrl($path));
}

public function getGenerateAbsoluteUrlData()
{
return [
['http://localhost/foo.png', '/foo.png', '/foo/bar.html'],
['http://localhost/foo/foo.png', 'foo.png', '/foo/bar.html'],
['http://localhost/foo/foo.png', 'foo.png', '/foo/bar'],
['http://localhost/foo/bar/foo.png', 'foo.png', '/foo/bar/'],

['http://example.com/baz', 'http://example.com/baz', '/'],
['https://example.com/baz', 'https://example.com/baz', '/'],
['//example.com/baz', '//example.com/baz', '/'],

['http://localhost/foo/bar?baz', '?baz', '/foo/bar'],
['http://localhost/foo/bar?baz=1', '?baz=1', '/foo/bar?foo=1'],
['http://localhost/foo/baz?baz=1', 'baz?baz=1', '/foo/bar?foo=1'],

['http://localhost/foo/bar#baz', '#baz', '/foo/bar'],
['http://localhost/foo/bar?0#baz', '#baz', '/foo/bar?0'],
['http://localhost/foo/bar?baz=1#baz', '?baz=1#baz', '/foo/bar?foo=1'],
['http://localhost/foo/baz?baz=1#baz', 'baz?baz=1#baz', '/foo/bar?foo=1'],
];
}

/**
* @dataProvider getGenerateAbsoluteUrlRequestContextData
*/
public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected)
{
if (!class_exists('Symfony\Component\Routing\RequestContext')) {
$this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.');
}

$requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path);
$helper = new UrlHelper(new RequestStack(), $requestContext);

$this->assertEquals($expected, $helper->getAbsoluteUrl($path));
}

/**
* @dataProvider getGenerateAbsoluteUrlRequestContextData
*/
public function testGenerateAbsoluteUrlWithoutRequestAndRequestContext($path)
{
if (!class_exists('Symfony\Component\Routing\RequestContext')) {
$this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.');
}

$helper = new UrlHelper(new RequestStack());

$this->assertEquals($path, $helper->getAbsoluteUrl($path));
}

public function getGenerateAbsoluteUrlRequestContextData()
{
return [
['/foo.png', '/foo', 'localhost', 'http', 80, 443, 'http://localhost/foo.png'],
['foo.png', '/foo', 'localhost', 'http', 80, 443, 'http://localhost/foo/foo.png'],
['foo.png', '/foo/bar/', 'localhost', 'http', 80, 443, 'http://localhost/foo/bar/foo.png'],
['/foo.png', '/foo', 'localhost', 'https', 80, 443, 'https://localhost/foo.png'],
['foo.png', '/foo', 'localhost', 'https', 80, 443, 'https://localhost/foo/foo.png'],
['foo.png', '/foo/bar/', 'localhost', 'https', 80, 443, 'https://localhost/foo/bar/foo.png'],
['/foo.png', '/foo', 'localhost', 'http', 443, 80, 'http://localhost:443/foo.png'],
['/foo.png', '/foo', 'localhost', 'https', 443, 80, 'https://localhost:80/foo.png'],
];
}

public function testGenerateAbsoluteUrlWithScriptFileName()
{
$request = Request::create('http://localhost/app/web/app_dev.php');
$request->server->set('SCRIPT_FILENAME', '/var/www/app/web/app_dev.php');

$stack = new RequestStack();
$stack->push($request);
$helper = new UrlHelper($stack);

$this->assertEquals(
'http://localhost/app/web/bundles/framework/css/structure.css',
$helper->getAbsoluteUrl('/app/web/bundles/framework/css/structure.css')
);
}

/**
* @dataProvider getGenerateRelativePathData()
*/
public function testGenerateRelativePath($expected, $path, $pathinfo)
{
if (!method_exists('Symfony\Component\HttpFoundation\Request', 'getRelativeUriForPath')) {
$this->markTestSkipped('Your version of Symfony HttpFoundation is too old.');
}

$stack = new RequestStack();
$stack->push(Request::create($pathinfo));
$urlHelper = new UrlHelper($stack);

$this->assertEquals($expected, $urlHelper->getRelativePath($path));
}

public function getGenerateRelativePathData()
{
return [
['../foo.png', '/foo.png', '/foo/bar.html'],
['../baz/foo.png', '/baz/foo.png', '/foo/bar.html'],
['baz/foo.png', 'baz/foo.png', '/foo/bar.html'],

['http://example.com/baz', 'http://example.com/baz', '/'],
['https://example.com/baz', 'https://example.com/baz', '/'],
['//example.com/baz', '//example.com/baz', '/'],
];
}
}
Loading