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 @@ +