Skip to content

Commit 1e091b9

Browse files
[Routing] Add {foo:bar} syntax to define a mapping between a route parameter and its corresponding request attribute
1 parent 3903840 commit 1e091b9

File tree

6 files changed

+128
-5
lines changed

6 files changed

+128
-5
lines changed

src/Symfony/Component/HttpKernel/EventListener/RouterListener.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,33 @@ public function onKernelRequest(RequestEvent $event): void
110110
'method' => $request->getMethod(),
111111
]);
112112

113-
$request->attributes->add($parameters);
113+
$attributes = $parameters;
114+
if ($mapping = $parameters['_route_mapping'] ?? false) {
115+
unset($parameters['_route_mapping']);
116+
$mappedAttributes = [];
117+
$attributes = [];
118+
119+
foreach ($parameters as $parameter => $value) {
120+
$attribute = $mapping[$parameter] ?? $parameter;
121+
122+
if (!isset($mappedAttributes[$attribute])) {
123+
$attributes[$attribute] = $value;
124+
$mappedAttributes[$attribute] = $parameter;
125+
} elseif ('' !== $mappedAttributes[$attribute]) {
126+
$attributes[$attribute] = [
127+
$mappedAttributes[$attribute] => $attributes[$attribute],
128+
$parameter => $value,
129+
];
130+
$mappedAttributes[$attribute] = '';
131+
} else {
132+
$attributes[$attribute][$parameter] = $value;
133+
}
134+
}
135+
136+
$attributes['_route_mapping'] = $mapping;
137+
}
138+
139+
$request->attributes->add($attributes);
114140
unset($parameters['_route'], $parameters['_controller']);
115141
$request->attributes->set('_route_params', $parameters);
116142
} catch (ResourceNotFoundException $e) {

src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,64 @@ public function testMethodNotAllowedException()
264264
$listener = new RouterListener($urlMatcher, new RequestStack());
265265
$listener->onKernelRequest($event);
266266
}
267+
268+
/**
269+
* @dataProvider provideRouteMapping
270+
*/
271+
public function testRouteMapping(array $expected, array $parameters)
272+
{
273+
$kernel = $this->createMock(HttpKernelInterface::class);
274+
$request = Request::create('http://localhost/');
275+
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
276+
277+
$requestMatcher = $this->createMock(RequestMatcherInterface::class);
278+
$requestMatcher->expects($this->any())
279+
->method('matchRequest')
280+
->with($this->isInstanceOf(Request::class))
281+
->willReturn($parameters);
282+
283+
$listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext());
284+
$listener->onKernelRequest($event);
285+
286+
$expected['_route_mapping'] = $parameters['_route_mapping'];
287+
unset($parameters['_route_mapping']);
288+
$expected['_route_params'] = $parameters;
289+
290+
$this->assertEquals($expected, $request->attributes->all());
291+
}
292+
293+
public static function provideRouteMapping(): iterable
294+
{
295+
yield [
296+
[
297+
'conference' => 'vienna-2024',
298+
],
299+
[
300+
'slug' => 'vienna-2024',
301+
'_route_mapping' => [
302+
'slug' => 'conference',
303+
],
304+
],
305+
];
306+
307+
yield [
308+
[
309+
'article' => [
310+
'id' => 'abc123',
311+
'date' => '2024-04-24',
312+
'slug' => 'symfony-rocks',
313+
],
314+
],
315+
[
316+
'id' => 'abc123',
317+
'date' => '2024-04-24',
318+
'slug' => 'symfony-rocks',
319+
'_route_mapping' => [
320+
'id' => 'article',
321+
'date' => 'article',
322+
'slug' => 'article',
323+
],
324+
],
325+
];
326+
}
267327
}

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.1
5+
---
6+
7+
* Add `{foo:bar}` syntax to define a mapping between a route parameter and its corresponding request attribute
8+
49
7.0
510
---
611

src/Symfony/Component/Routing/Matcher/UrlMatcher.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ protected function getAttributes(Route $route, string $name, array $attributes):
197197
}
198198
$attributes['_route'] = $name;
199199

200+
if ($mapping = $route->getOption('mapping')) {
201+
$attributes['_route_mapping'] = $mapping;
202+
}
203+
200204
return $this->mergeDefaults($attributes, $defaults);
201205
}
202206

src/Symfony/Component/Routing/Route.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,20 +412,31 @@ public function compile(): CompiledRoute
412412

413413
private function extractInlineDefaultsAndRequirements(string $pattern): string
414414
{
415-
if (false === strpbrk($pattern, '?<')) {
415+
if (false === strpbrk($pattern, '?<:')) {
416416
return $pattern;
417417
}
418418

419-
return preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) {
419+
$mapping = $this->getDefault('_route_mapping') ?? [];
420+
421+
$pattern = preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(:[\w\x80-\xFF]++)?(<.*?>)?(\?[^\}]*+)?\}#', function ($m) use (&$mapping) {
422+
if (isset($m[5][0])) {
423+
$this->setDefault($m[2], '?' !== $m[5] ? substr($m[5], 1) : null);
424+
}
420425
if (isset($m[4][0])) {
421-
$this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null);
426+
$this->setRequirement($m[2], substr($m[4], 1, -1));
422427
}
423428
if (isset($m[3][0])) {
424-
$this->setRequirement($m[2], substr($m[3], 1, -1));
429+
$mapping[$m[2]] = substr($m[3], 1);
425430
}
426431

427432
return '{'.$m[1].$m[2].'}';
428433
}, $pattern);
434+
435+
if ($mapping) {
436+
$this->setDefault('_route_mapping', $mapping);
437+
}
438+
439+
return $pattern;
429440
}
430441

431442
private function sanitizeRequirement(string $key, string $regex): string

src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,23 @@ public function testUtf8VarName()
10001000
$this->assertEquals(['_route' => 'foo', 'bär' => 'baz', 'bäz' => 'foo'], $matcher->match('/foo/baz'));
10011001
}
10021002

1003+
public function testMapping()
1004+
{
1005+
$collection = new RouteCollection();
1006+
$collection->add('a', new Route('/conference/{slug:conference}'));
1007+
1008+
$matcher = $this->getUrlMatcher($collection);
1009+
1010+
$expected = [
1011+
'_route' => 'a',
1012+
'slug' => 'vienna-2024',
1013+
'_route_mapping' => [
1014+
'slug' => 'conference',
1015+
],
1016+
];
1017+
$this->assertEquals($expected, $matcher->match('/conference/vienna-2024'));
1018+
}
1019+
10031020
protected function getUrlMatcher(RouteCollection $routes, ?RequestContext $context = null)
10041021
{
10051022
return new UrlMatcher($routes, $context ?? new RequestContext());

0 commit comments

Comments
 (0)