From 1e091b9da3ba67f3baf6e8831b2c7ff712f442c6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 24 Apr 2024 11:31:47 +0200 Subject: [PATCH] [Routing] Add `{foo:bar}` syntax to define a mapping between a route parameter and its corresponding request attribute --- .../EventListener/RouterListener.php | 28 ++++++++- .../EventListener/RouterListenerTest.php | 60 +++++++++++++++++++ src/Symfony/Component/Routing/CHANGELOG.md | 5 ++ .../Component/Routing/Matcher/UrlMatcher.php | 4 ++ src/Symfony/Component/Routing/Route.php | 19 ++++-- .../Routing/Tests/Matcher/UrlMatcherTest.php | 17 ++++++ 6 files changed, 128 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php index a957af8c0c0a..689d08122afb 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php @@ -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) { diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php index e68461a18cfa..d13093db0c55 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php @@ -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', + ], + ], + ]; + } } diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 0a3f28a7672c..bb4f4baf2221 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/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 --- diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index 723803323dbe..09c1d29967cc 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -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); } diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index ac8d8bc6e908..abbc39907ccf 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -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 diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 78bf2b3d75a6..d9cfa7b1bd57 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -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()); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy