Skip to content

[Twig] added subpath function #3965

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

Closed
wants to merge 2 commits into from
Closed
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
61 changes: 58 additions & 3 deletions src/Symfony/Bridge/Twig/Extension/RoutingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Bridge\Twig\Extension;

use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
Expand All @@ -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;
}

/**
Expand All @@ -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);
}

/**
Expand Down
72 changes: 72 additions & 0 deletions src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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),
);
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Bridge/Twig/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -31,6 +32,7 @@
},
"suggest": {
"symfony/form": "",
"symfony/http-foundation": "",
"symfony/http-kernel": "",
"symfony/routing": "",
"symfony/templating": "",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@

<service id="twig.extension.routing" class="%twig.extension.routing.class%" public="false">
<argument type="service" id="router" />
<argument type="service" id="request_stack" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

twig bundle doesnt have the framework bundle as dependency yet. but since the twig bundle relies on service definitions in framework bundle, shouldn't it be the case?!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, the Twig bridge is used by Silex without the definition as we are using Pimple there, so this is not a hard dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the twig bundle, not the bridge.

</service>

<service id="twig.extension.yaml" class="%twig.extension.yaml.class%" public="false">
Expand Down