diff --git a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php
index 237c36c190c22..4af94f6c06d49 100644
--- a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php
+++ b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php
@@ -11,6 +11,7 @@
namespace Symfony\Bridge\Twig\Extension;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
@@ -20,11 +21,26 @@
*/
class RoutingExtension extends \Twig_Extension
{
+ /**
+ * @var UrlGeneratorInterface
+ */
private $generator;
- public function __construct(UrlGeneratorInterface $generator)
+ /**
+ * @var RequestStack|null
+ */
+ private $requestStack;
+
+ /**
+ * Constructor.
+ *
+ * @param UrlGeneratorInterface $generator A UrlGeneratorInterface instance
+ * @param RequestStack|null $requestStack An optional stack containing master/sub requests
+ */
+ public function __construct(UrlGeneratorInterface $generator, RequestStack $requestStack = null)
{
$this->generator = $generator;
+ $this->requestStack = $requestStack;
}
/**
@@ -37,17 +53,56 @@ public function getFunctions()
return array(
new \Twig_SimpleFunction('url', array($this, 'getUrl'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
new \Twig_SimpleFunction('path', array($this, 'getPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
+ new \Twig_SimpleFunction('subpath', array($this, 'getSubPath')),
);
}
+ public function getUrl($name, $parameters = array(), $schemeRelative = false)
+ {
+ return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
public function getPath($name, $parameters = array(), $relative = false)
{
return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
}
- public function getUrl($name, $parameters = array(), $schemeRelative = false)
+ /**
+ * Returns the relative path to a route (defaults to current route) with all current route parameters merged
+ * with the passed params.
+ *
+ * Optionally one can also include the params of the query string. It's also possible to remove existing
+ * params by passing null as value of a specific parameter. The path is a relative path to the
+ * target URL, e.g. "../slug", based on the current request path.
+ *
+ * Beware when using this method in a subrequest as it will use the params of the subrequest and will
+ * also generate a relative path based on it. The resulting relative reference is probably the wrong
+ * target when resolved by the user agent (browser) based on the main request.
+ *
+ * @param string $name The route name (when empty it defaults to the current route)
+ * @param array $parameters The parameters that should be added or overwrite existing params
+ * @param Boolean $includeQuery Whether the current params in the query string should be included (disabled by default)
+ *
+ * @return string The path to the route with given parameters
+ *
+ * @throws \LogicException when the request is not set
+ */
+ public function getSubPath($name = '', array $parameters = array(), $includeQuery = false)
{
- return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
+ if (null === $this->requestStack || null === $request = $this->requestStack->getCurrentRequest()) {
+ throw new \LogicException('The subpath function needs the request to be set in the request stack.');
+ }
+
+ $parameters = array_replace(
+ $includeQuery ? $request->query->all() : array(),
+ $request->attributes->get('_route_params', array()),
+ $parameters
+ );
+ $parameters = array_filter($parameters, function ($value) {
+ return null !== $value;
+ });
+
+ return $this->generator->generate($name ?: $request->attributes->get('_route', ''), $parameters, UrlGeneratorInterface::RELATIVE_PATH);
}
/**
diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php
index cd0bbdf0ec7d4..a3da4e7be4c8e 100644
--- a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php
+++ b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php
@@ -12,9 +12,79 @@
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Bridge\Twig\Extension\RoutingExtension;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
class RoutingExtensionTest extends \PHPUnit_Framework_TestCase
{
+ public function testUrlGeneration()
+ {
+ $routes = new RouteCollection();
+ $routes->add('dir', new Route('/{dir}/'));
+ $routes->add('page', new Route('/{dir}/{page}.{_format}', array('_format' => 'html')));
+ $routes->add('comments', new Route('/{dir}/{page}/comments'));
+
+ $request = Request::create('http://example.com/dir/page?foo=bar&test=test');
+ $request->attributes->set('_route', 'page');
+ $request->attributes->set('_route_params', array('dir' => 'dir', 'page' => 'page', '_format' => 'html'));
+
+ $requestStack = new RequestStack();
+ $requestStack->push($request);
+
+ $context = new RequestContext();
+ $context->fromRequest($request);
+ $generator = new UrlGenerator($routes, $context);
+
+ $extension = new RoutingExtension($generator, $requestStack);
+
+ $this->assertSame('http://example.com/dir/page/comments', $extension->getUrl('comments', array('dir' => 'dir', 'page' => 'page')));
+ $this->assertSame('//example.com/dir/page/comments', $extension->getUrl('comments', array('dir' => 'dir', 'page' => 'page'), true));
+
+ $this->assertSame('/dir/page/comments', $extension->getPath('comments', array('dir' => 'dir', 'page' => 'page')));
+ $this->assertSame('page/comments', $extension->getPath('comments', array('dir' => 'dir', 'page' => 'page'), true));
+
+ $this->assertSame('page.pdf', $extension->getSubPath('', array('_format' => 'pdf')));
+ $this->assertSame('?test=test', $extension->getSubPath('', array('test' => 'test'), false));
+ $this->assertSame('?foo=bar&test=test&extra=extra', $extension->getSubPath('', array('extra' => 'extra'), true));
+ $this->assertSame('?foo=bar&extra=extra', $extension->getSubPath('', array('extra' => 'extra', 'test' => null), true));
+ $this->assertSame('otherpage.json?foo=bar&test=test&extra=extra', $extension->getSubPath('', array('extra' => 'extra', 'page' => 'otherpage', '_format' => 'json'), true));
+ $this->assertSame('page/comments', $extension->getSubPath('comments', array('_format' => null)));
+ $this->assertSame('./', $extension->getSubPath('dir', array('page' => null, '_format' => null)));
+ $this->assertSame('./?foo=bar', $extension->getSubPath('dir', array('page' => null, '_format' => null, 'test' => null), true));
+ $this->assertSame('../otherdir/page.xml', $extension->getSubPath('page', array('dir' => 'otherdir', '_format' => 'xml')));
+
+ // we remove the request query string, so the resulting empty relative reference is actually correct for the current url and includeQuery=false
+ $context->setQueryString('');
+ $this->assertSame('', $extension->getSubPath());
+ }
+
+ public function testPlaceholdersHaveHigherPriorityThanQueryInSubPath()
+ {
+ $routes = new RouteCollection();
+ $routes->add('page', new Route('/{page}'));
+
+ $request = Request::create('http://example.com/mypage?page=querypage&bar=test');
+ $request->attributes->set('_route', 'page');
+ $request->attributes->set('_route_params', array('page' => 'mypage'));
+
+ $requestStack = new RequestStack();
+ $requestStack->push($request);
+
+ $context = new RequestContext();
+ $context->fromRequest($request);
+ $generator = new UrlGenerator($routes, $context);
+
+ $extension = new RoutingExtension($generator, $requestStack);
+
+ $this->assertStringStartsNotWith('querypage', $extension->getSubPath('', array(), true),
+ 'when the request query string has a parameter with the same name as a placeholder, the query param is ignored when includeQuery=true'
+ );
+ }
+
/**
* @dataProvider getEscapingTemplates
*/
@@ -45,6 +115,8 @@ public function getEscapingTemplates()
array('{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}', true),
array('{{ path(name = "foo", parameters = { foo: foo }) }}', true),
array('{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}', true),
+
+ array('{{ subpath("foo") }}', true),
);
}
}
diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json
index 5797586731e54..1e13d5eef0fad 100644
--- a/src/Symfony/Bridge/Twig/composer.json
+++ b/src/Symfony/Bridge/Twig/composer.json
@@ -21,6 +21,7 @@
},
"require-dev": {
"symfony/form": "~2.2",
+ "symfony/http-foundation": "~2.4",
"symfony/http-kernel": "~2.2",
"symfony/routing": "~2.2",
"symfony/templating": "~2.1",
@@ -31,6 +32,7 @@
},
"suggest": {
"symfony/form": "",
+ "symfony/http-foundation": "",
"symfony/http-kernel": "",
"symfony/routing": "",
"symfony/templating": "",
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
index 48c0055d9cfea..0f5f14063a36a 100644
--- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
+++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
@@ -81,6 +81,7 @@
+