From 9cd8dd37a5b04b1e2f2a111fc6f3d7b4337f5ed8 Mon Sep 17 00:00:00 2001 From: Benjamin Morel Date: Thu, 22 May 2025 09:35:49 +0200 Subject: [PATCH 1/7] Allow query-specific parameters in URL generator using `_query` --- CHANGELOG.md | 5 ++ Generator/UrlGenerator.php | 13 +++++ Tests/Generator/UrlGeneratorTest.php | 74 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d21e550f..4ef96d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Allow query-specific parameters in `UrlGenerator` using `_query` + 7.3 --- diff --git a/Generator/UrlGenerator.php b/Generator/UrlGenerator.php index 216b0d54..d82b9189 100644 --- a/Generator/UrlGenerator.php +++ b/Generator/UrlGenerator.php @@ -142,6 +142,18 @@ public function generate(string $name, array $parameters = [], int $referenceTyp */ protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []): string { + $queryParameters = []; + + if (isset($parameters['_query'])) { + if (\is_array($parameters['_query'])) { + $queryParameters = $parameters['_query']; + unset($parameters['_query']); + } else { + trigger_deprecation('symfony/routing', '7.4', 'Parameter "_query" is reserved for passing an array of query parameters. Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.'); + // throw new InvalidParameterException('Parameter "_query" must be an array of query parameters.'); + } + } + $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); @@ -260,6 +272,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem // add a query string if needed $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); + $extra = array_merge($extra, $queryParameters); array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { if (\is_object($v)) { diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index 25a4c674..27af7679 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -1054,6 +1054,80 @@ public function testUtf8VarName() $this->assertSame('/app.php/foo/baz', $this->getGenerator($routes)->generate('test', ['bär' => 'baz'])); } + public function testQueryParameters() + { + $routes = $this->getRoutes('user', new Route('/user/{username}')); + $url = $this->getGenerator($routes)->generate('user', [ + 'username' => 'john', + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'baz', + '_query' => [ + 'a' => '123', + 'd' => '789', + ], + ]); + $this->assertSame('/app.php/user/john?a=123&b=bar&c=baz&d=789', $url); + } + + public function testRouteHostParameterAndQueryParameterWithSameName() + { + $routes = $this->getRoutes('admin_stats', new Route('/admin/stats', requirements: ['domain' => '.+'], host: '{siteCode}.{domain}')); + $url = $this->getGenerator($routes)->generate('admin_stats', [ + 'siteCode' => 'fr', + 'domain' => 'example.com', + '_query' => [ + 'siteCode' => 'us', + ], + ], UrlGeneratorInterface::NETWORK_PATH); + $this->assertSame('//fr.example.com/app.php/admin/stats?siteCode=us', $url); + } + + public function testRoutePathParameterAndQueryParameterWithSameName() + { + $routes = $this->getRoutes('user', new Route('/user/{id}')); + $url = $this->getGenerator($routes)->generate('user', [ + 'id' => '123', + '_query' => [ + 'id' => '456', + ], + ]); + $this->assertSame('/app.php/user/123?id=456', $url); + } + + public function testQueryParameterCannotSubstituteRouteParameter() + { + $routes = $this->getRoutes('user', new Route('/user/{id}')); + + $this->expectException(MissingMandatoryParametersException::class); + $this->expectExceptionMessage('Some mandatory parameters are missing ("id") to generate a URL for route "user".'); + + $this->getGenerator($routes)->generate('user', [ + '_query' => [ + 'id' => '456', + ], + ]); + } + + /** + * @group legacy + */ + public function testQueryParametersWithScalarValue() + { + $routes = $this->getRoutes('user', new Route('/user/{id}')); + + $this->expectDeprecation( + 'Since symfony/routing 7.4: Parameter "_query" is reserved for passing an array of query parameters. ' . + 'Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.', + ); + + $url = $this->getGenerator($routes)->generate('user', [ + 'id' => '123', + '_query' => 'foo', + ]); + $this->assertSame('/app.php/user/123?_query=foo', $url); + } + protected function getGenerator(RouteCollection $routes, array $parameters = [], $logger = null, ?string $defaultLocale = null) { $context = new RequestContext('/app.php'); From c71567120c75759c0803673129b0aa2c0684afe6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 16:08:14 +0200 Subject: [PATCH 2/7] Allow Symfony ^8.0 --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 59e30bef..1fcc24b6 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,11 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "psr/log": "^1|^2|^3" }, "conflict": { From 608459eef42b6f4cf9e7dc34a9529e48c99bd56f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 17:50:55 +0200 Subject: [PATCH 3/7] Bump Symfony 8 to PHP >= 8.4 --- composer.json | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 1fcc24b6..d6588faf 100644 --- a/composer.json +++ b/composer.json @@ -16,21 +16,16 @@ } ], "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Routing\\": "" }, From 589b5de87da2f3afaf5e0c23f28498d313c391b3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 4 Jun 2025 18:31:05 +0200 Subject: [PATCH 4/7] Enforce return types on all components --- Loader/AttributeClassLoader.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Loader/AttributeClassLoader.php b/Loader/AttributeClassLoader.php index 254582bf..04d1db17 100644 --- a/Loader/AttributeClassLoader.php +++ b/Loader/AttributeClassLoader.php @@ -273,10 +273,8 @@ public function getResolver(): LoaderResolverInterface /** * Gets the default route name for a class method. - * - * @return string */ - protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method): string { $name = str_replace('\\', '_', $class->name).'_'.$method->name; $name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name); @@ -375,10 +373,8 @@ protected function createRoute(string $path, array $defaults, array $requirement /** * @param RouteAttribute $attr or an object that exposes a similar interface - * - * @return void */ - abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $attr); + abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $attr): void; /** * @return iterable From 1366ed8a23adefd546ceb975f70ad6a16520120c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 14 Jun 2025 22:01:30 +0200 Subject: [PATCH 5/7] replace expectDeprecation() with expectUserDeprecationMessage() --- Tests/Generator/UrlGeneratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index 27af7679..75196bd2 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -1116,7 +1116,7 @@ public function testQueryParametersWithScalarValue() { $routes = $this->getRoutes('user', new Route('/user/{id}')); - $this->expectDeprecation( + $this->expectUserDeprecationMessage( 'Since symfony/routing 7.4: Parameter "_query" is reserved for passing an array of query parameters. ' . 'Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.', ); From 12f7de4a4fc75f705856f160917f48f62a201507 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sat, 21 Jun 2025 13:27:44 +0200 Subject: [PATCH 6/7] [Routing] Throw exception for non-array _query parameter --- CHANGELOG.md | 5 +++++ Generator/UrlGenerator.php | 3 +-- Tests/Generator/UrlGeneratorTest.php | 11 ++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef96d53..1c9b7453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +8.0 +--- + + * Providing a non-array `_query` parameter to `UrlGenerator` causes an `InvalidParameterException` + 7.4 --- diff --git a/Generator/UrlGenerator.php b/Generator/UrlGenerator.php index d82b9189..32d57e9c 100644 --- a/Generator/UrlGenerator.php +++ b/Generator/UrlGenerator.php @@ -149,8 +149,7 @@ protected function doGenerate(array $variables, array $defaults, array $requirem $queryParameters = $parameters['_query']; unset($parameters['_query']); } else { - trigger_deprecation('symfony/routing', '7.4', 'Parameter "_query" is reserved for passing an array of query parameters. Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.'); - // throw new InvalidParameterException('Parameter "_query" must be an array of query parameters.'); + throw new InvalidParameterException('Parameter "_query" must be an array of query parameters.'); } } diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index 75196bd2..72eb1af7 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -1109,23 +1109,16 @@ public function testQueryParameterCannotSubstituteRouteParameter() ]); } - /** - * @group legacy - */ public function testQueryParametersWithScalarValue() { $routes = $this->getRoutes('user', new Route('/user/{id}')); - $this->expectUserDeprecationMessage( - 'Since symfony/routing 7.4: Parameter "_query" is reserved for passing an array of query parameters. ' . - 'Passing a scalar value is deprecated and will throw an exception in Symfony 8.0.', - ); + $this->expectException(InvalidParameterException::class); - $url = $this->getGenerator($routes)->generate('user', [ + $this->getGenerator($routes)->generate('user', [ 'id' => '123', '_query' => 'foo', ]); - $this->assertSame('/app.php/user/123?_query=foo', $url); } protected function getGenerator(RouteCollection $routes, array $parameters = [], $logger = null, ?string $defaultLocale = null) From 6d44831e1acd8b4a3fc8988e418696ecf2a9090b Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sat, 21 Jun 2025 13:16:44 +0200 Subject: [PATCH 7/7] [Routing] Remove deprecated AttributeClassLoader property and setter --- CHANGELOG.md | 1 + Loader/AttributeClassLoader.php | 23 ++--------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9b7453..5aa63900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Providing a non-array `_query` parameter to `UrlGenerator` causes an `InvalidParameterException` + * Remove the protected `AttributeClassLoader::$routeAnnotationClass` property and the `setRouteAnnotationClass()` method, use `AttributeClassLoader::setRouteAttributeClass()` instead 7.4 --- diff --git a/Loader/AttributeClassLoader.php b/Loader/AttributeClassLoader.php index 04d1db17..58494045 100644 --- a/Loader/AttributeClassLoader.php +++ b/Loader/AttributeClassLoader.php @@ -55,10 +55,6 @@ */ abstract class AttributeClassLoader implements LoaderInterface { - /** - * @deprecated since Symfony 7.2, use "setRouteAttributeClass()" instead. - */ - protected string $routeAnnotationClass = RouteAttribute::class; private string $routeAttributeClass = RouteAttribute::class; protected int $defaultRouteIndex = 0; @@ -67,24 +63,11 @@ public function __construct( ) { } - /** - * @deprecated since Symfony 7.2, use "setRouteAttributeClass(string $class)" instead - * - * Sets the annotation class to read route properties from. - */ - public function setRouteAnnotationClass(string $class): void - { - trigger_deprecation('symfony/routing', '7.2', 'The "%s()" method is deprecated, use "%s::setRouteAttributeClass()" instead.', __METHOD__, self::class); - - $this->setRouteAttributeClass($class); - } - /** * Sets the attribute class to read route properties from. */ public function setRouteAttributeClass(string $class): void { - $this->routeAnnotationClass = $class; $this->routeAttributeClass = $class; } @@ -293,8 +276,7 @@ protected function getGlobals(\ReflectionClass $class): array { $globals = $this->resetGlobals(); - // to be replaced in Symfony 8.0 by $this->routeAttributeClass - if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + if ($attribute = $class->getAttributes($this->routeAttributeClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { $attr = $attribute->newInstance(); if (null !== $attr->getName()) { @@ -381,8 +363,7 @@ abstract protected function configureRoute(Route $route, \ReflectionClass $class */ private function getAttributes(\ReflectionClass|\ReflectionMethod $reflection): iterable { - // to be replaced in Symfony 8.0 by $this->routeAttributeClass - foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + foreach ($reflection->getAttributes($this->routeAttributeClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { yield $attribute->newInstance(); } }