feature #29599 [Routing] Allow force-generation of trailing parameters using eg "/exports/news.{!_format}" (zavulon)

This PR was squashed before being merged into the 4.3-dev branch (closes #29599).

Discussion
----------

[Routing] Allow force-generation of trailing parameters using eg "/exports/news.{!_format}"

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #29593
| License       | MIT

When a route is defined as path: `/exports/news.{!_format}`, we should force `_format` be defined in `defaults` and the generator should generate URLs with that default when none is provided (should work with any parameter of course).

Commits
-------

9fab3d62ec [Routing] Allow force-generation of trailing parameters using eg \"/exports/news.{!_format}\"
This commit is contained in:
Nicolas Grekas 2018-12-24 10:28:22 +01:00
commit 57dad6640b
4 changed files with 48 additions and 6 deletions

View File

@ -156,21 +156,26 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
$message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.';
foreach ($tokens as $token) {
if ('variable' === $token[0]) {
if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) {
$varName = $token[3];
if ($important = ('!' === $varName[0])) {
$varName = substr($varName, 1);
}
if (!$optional || $important || !array_key_exists($varName, $defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) {
// check requirement
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) {
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#'.(empty($token[4]) ? '' : 'u'), $mergedParams[$varName])) {
if ($this->strictRequirements) {
throw new InvalidParameterException(strtr($message, array('{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]])));
throw new InvalidParameterException(strtr($message, array('{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName])));
}
if ($this->logger) {
$this->logger->error($message, array('parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]));
$this->logger->error($message, array('parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]));
}
return;
}
$url = $token[1].$mergedParams[$token[3]].$url;
$url = $token[1].$mergedParams[$varName].$url;
$optional = false;
}
} else {

View File

@ -111,7 +111,7 @@ class RouteCompiler implements RouteCompilerInterface
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
preg_match_all('#\{\w+\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
preg_match_all('#\{!?\w+\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach ($matches as $match) {
$varName = substr($match[0][0], 1, -1);
// get all static text preceding the current variable
@ -184,6 +184,9 @@ class RouteCompiler implements RouteCompilerInterface
}
$tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
if ('!' === $varName[0]) {
$varName = substr($varName, 1);
}
$variables[] = $varName;
}
@ -283,6 +286,10 @@ class RouteCompiler implements RouteCompilerInterface
// Text tokens
return preg_quote($token[1], self::REGEX_DELIMITER);
} else {
if ('variable' === $token[0] && '!' === $token[3][0]) {
$token[3] = substr($token[3], 1);
}
// Variable tokens
if (0 === $index && 0 === $firstOptional) {
// When the only token is an optional variable token, the separator is required

View File

@ -397,6 +397,27 @@ class UrlGeneratorTest extends TestCase
$this->assertSame('/app.php/index.mobile.html', $generator->generate('test', array('page' => 'index', '_format' => 'mobile.html')));
}
public function testImportantVariable()
{
$routes = $this->getRoutes('test', (new Route('/{page}.{!_format}'))->addDefaults(array('_format' => 'mobile.html')));
$generator = $this->getGenerator($routes);
$this->assertSame('/app.php/index.xml', $generator->generate('test', array('page' => 'index', '_format' => 'xml')));
$this->assertSame('/app.php/index.mobile.html', $generator->generate('test', array('page' => 'index', '_format' => 'mobile.html')));
$this->assertSame('/app.php/index.mobile.html', $generator->generate('test', array('page' => 'index')));
}
/**
* @expectedException \Symfony\Component\Routing\Exception\MissingMandatoryParametersException
*/
public function testImportantVariableWithNoDefault()
{
$routes = $this->getRoutes('test', new Route('/{page}.{!_format}'));
$generator = $this->getGenerator($routes);
$generator->generate('test', array('page' => 'index'));
}
/**
* @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
*/

View File

@ -177,6 +177,15 @@ class UrlMatcherTest extends TestCase
$this->assertEquals(array('_route' => '$péß^a|'), $matcher->match('/bar'));
}
public function testMatchImportantVariable()
{
$collection = new RouteCollection();
$collection->add('index', new Route('/index.{!_format}', array('_format' => 'xml')));
$matcher = $this->getUrlMatcher($collection);
$this->assertEquals(array('_route' => 'index', '_format' => 'xml'), $matcher->match('/index.xml'));
}
/**
* @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
*/