-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
URL manipulations as a Twig extension #13264
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
<?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\Bridge\Twig\Extension; | ||
|
||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\HttpFoundation\Request; | ||
|
||
/** | ||
* Twig extension for the Symfony HttpFoundation component. | ||
* | ||
* @author Fabien Potencier <fabien@symfony.com> | ||
*/ | ||
class HttpFoundationExtension extends \Twig_Extension | ||
{ | ||
private $requestStack; | ||
|
||
public function __construct(RequestStack $requestStack) | ||
{ | ||
$this->requestStack = $requestStack; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getFunctions() | ||
{ | ||
return array( | ||
new \Twig_SimpleFunction('absolute_url', array($this, 'generateAbsoluteUrl')), | ||
new \Twig_SimpleFunction('relative_path', array($this, 'generateRelativePath')), | ||
); | ||
} | ||
|
||
/** | ||
* Returns the absolute URL for the given absolute or relative path. | ||
* | ||
* This method returns the path unchanged if no request is available. | ||
* | ||
* @param string $path The path | ||
* | ||
* @return string The absolute URL | ||
* | ||
* @see Request::getUriForPath() | ||
*/ | ||
public function generateAbsoluteUrl($path) | ||
{ | ||
if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The case of URLs starting with |
||
return $path; | ||
} | ||
|
||
if (!$request = $this->requestStack->getMasterRequest()) { | ||
return $path; | ||
} | ||
|
||
if (!$path || '/' !== $path[0]) { | ||
$prefix = $request->getPathInfo(); | ||
$last = strlen($prefix) - 1; | ||
if ($last !== $pos = strrpos($prefix, '/')) { | ||
$prefix = substr($prefix, 0, $pos).'/'; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic above should probably be moved to |
||
|
||
$path = $prefix.$path; | ||
} | ||
|
||
return $request->getUriForPath($path); | ||
} | ||
|
||
/** | ||
* Returns a relative path based on the current Request. | ||
* | ||
* This method returns the path unchanged if no request is available. | ||
* | ||
* @param string $path The path | ||
* | ||
* @return string The relative path | ||
* | ||
* @see Request::getRelativeUriForPath() | ||
*/ | ||
public function generateRelativePath($path) | ||
{ | ||
if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) { | ||
return $path; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not relative There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be better to throw an exception? |
||
} | ||
|
||
if (!$request = $this->requestStack->getMasterRequest()) { | ||
return $path; | ||
} | ||
|
||
return $request->getRelativeUriForPath($path); | ||
} | ||
|
||
/** | ||
* Returns the name of the extension. | ||
* | ||
* @return string The extension name | ||
*/ | ||
public function getName() | ||
{ | ||
return 'request'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?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\Bridge\Twig\Tests\Extension; | ||
|
||
use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\HttpFoundation\Request; | ||
|
||
class HttpFoundationExtensionTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
/** | ||
* @dataProvider getGenerateAbsoluteUrlData() | ||
*/ | ||
public function testGenerateAbsoluteUrl($expected, $path, $pathinfo) | ||
{ | ||
$stack = new RequestStack(); | ||
$stack->push(Request::create($pathinfo)); | ||
$extension = new HttpFoundationExtension($stack); | ||
|
||
$this->assertEquals($expected, $extension->generateAbsoluteUrl($path)); | ||
} | ||
|
||
public function getGenerateAbsoluteUrlData() | ||
{ | ||
return array( | ||
array('http://localhost/foo.png', '/foo.png', '/foo/bar.html'), | ||
array('http://localhost/foo/foo.png', 'foo.png', '/foo/bar.html'), | ||
array('http://localhost/foo/foo.png', 'foo.png', '/foo/bar'), | ||
array('http://localhost/foo/bar/foo.png', 'foo.png', '/foo/bar/'), | ||
|
||
array('http://example.com/baz', 'http://example.com/baz', '/'), | ||
array('https://example.com/baz', 'https://example.com/baz', '/'), | ||
array('//example.com/baz', '//example.com/baz', '/'), | ||
); | ||
} | ||
|
||
/** | ||
* @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)); | ||
$extension = new HttpFoundationExtension($stack); | ||
|
||
$this->assertEquals($expected, $extension->generateRelativePath($path)); | ||
} | ||
|
||
public function getGenerateRelativePathData() | ||
{ | ||
return array( | ||
array('../foo.png', '/foo.png', '/foo/bar.html'), | ||
array('../baz/foo.png', '/baz/foo.png', '/foo/bar.html'), | ||
array('baz/foo.png', 'baz/foo.png', '/foo/bar.html'), | ||
|
||
array('http://example.com/baz', 'http://example.com/baz', '/'), | ||
array('https://example.com/baz', 'https://example.com/baz', '/'), | ||
array('//example.com/baz', '//example.com/baz', '/'), | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1137,6 +1137,61 @@ public function getUriForPath($path) | |
return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path; | ||
} | ||
|
||
/** | ||
* Returns the path as relative reference from the current Request path. | ||
* | ||
* Only the URIs path component (no schema, host etc.) is relevant and must be given. | ||
* Both paths must be absolute and not contain relative parts. | ||
* Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. | ||
* Furthermore, they can be used to reduce the link size in documents. | ||
* | ||
* Example target paths, given a base path of "/a/b/c/d": | ||
* - "/a/b/c/d" -> "" | ||
* - "/a/b/c/" -> "./" | ||
* - "/a/b/" -> "../" | ||
* - "/a/b/c/other" -> "other" | ||
* - "/a/x/y" -> "../../x/y" | ||
* | ||
* @param string $path The target path | ||
* | ||
* @return string The relative target path | ||
*/ | ||
public function getRelativeUriForPath($path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is exactly the same as the code in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe instead of duplicating that code add new class (same like there is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stloyd That would be in a new component as Routing and HttpFoundation are independent. IMO, it's not worth it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it does not. As you can see, the requirement is only for dev. Anyway, the code is now slightly different. |
||
{ | ||
// be sure that we are dealing with an absolute path | ||
if (!isset($path[0]) || '/' !== $path[0]) { | ||
return $path; | ||
} | ||
|
||
if ($path === $basePath = $this->getPathInfo()) { | ||
return ''; | ||
} | ||
|
||
$sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); | ||
$targetDirs = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $path); | ||
array_pop($sourceDirs); | ||
$targetFile = array_pop($targetDirs); | ||
|
||
foreach ($sourceDirs as $i => $dir) { | ||
if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { | ||
unset($sourceDirs[$i], $targetDirs[$i]); | ||
} else { | ||
break; | ||
} | ||
} | ||
|
||
$targetDirs[] = $targetFile; | ||
$path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs); | ||
|
||
// A reference to the same base directory or an empty subdirectory must be prefixed with "./". | ||
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used | ||
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name | ||
// (see http://tools.ietf.org/html/rfc3986#section-4.2). | ||
return !isset($path[0]) || '/' === $path[0] | ||
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) | ||
? "./$path" : $path; | ||
} | ||
|
||
/** | ||
* Generates the normalized query string for the Request. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request
should be probably added to the use statements, or full namespace should be used. We've got two Request classes in Symfony. Similar case in the next docblock.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed