Skip to content

Commit

Permalink
[Routing] Add {foo:bar} syntax to define a mapping between a route …
Browse files Browse the repository at this point in the history
…parameter and its corresponding request attribute
  • Loading branch information
nicolas-grekas committed May 2, 2024
1 parent 3903840 commit 1e091b9
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 5 deletions.
Expand Up @@ -110,7 +110,33 @@ public function onKernelRequest(RequestEvent $event): void
'method' => $request->getMethod(),
]);

$request->attributes->add($parameters);
$attributes = $parameters;
if ($mapping = $parameters['_route_mapping'] ?? false) {
unset($parameters['_route_mapping']);
$mappedAttributes = [];
$attributes = [];

foreach ($parameters as $parameter => $value) {
$attribute = $mapping[$parameter] ?? $parameter;

if (!isset($mappedAttributes[$attribute])) {
$attributes[$attribute] = $value;
$mappedAttributes[$attribute] = $parameter;
} elseif ('' !== $mappedAttributes[$attribute]) {
$attributes[$attribute] = [
$mappedAttributes[$attribute] => $attributes[$attribute],
$parameter => $value,
];
$mappedAttributes[$attribute] = '';
} else {
$attributes[$attribute][$parameter] = $value;
}
}

$attributes['_route_mapping'] = $mapping;
}

$request->attributes->add($attributes);
unset($parameters['_route'], $parameters['_controller']);
$request->attributes->set('_route_params', $parameters);
} catch (ResourceNotFoundException $e) {
Expand Down
Expand Up @@ -264,4 +264,64 @@ public function testMethodNotAllowedException()
$listener = new RouterListener($urlMatcher, new RequestStack());
$listener->onKernelRequest($event);
}

/**
* @dataProvider provideRouteMapping
*/
public function testRouteMapping(array $expected, array $parameters)
{
$kernel = $this->createMock(HttpKernelInterface::class);
$request = Request::create('http://localhost/');
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);

$requestMatcher = $this->createMock(RequestMatcherInterface::class);
$requestMatcher->expects($this->any())
->method('matchRequest')
->with($this->isInstanceOf(Request::class))
->willReturn($parameters);

$listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext());
$listener->onKernelRequest($event);

$expected['_route_mapping'] = $parameters['_route_mapping'];
unset($parameters['_route_mapping']);
$expected['_route_params'] = $parameters;

$this->assertEquals($expected, $request->attributes->all());
}

public static function provideRouteMapping(): iterable
{
yield [
[
'conference' => 'vienna-2024',
],
[
'slug' => 'vienna-2024',
'_route_mapping' => [
'slug' => 'conference',
],
],
];

yield [
[
'article' => [
'id' => 'abc123',
'date' => '2024-04-24',
'slug' => 'symfony-rocks',
],
],
[
'id' => 'abc123',
'date' => '2024-04-24',
'slug' => 'symfony-rocks',
'_route_mapping' => [
'id' => 'article',
'date' => 'article',
'slug' => 'article',
],
],
];
}
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.1
---

* Add `{foo:bar}` syntax to define a mapping between a route parameter and its corresponding request attribute

7.0
---

Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Component/Routing/Matcher/UrlMatcher.php
Expand Up @@ -197,6 +197,10 @@ protected function getAttributes(Route $route, string $name, array $attributes):
}
$attributes['_route'] = $name;

if ($mapping = $route->getOption('mapping')) {
$attributes['_route_mapping'] = $mapping;
}

return $this->mergeDefaults($attributes, $defaults);
}

Expand Down
19 changes: 15 additions & 4 deletions src/Symfony/Component/Routing/Route.php
Expand Up @@ -412,20 +412,31 @@ public function compile(): CompiledRoute

private function extractInlineDefaultsAndRequirements(string $pattern): string
{
if (false === strpbrk($pattern, '?<')) {
if (false === strpbrk($pattern, '?<:')) {
return $pattern;
}

return preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) {
$mapping = $this->getDefault('_route_mapping') ?? [];

$pattern = preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(:[\w\x80-\xFF]++)?(<.*?>)?(\?[^\}]*+)?\}#', function ($m) use (&$mapping) {
if (isset($m[5][0])) {
$this->setDefault($m[2], '?' !== $m[5] ? substr($m[5], 1) : null);
}
if (isset($m[4][0])) {
$this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null);
$this->setRequirement($m[2], substr($m[4], 1, -1));
}
if (isset($m[3][0])) {
$this->setRequirement($m[2], substr($m[3], 1, -1));
$mapping[$m[2]] = substr($m[3], 1);
}

return '{'.$m[1].$m[2].'}';
}, $pattern);

if ($mapping) {
$this->setDefault('_route_mapping', $mapping);
}

return $pattern;
}

private function sanitizeRequirement(string $key, string $regex): string
Expand Down
17 changes: 17 additions & 0 deletions src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
Expand Up @@ -1000,6 +1000,23 @@ public function testUtf8VarName()
$this->assertEquals(['_route' => 'foo', 'bär' => 'baz', 'bäz' => 'foo'], $matcher->match('/foo/baz'));
}

public function testMapping()
{
$collection = new RouteCollection();
$collection->add('a', new Route('/conference/{slug:conference}'));

$matcher = $this->getUrlMatcher($collection);

$expected = [
'_route' => 'a',
'slug' => 'vienna-2024',
'_route_mapping' => [
'slug' => 'conference',
],
];
$this->assertEquals($expected, $matcher->match('/conference/vienna-2024'));
}

protected function getUrlMatcher(RouteCollection $routes, ?RequestContext $context = null)
{
return new UrlMatcher($routes, $context ?? new RequestContext());
Expand Down

0 comments on commit 1e091b9

Please sign in to comment.