From 9cd8dd37a5b04b1e2f2a111fc6f3d7b4337f5ed8 Mon Sep 17 00:00:00 2001 From: Benjamin Morel Date: Thu, 22 May 2025 09:35:49 +0200 Subject: [PATCH 01/44] 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 02/44] 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 03/44] 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 04/44] 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 05/44] 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 06/44] [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 07/44] [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(); } } From 2a7b4777f5ae8258bbb47e811515d27d2cb9f307 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 8 Jul 2025 11:08:29 +0200 Subject: [PATCH 08/44] Various CS fixes --- 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 75196bd2..d513b318 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -1117,7 +1117,7 @@ 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. ' . + '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 74f73c5a5e7a20981990618176f4749277911abd Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Sat, 12 Jul 2025 15:55:19 +0200 Subject: [PATCH 09/44] optimize `in_array` calls --- Loader/AttributeFileLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loader/AttributeFileLoader.php b/Loader/AttributeFileLoader.php index 3214d589..2c52d239 100644 --- a/Loader/AttributeFileLoader.php +++ b/Loader/AttributeFileLoader.php @@ -115,7 +115,7 @@ protected function findClass(string $file): string|false if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { $skipClassToken = true; break; - } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { + } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT], true)) { break; } } From aff4fac9b6d5ee2949cad4d6c688822d96b9b9a4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 24 Jul 2025 14:45:41 +0200 Subject: [PATCH 10/44] Fix typos --- Tests/Fixtures/dumper/compiled_url_matcher13.php | 6 +++--- Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/Fixtures/dumper/compiled_url_matcher13.php b/Tests/Fixtures/dumper/compiled_url_matcher13.php index 63252943..466550c3 100644 --- a/Tests/Fixtures/dumper/compiled_url_matcher13.php +++ b/Tests/Fixtures/dumper/compiled_url_matcher13.php @@ -11,15 +11,15 @@ ], [ // $regexpList 0 => '{^(?' - .'|(?i:([^\\.]++)\\.exampple\\.com)\\.(?' + .'|(?i:([^\\.]++)\\.example\\.com)\\.(?' .'|/abc([^/]++)(?' - .'|(*:56)' + .'|(*:55)' .')' .')' .')/?$}sD', ], [ // $dynamicRoutes - 56 => [ + 55 => [ [['_route' => 'r1'], ['foo', 'foo'], null, null, false, true, null], [['_route' => 'r2'], ['foo', 'foo'], null, null, false, true, null], [null, null, null, null, false, false, 0], diff --git a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php index d6be915a..1bb2c2e3 100644 --- a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php +++ b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php @@ -439,8 +439,8 @@ public static function getRouteCollections() /* test case 13 */ $hostCollection = new RouteCollection(); - $hostCollection->add('r1', (new Route('abc{foo}'))->setHost('{foo}.exampple.com')); - $hostCollection->add('r2', (new Route('abc{foo}'))->setHost('{foo}.exampple.com')); + $hostCollection->add('r1', (new Route('abc{foo}'))->setHost('{foo}.example.com')); + $hostCollection->add('r2', (new Route('abc{foo}'))->setHost('{foo}.example.com')); /* test case 14 */ $fixedLocaleCollection = new RouteCollection(); From 8bd9f39190e4f5114031362700a1c034b7ca9e3d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 9 Oct 2024 11:06:51 +0200 Subject: [PATCH 11/44] run tests using PHPUnit 11.5 --- .../Dumper/CompiledUrlGeneratorDumperTest.php | 20 ++++++--------- Tests/Generator/UrlGeneratorTest.php | 25 ++++++++----------- phpunit.xml.dist | 11 +++++--- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php b/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php index 8edc49a6..d0e35e81 100644 --- a/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php +++ b/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Routing\Tests\Generator\Dumper; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Generator\CompiledUrlGenerator; @@ -24,8 +25,6 @@ class CompiledUrlGeneratorDumperTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - private RouteCollection $routeCollection; private CompiledUrlGeneratorDumper $generatorDumper; private string $testTmpFilepath; @@ -338,9 +337,8 @@ public function testIndirectCircularReferenceShouldThrowAnException() $this->generatorDumper->dump(); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testDeprecatedAlias() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: The "b" route alias is deprecated. You should stop using it, as it will be removed in the future.'); @@ -356,9 +354,8 @@ public function testDeprecatedAlias() $compiledUrlGenerator->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testDeprecatedAliasWithCustomMessage() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -374,9 +371,8 @@ public function testDeprecatedAliasWithCustomMessage() $compiledUrlGenerator->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testTargettingADeprecatedAliasShouldTriggerDeprecation() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index d513b318..79caf33e 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -11,9 +11,10 @@ namespace Symfony\Component\Routing\Tests\Generator; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; @@ -26,8 +27,6 @@ class UrlGeneratorTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - public function testAbsoluteUrlWithPort80() { $routes = $this->getRoutes('test', new Route('/testing')); @@ -806,9 +805,8 @@ public function testAliasWhichTargetRouteDoesntExist() $this->getGenerator($routes)->generate('d'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testDeprecatedAlias() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: The "b" route alias is deprecated. You should stop using it, as it will be removed in the future.'); @@ -821,9 +819,8 @@ public function testDeprecatedAlias() $this->getGenerator($routes)->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testDeprecatedAliasWithCustomMessage() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -836,9 +833,8 @@ public function testDeprecatedAliasWithCustomMessage() $this->getGenerator($routes)->generate('b'); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testTargettingADeprecatedAliasShouldTriggerDeprecation() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -1109,9 +1105,8 @@ public function testQueryParameterCannotSubstituteRouteParameter() ]); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testQueryParametersWithScalarValue() { $routes = $this->getRoutes('user', new Route('/user/{id}')); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 587ee4c0..6d89fd81 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,11 @@ @@ -18,7 +19,7 @@ - + ./ @@ -26,5 +27,9 @@ ./Tests ./vendor - + + + + + From 22b0f6b1d4c97a58654d8326f58e76cc832869de Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 31 Jul 2025 14:36:46 +0200 Subject: [PATCH 12/44] replace PHPUnit annotations with attributes --- Tests/Attribute/RouteTest.php | 5 +- Tests/Generator/UrlGeneratorTest.php | 17 +- Tests/Loader/AttributeClassLoaderTest.php | 5 +- Tests/Loader/ContainerLoaderTest.php | 5 +- Tests/Loader/ObjectLoaderTest.php | 5 +- Tests/Loader/PhpFileLoaderTest.php | 5 +- Tests/Loader/Psr4DirectoryLoaderTest.php | 9 +- Tests/Loader/XmlFileLoaderTest.php | 17 +- Tests/Loader/YamlFileLoaderTest.php | 13 +- .../Dumper/CompiledUrlMatcherDumperTest.php | 5 +- .../Dumper/StaticPrefixCollectionTest.php | 5 +- .../ExpressionLanguageProviderTest.php | 9 +- Tests/RequestContextTest.php | 23 +- Tests/Requirement/EnumRequirementTest.php | 5 +- Tests/Requirement/RequirementTest.php | 412 +++++++----------- Tests/RouteCompilerTest.php | 21 +- Tests/RouteTest.php | 25 +- 17 files changed, 229 insertions(+), 357 deletions(-) diff --git a/Tests/Attribute/RouteTest.php b/Tests/Attribute/RouteTest.php index bbaa7563..8ff0a4dd 100644 --- a/Tests/Attribute/RouteTest.php +++ b/Tests/Attribute/RouteTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests\Attribute; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\FooController; class RouteTest extends TestCase { - /** - * @dataProvider getValidParameters - */ + #[DataProvider('getValidParameters')] public function testLoadFromAttribute(string $methodName, string $getter, mixed $expectedReturn) { $route = (new \ReflectionMethod(FooController::class, $methodName))->getAttributes(Route::class)[0]->newInstance(); diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index 79caf33e..b6798cfe 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Generator; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; @@ -109,9 +110,7 @@ public function testNotPassedOptionalParameterInBetween() $this->assertSame('/app.php/', $this->getGenerator($routes)->generate('test')); } - /** - * @dataProvider valuesProvider - */ + #[DataProvider('valuesProvider')] public function testRelativeUrlWithExtraParameters(string $expectedQueryString, string $parameter, $value) { $routes = $this->getRoutes('test', new Route('/testing')); @@ -120,9 +119,7 @@ public function testRelativeUrlWithExtraParameters(string $expectedQueryString, $this->assertSame('/app.php/testing'.$expectedQueryString, $url); } - /** - * @dataProvider valuesProvider - */ + #[DataProvider('valuesProvider')] public function testAbsoluteUrlWithExtraParameters(string $expectedQueryString, string $parameter, $value) { $routes = $this->getRoutes('test', new Route('/testing')); @@ -886,9 +883,7 @@ public function testIndirectCircularReferenceShouldThrowAnException() $this->getGenerator($routes)->generate('a'); } - /** - * @dataProvider provideRelativePaths - */ + #[DataProvider('provideRelativePaths')] public function testGetRelativePath($sourcePath, $targetPath, $expectedPath) { $this->assertSame($expectedPath, UrlGenerator::getRelativePath($sourcePath, $targetPath)); @@ -1027,9 +1022,7 @@ public function testFragmentsCanBeDefinedAsDefaults() $this->assertEquals('/app.php/testing#fragment', $url); } - /** - * @dataProvider provideLookAroundRequirementsInPath - */ + #[DataProvider('provideLookAroundRequirementsInPath')] public function testLookRoundRequirementsInPath($expected, $path, $requirement) { $routes = $this->getRoutes('test', new Route($path, [], ['foo' => $requirement, 'baz' => '.+?'])); diff --git a/Tests/Loader/AttributeClassLoaderTest.php b/Tests/Loader/AttributeClassLoaderTest.php index 50a10a16..2fa00e07 100644 --- a/Tests/Loader/AttributeClassLoaderTest.php +++ b/Tests/Loader/AttributeClassLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Alias; use Symfony\Component\Routing\Exception\LogicException; @@ -67,9 +68,7 @@ public function testGetResolver() $loader->getResolver(); } - /** - * @dataProvider provideTestSupportsChecksResource - */ + #[DataProvider('provideTestSupportsChecksResource')] public function testSupportsChecksResource($resource, $expectedSupports) { $this->assertSame($expectedSupports, $this->loader->supports($resource), '->supports() returns true if the resource is loadable'); diff --git a/Tests/Loader/ContainerLoaderTest.php b/Tests/Loader/ContainerLoaderTest.php index e4f99238..967a17b1 100644 --- a/Tests/Loader/ContainerLoaderTest.php +++ b/Tests/Loader/ContainerLoaderTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Routing\Loader\ContainerLoader; class ContainerLoaderTest extends TestCase { - /** - * @dataProvider supportsProvider - */ + #[DataProvider('supportsProvider')] public function testSupports(bool $expected, ?string $type = null) { $this->assertSame($expected, (new ContainerLoader(new Container()))->supports('foo', $type)); diff --git a/Tests/Loader/ObjectLoaderTest.php b/Tests/Loader/ObjectLoaderTest.php index 42743fed..1b0d2673 100644 --- a/Tests/Loader/ObjectLoaderTest.php +++ b/Tests/Loader/ObjectLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Loader\ObjectLoader; use Symfony\Component\Routing\Route; @@ -40,9 +41,7 @@ public function testLoadCallsServiceAndReturnsCollection() $this->assertNotEmpty($actualRoutes->getResources()); } - /** - * @dataProvider getBadResourceStrings - */ + #[DataProvider('getBadResourceStrings')] public function testExceptionWithoutSyntax(string $resourceString) { $loader = new TestObjectLoader(); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index 16071e5b..a52b61b6 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -337,9 +338,7 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('php'), $routes); } - /** - * @dataProvider providePsr4ConfigFiles - */ + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); diff --git a/Tests/Loader/Psr4DirectoryLoaderTest.php b/Tests/Loader/Psr4DirectoryLoaderTest.php index 0720caca..9039ef9f 100644 --- a/Tests/Loader/Psr4DirectoryLoaderTest.php +++ b/Tests/Loader/Psr4DirectoryLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\DelegatingLoader; @@ -66,9 +67,7 @@ public function testAbstractController() $this->assertSame(MyChildController::class.'::someAction', $route->getDefault('_controller')); } - /** - * @dataProvider provideNamespacesThatNeedTrimming - */ + #[DataProvider('provideNamespacesThatNeedTrimming')] public function testPsr4NamespaceTrim(string $namespace) { $route = $this->getLoader() @@ -91,9 +90,7 @@ public static function provideNamespacesThatNeedTrimming(): array ]; } - /** - * @dataProvider provideInvalidPsr4Namespaces - */ + #[DataProvider('provideInvalidPsr4Namespaces')] public function testInvalidPsr4Namespace(string $namespace, string $expectedExceptionMessage) { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Loader/XmlFileLoaderTest.php b/Tests/Loader/XmlFileLoaderTest.php index 7afc3d2e..e8fe104d 100644 --- a/Tests/Loader/XmlFileLoaderTest.php +++ b/Tests/Loader/XmlFileLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -214,9 +215,7 @@ public function testLocalizedImportsOfNotLocalizedRoutes() $this->assertSame('en', $routeCollection->get('imported.en')->getRequirement('_locale')); } - /** - * @dataProvider getPathsToInvalidFiles - */ + #[DataProvider('getPathsToInvalidFiles')] public function testLoadThrowsExceptionWithInvalidFile($filePath) { $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); @@ -226,9 +225,7 @@ public function testLoadThrowsExceptionWithInvalidFile($filePath) $loader->load($filePath); } - /** - * @dataProvider getPathsToInvalidFiles - */ + #[DataProvider('getPathsToInvalidFiles')] public function testLoadThrowsExceptionWithInvalidFileEvenWithoutSchemaValidation(string $filePath) { $loader = new CustomXmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); @@ -472,9 +469,7 @@ public function testOverrideControllerInDefaults() $loader->load('override_defaults.xml'); } - /** - * @dataProvider provideFilesImportingRoutesWithControllers - */ + #[DataProvider('provideFilesImportingRoutesWithControllers')] public function testImportRouteWithController(string $file) { $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); @@ -617,9 +612,7 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('xml'), $routes); } - /** - * @dataProvider providePsr4ConfigFiles - */ + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); diff --git a/Tests/Loader/YamlFileLoaderTest.php b/Tests/Loader/YamlFileLoaderTest.php index 4f6ed3a2..beb15ae3 100644 --- a/Tests/Loader/YamlFileLoaderTest.php +++ b/Tests/Loader/YamlFileLoaderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Loader; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -46,9 +47,7 @@ public function testLoadDoesNothingIfEmpty() $this->assertEquals([new FileResource(realpath(__DIR__.'/../Fixtures/empty.yml'))], $collection->getResources()); } - /** - * @dataProvider getPathsToInvalidFiles - */ + #[DataProvider('getPathsToInvalidFiles')] public function testLoadThrowsExceptionWithInvalidFile(string $filePath) { $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); @@ -161,9 +160,7 @@ public function testOverrideControllerInDefaults() $loader->load('override_defaults.yml'); } - /** - * @dataProvider provideFilesImportingRoutesWithControllers - */ + #[DataProvider('provideFilesImportingRoutesWithControllers')] public function testImportRouteWithController($file) { $loader = new YamlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); @@ -522,9 +519,7 @@ protected function configureRoute( $this->assertSame(1, $routes->getPriority('also_important')); } - /** - * @dataProvider providePsr4ConfigFiles - */ + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); diff --git a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php index 1bb2c2e3..cca2c365 100644 --- a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php +++ b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Matcher\Dumper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -47,9 +48,7 @@ public function testRedirectPreservesUrlEncoding() $matcher->match('/foo%3Abar'); } - /** - * @dataProvider getRouteCollections - */ + #[DataProvider('getRouteCollections')] public function testDump(RouteCollection $collection, $fixture) { $basePath = __DIR__.'/../../Fixtures/dumper/'; diff --git a/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php b/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php index 9935ced4..f88fcdba 100644 --- a/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php +++ b/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests\Matcher\Dumper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Matcher\Dumper\StaticPrefixCollection; use Symfony\Component\Routing\Route; class StaticPrefixCollectionTest extends TestCase { - /** - * @dataProvider routeProvider - */ + #[DataProvider('routeProvider')] public function testGrouping(array $routes, $expected) { $collection = new StaticPrefixCollection('/'); diff --git a/Tests/Matcher/ExpressionLanguageProviderTest.php b/Tests/Matcher/ExpressionLanguageProviderTest.php index 71280257..2e5ab7ca 100644 --- a/Tests/Matcher/ExpressionLanguageProviderTest.php +++ b/Tests/Matcher/ExpressionLanguageProviderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Matcher; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -40,9 +41,7 @@ protected function setUp(): void $this->expressionLanguage->registerProvider(new ExpressionLanguageProvider($functionProvider)); } - /** - * @dataProvider compileProvider - */ + #[DataProvider('compileProvider')] public function testCompile(string $expression, string $expected) { $this->assertSame($expected, $this->expressionLanguage->compile($expression)); @@ -57,9 +56,7 @@ public static function compileProvider(): iterable ]; } - /** - * @dataProvider evaluateProvider - */ + #[DataProvider('evaluateProvider')] public function testEvaluate(string $expression, $expected) { $this->assertSame($expected, $this->expressionLanguage->evaluate($expression, ['context' => $this->context])); diff --git a/Tests/RequestContextTest.php b/Tests/RequestContextTest.php index fcc42ff5..d815fbed 100644 --- a/Tests/RequestContextTest.php +++ b/Tests/RequestContextTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RequestContext; @@ -85,18 +86,16 @@ public function testFromUriBeingEmpty() $this->assertSame('/', $requestContext->getPathInfo()); } - /** - * @testWith ["http://foo.com\\bar"] - * ["\\\\foo.com/bar"] - * ["a\rb"] - * ["a\nb"] - * ["a\tb"] - * ["\u0000foo"] - * ["foo\u0000"] - * [" foo"] - * ["foo "] - * [":"] - */ + #[TestWith(['http://foo.com\\bar'])] + #[TestWith(['\\\\foo.com/bar'])] + #[TestWith(["a\rb"])] + #[TestWith(["a\nb"])] + #[TestWith(["a\tb"])] + #[TestWith(["\u0000foo"])] + #[TestWith(["foo\u0000"])] + #[TestWith([' foo'])] + #[TestWith(['foo '])] + #[TestWith([':'])] public function testFromBadUri(string $uri) { $context = RequestContext::fromUri($uri); diff --git a/Tests/Requirement/EnumRequirementTest.php b/Tests/Requirement/EnumRequirementTest.php index 68b32ea7..842d4baa 100644 --- a/Tests/Requirement/EnumRequirementTest.php +++ b/Tests/Requirement/EnumRequirementTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests\Requirement; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Exception\InvalidArgumentException; use Symfony\Component\Routing\Requirement\EnumRequirement; @@ -46,9 +47,7 @@ public function testCaseFromAnotherEnum() new EnumRequirement([TestStringBackedEnum::Diamonds, TestStringBackedEnum2::Spades]); } - /** - * @dataProvider provideToString - */ + #[DataProvider('provideToString')] public function testToString(string $expected, string|array $cases = []) { $this->assertSame($expected, (string) new EnumRequirement($cases)); diff --git a/Tests/Requirement/RequirementTest.php b/Tests/Requirement/RequirementTest.php index d7e0ba07..24c58630 100644 --- a/Tests/Requirement/RequirementTest.php +++ b/Tests/Requirement/RequirementTest.php @@ -11,21 +11,21 @@ namespace Symfony\Component\Routing\Tests\Requirement; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Requirement\Requirement; use Symfony\Component\Routing\Route; class RequirementTest extends TestCase { - /** - * @testWith ["FOO"] - * ["foo"] - * ["1987"] - * ["42-42"] - * ["fo2o-bar"] - * ["foo-bA198r-Ccc"] - * ["fo10O-bar-CCc-fooba187rccc"] - */ + #[TestWith(['FOO'])] + #[TestWith(['foo'])] + #[TestWith(['1987'])] + #[TestWith(['42-42'])] + #[TestWith(['fo2o-bar'])] + #[TestWith(['foo-bA198r-Ccc'])] + #[TestWith(['fo10O-bar-CCc-fooba187rccc'])] public function testAsciiSlugOK(string $slug) { $this->assertMatchesRegularExpression( @@ -34,16 +34,14 @@ public function testAsciiSlugOK(string $slug) ); } - /** - * @testWith [""] - * ["-"] - * ["fôo"] - * ["-FOO"] - * ["foo-"] - * ["-foo-"] - * ["-foo-bar-"] - * ["foo--bar"] - */ + #[TestWith([''])] + #[TestWith(['-'])] + #[TestWith(['fôo'])] + #[TestWith(['-FOO'])] + #[TestWith(['foo-'])] + #[TestWith(['-foo-'])] + #[TestWith(['-foo-bar-'])] + #[TestWith(['foo--bar'])] public function testAsciiSlugKO(string $slug) { $this->assertDoesNotMatchRegularExpression( @@ -52,11 +50,9 @@ public function testAsciiSlugKO(string $slug) ); } - /** - * @testWith ["foo"] - * ["foo/bar/ccc"] - * ["///"] - */ + #[TestWith(['foo'])] + #[TestWith(['foo/bar/ccc'])] + #[TestWith(['///'])] public function testCatchAllOK(string $path) { $this->assertMatchesRegularExpression( @@ -65,9 +61,7 @@ public function testCatchAllOK(string $path) ); } - /** - * @testWith [""] - */ + #[TestWith([''])] public function testCatchAllKO(string $path) { $this->assertDoesNotMatchRegularExpression( @@ -76,13 +70,11 @@ public function testCatchAllKO(string $path) ); } - /** - * @testWith ["0000-01-01"] - * ["9999-12-31"] - * ["2022-04-15"] - * ["2024-02-29"] - * ["1243-04-31"] - */ + #[TestWith(['0000-01-01'])] + #[TestWith(['9999-12-31'])] + #[TestWith(['2022-04-15'])] + #[TestWith(['2024-02-29'])] + #[TestWith(['1243-04-31'])] public function testDateYmdOK(string $date) { $this->assertMatchesRegularExpression( @@ -91,14 +83,12 @@ public function testDateYmdOK(string $date) ); } - /** - * @testWith [""] - * ["foo"] - * ["0000-01-00"] - * ["9999-00-31"] - * ["2022-02-30"] - * ["2022-02-31"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['0000-01-00'])] + #[TestWith(['9999-00-31'])] + #[TestWith(['2022-02-30'])] + #[TestWith(['2022-02-31'])] public function testDateYmdKO(string $date) { $this->assertDoesNotMatchRegularExpression( @@ -107,14 +97,12 @@ public function testDateYmdKO(string $date) ); } - /** - * @testWith ["0"] - * ["012"] - * ["1"] - * ["42"] - * ["42198"] - * ["999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"] - */ + #[TestWith(['0'])] + #[TestWith(['012'])] + #[TestWith(['1'])] + #[TestWith(['42'])] + #[TestWith(['42198'])] + #[TestWith(['999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'])] public function testDigitsOK(string $digits) { $this->assertMatchesRegularExpression( @@ -123,12 +111,10 @@ public function testDigitsOK(string $digits) ); } - /** - * @testWith [""] - * ["foo"] - * ["-1"] - * ["3.14"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['-1'])] + #[TestWith(['3.14'])] public function testDigitsKO(string $digits) { $this->assertDoesNotMatchRegularExpression( @@ -137,10 +123,8 @@ public function testDigitsKO(string $digits) ); } - /** - * @testWith ["67c8b7d295c70befc3070bf2"] - * ["000000000000000000000000"] - */ + #[TestWith(['67c8b7d295c70befc3070bf2'])] + #[TestWith(['000000000000000000000000'])] public function testMongoDbIdOK(string $id) { $this->assertMatchesRegularExpression( @@ -149,12 +133,10 @@ public function testMongoDbIdOK(string $id) ); } - /** - * @testWith ["67C8b7D295C70BEFC3070BF2"] - * ["67c8b7d295c70befc3070bg2"] - * ["67c8b7d295c70befc3070bf2a"] - * ["67c8b7d295c70befc3070bf"] - */ + #[TestWith(['67C8b7D295C70BEFC3070BF2'])] + #[TestWith(['67c8b7d295c70befc3070bg2'])] + #[TestWith(['67c8b7d295c70befc3070bf2a'])] + #[TestWith(['67c8b7d295c70befc3070bf'])] public function testMongoDbIdKO(string $id) { $this->assertDoesNotMatchRegularExpression( @@ -163,12 +145,10 @@ public function testMongoDbIdKO(string $id) ); } - /** - * @testWith ["1"] - * ["42"] - * ["42198"] - * ["999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"] - */ + #[TestWith(['1'])] + #[TestWith(['42'])] + #[TestWith(['42198'])] + #[TestWith(['999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'])] public function testPositiveIntOK(string $digits) { $this->assertMatchesRegularExpression( @@ -177,14 +157,12 @@ public function testPositiveIntOK(string $digits) ); } - /** - * @testWith [""] - * ["0"] - * ["045"] - * ["foo"] - * ["-1"] - * ["3.14"] - */ + #[TestWith([''])] + #[TestWith(['0'])] + #[TestWith(['045'])] + #[TestWith(['foo'])] + #[TestWith(['-1'])] + #[TestWith(['3.14'])] public function testPositiveIntKO(string $digits) { $this->assertDoesNotMatchRegularExpression( @@ -193,12 +171,10 @@ public function testPositiveIntKO(string $digits) ); } - /** - * @testWith ["00000000000000000000000000"] - * ["ZZZZZZZZZZZZZZZZZZZZZZZZZZ"] - * ["01G0P4XH09KW3RCF7G4Q57ESN0"] - * ["05CSACM1MS9RB9H5F61BYA146Q"] - */ + #[TestWith(['00000000000000000000000000'])] + #[TestWith(['ZZZZZZZZZZZZZZZZZZZZZZZZZZ'])] + #[TestWith(['01G0P4XH09KW3RCF7G4Q57ESN0'])] + #[TestWith(['05CSACM1MS9RB9H5F61BYA146Q'])] public function testUidBase32OK(string $uid) { $this->assertMatchesRegularExpression( @@ -207,12 +183,10 @@ public function testUidBase32OK(string $uid) ); } - /** - * @testWith [""] - * ["foo"] - * ["01G0P4XH09KW3RCF7G4Q57ESN"] - * ["01G0P4XH09KW3RCF7G4Q57ESNU"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['01G0P4XH09KW3RCF7G4Q57ESN'])] + #[TestWith(['01G0P4XH09KW3RCF7G4Q57ESNU'])] public function testUidBase32KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -221,12 +195,10 @@ public function testUidBase32KO(string $uid) ); } - /** - * @testWith ["1111111111111111111111"] - * ["zzzzzzzzzzzzzzzzzzzzzz"] - * ["1BkPBX6T19U8TUAjBTtgwH"] - * ["1fg491dt8eQpf2TU42o2bY"] - */ + #[TestWith(['1111111111111111111111'])] + #[TestWith(['zzzzzzzzzzzzzzzzzzzzzz'])] + #[TestWith(['1BkPBX6T19U8TUAjBTtgwH'])] + #[TestWith(['1fg491dt8eQpf2TU42o2bY'])] public function testUidBase58OK(string $uid) { $this->assertMatchesRegularExpression( @@ -235,12 +207,10 @@ public function testUidBase58OK(string $uid) ); } - /** - * @testWith [""] - * ["foo"] - * ["1BkPBX6T19U8TUAjBTtgw"] - * ["1BkPBX6T19U8TUAjBTtgwI"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['1BkPBX6T19U8TUAjBTtgw'])] + #[TestWith(['1BkPBX6T19U8TUAjBTtgwI'])] public function testUidBase58KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -249,9 +219,7 @@ public function testUidBase58KO(string $uid) ); } - /** - * @dataProvider provideUidRfc4122 - */ + #[DataProvider('provideUidRfc4122')] public function testUidRfc4122OK(string $uid) { $this->assertMatchesRegularExpression( @@ -260,9 +228,7 @@ public function testUidRfc4122OK(string $uid) ); } - /** - * @dataProvider provideUidRfc4122KO - */ + #[DataProvider('provideUidRfc4122KO')] public function testUidRfc4122KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -271,9 +237,7 @@ public function testUidRfc4122KO(string $uid) ); } - /** - * @dataProvider provideUidRfc4122 - */ + #[DataProvider('provideUidRfc4122')] public function testUidRfc9562OK(string $uid) { $this->assertMatchesRegularExpression( @@ -282,9 +246,7 @@ public function testUidRfc9562OK(string $uid) ); } - /** - * @dataProvider provideUidRfc4122KO - */ + #[DataProvider('provideUidRfc4122KO')] public function testUidRfc9562KO(string $uid) { $this->assertDoesNotMatchRegularExpression( @@ -310,11 +272,9 @@ public static function provideUidRfc4122KO(): iterable yield ['01802c4ec4099f07863cf025ca7766a0']; } - /** - * @testWith ["00000000000000000000000000"] - * ["7ZZZZZZZZZZZZZZZZZZZZZZZZZ"] - * ["01G0P4ZPM69QTD4MM4ENAEA4EW"] - */ + #[TestWith(['00000000000000000000000000'])] + #[TestWith(['7ZZZZZZZZZZZZZZZZZZZZZZZZZ'])] + #[TestWith(['01G0P4ZPM69QTD4MM4ENAEA4EW'])] public function testUlidOK(string $ulid) { $this->assertMatchesRegularExpression( @@ -323,12 +283,10 @@ public function testUlidOK(string $ulid) ); } - /** - * @testWith [""] - * ["foo"] - * ["8ZZZZZZZZZZZZZZZZZZZZZZZZZ"] - * ["01G0P4ZPM69QTD4MM4ENAEA4E"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['8ZZZZZZZZZZZZZZZZZZZZZZZZZ'])] + #[TestWith(['01G0P4ZPM69QTD4MM4ENAEA4E'])] public function testUlidKO(string $ulid) { $this->assertDoesNotMatchRegularExpression( @@ -337,15 +295,13 @@ public function testUlidKO(string $ulid) ); } - /** - * @testWith ["00000000-0000-1000-8000-000000000000"] - * ["ffffffff-ffff-6fff-bfff-ffffffffffff"] - * ["8c670a1c-bc95-11ec-8422-0242ac120002"] - * ["61c86569-e477-3ed9-9e3b-1562edb03277"] - * ["e55a29be-ba25-46e0-a5e5-85b78a6f9a11"] - * ["bad98960-f1a1-530e-9a82-07d0b6c4e62f"] - * ["1ecbc9a8-432d-6b14-af93-715adc3b830c"] - */ + #[TestWith(['00000000-0000-1000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-6fff-bfff-ffffffffffff'])] + #[TestWith(['8c670a1c-bc95-11ec-8422-0242ac120002'])] + #[TestWith(['61c86569-e477-3ed9-9e3b-1562edb03277'])] + #[TestWith(['e55a29be-ba25-46e0-a5e5-85b78a6f9a11'])] + #[TestWith(['bad98960-f1a1-530e-9a82-07d0b6c4e62f'])] + #[TestWith(['1ecbc9a8-432d-6b14-af93-715adc3b830c'])] public function testUuidOK(string $uuid) { $this->assertMatchesRegularExpression( @@ -354,15 +310,13 @@ public function testUuidOK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["01802c74-d78c-b085-0cdf-7cbad87c70a3"] - * ["e55a29be-ba25-46e0-a5e5-85b78a6f9a1"] - * ["e55a29bh-ba25-46e0-a5e5-85b78a6f9a11"] - * ["e55a29beba2546e0a5e585b78a6f9a11"] - * ["21902510-bc96-21ec-8422-0242ac120002"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['01802c74-d78c-b085-0cdf-7cbad87c70a3'])] + #[TestWith(['e55a29be-ba25-46e0-a5e5-85b78a6f9a1'])] + #[TestWith(['e55a29bh-ba25-46e0-a5e5-85b78a6f9a11'])] + #[TestWith(['e55a29beba2546e0a5e585b78a6f9a11'])] + #[TestWith(['21902510-bc96-21ec-8422-0242ac120002'])] public function testUuidKO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -371,13 +325,11 @@ public function testUuidKO(string $uuid) ); } - /** - * @testWith ["00000000-0000-1000-8000-000000000000"] - * ["ffffffff-ffff-1fff-bfff-ffffffffffff"] - * ["21902510-bc96-11ec-8422-0242ac120002"] - * ["a8ff8f60-088e-1099-a09d-53afc49918d1"] - * ["b0ac612c-9117-17a1-901f-53afc49918d1"] - */ + #[TestWith(['00000000-0000-1000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-1fff-bfff-ffffffffffff'])] + #[TestWith(['21902510-bc96-11ec-8422-0242ac120002'])] + #[TestWith(['a8ff8f60-088e-1099-a09d-53afc49918d1'])] + #[TestWith(['b0ac612c-9117-17a1-901f-53afc49918d1'])] public function testUuidV1OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -386,14 +338,12 @@ public function testUuidV1OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["a3674b89-0170-3e30-8689-52939013e39c"] - * ["e0040090-3cb0-4bf9-a868-407770c964f9"] - * ["2e2b41d9-e08c-53d2-b435-818b9c323942"] - * ["2a37b67a-5eaa-6424-b5d6-ffc9ba0f2a13"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['a3674b89-0170-3e30-8689-52939013e39c'])] + #[TestWith(['e0040090-3cb0-4bf9-a868-407770c964f9'])] + #[TestWith(['2e2b41d9-e08c-53d2-b435-818b9c323942'])] + #[TestWith(['2a37b67a-5eaa-6424-b5d6-ffc9ba0f2a13'])] public function testUuidV1KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -402,12 +352,10 @@ public function testUuidV1KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-3000-8000-000000000000"] - * ["ffffffff-ffff-3fff-bfff-ffffffffffff"] - * ["2b3f1427-33b2-30a9-8759-07355007c204"] - * ["c38e7b09-07f7-3901-843d-970b0186b873"] - */ + #[TestWith(['00000000-0000-3000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-3fff-bfff-ffffffffffff'])] + #[TestWith(['2b3f1427-33b2-30a9-8759-07355007c204'])] + #[TestWith(['c38e7b09-07f7-3901-843d-970b0186b873'])] public function testUuidV3OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -416,14 +364,12 @@ public function testUuidV3OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["e24d9c0e-bc98-11ec-9924-53afc49918d1"] - * ["1c240248-7d0b-41a4-9d20-61ad2915a58c"] - * ["4816b668-385b-5a65-808d-bca410f45090"] - * ["1d2f3104-dff6-64c6-92ff-0f74b1d0e2af"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['e24d9c0e-bc98-11ec-9924-53afc49918d1'])] + #[TestWith(['1c240248-7d0b-41a4-9d20-61ad2915a58c'])] + #[TestWith(['4816b668-385b-5a65-808d-bca410f45090'])] + #[TestWith(['1d2f3104-dff6-64c6-92ff-0f74b1d0e2af'])] public function testUuidV3KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -432,12 +378,10 @@ public function testUuidV3KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-4000-8000-000000000000"] - * ["ffffffff-ffff-4fff-bfff-ffffffffffff"] - * ["b8f15bf4-46e2-4757-bbce-11ae83f7a6ea"] - * ["eaf51230-1ce2-40f1-ab18-649212b26198"] - */ + #[TestWith(['00000000-0000-4000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-4fff-bfff-ffffffffffff'])] + #[TestWith(['b8f15bf4-46e2-4757-bbce-11ae83f7a6ea'])] + #[TestWith(['eaf51230-1ce2-40f1-ab18-649212b26198'])] public function testUuidV4OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -446,14 +390,12 @@ public function testUuidV4OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["15baaab2-f310-11d2-9ecf-53afc49918d1"] - * ["acd44dc8-d2cc-326c-9e3a-80a3305a25e8"] - * ["7fc2705f-a8a4-5b31-99a8-890686d64189"] - * ["1ecbc991-3552-6920-998e-efad54178a98"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['15baaab2-f310-11d2-9ecf-53afc49918d1'])] + #[TestWith(['acd44dc8-d2cc-326c-9e3a-80a3305a25e8'])] + #[TestWith(['7fc2705f-a8a4-5b31-99a8-890686d64189'])] + #[TestWith(['1ecbc991-3552-6920-998e-efad54178a98'])] public function testUuidV4KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -462,12 +404,10 @@ public function testUuidV4KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-5000-8000-000000000000"] - * ["ffffffff-ffff-5fff-bfff-ffffffffffff"] - * ["49f4d32c-28b3-5802-8717-a2896180efbd"] - * ["58b3c62e-a7df-5a82-93a6-fbe5fda681c1"] - */ + #[TestWith(['00000000-0000-5000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-5fff-bfff-ffffffffffff'])] + #[TestWith(['49f4d32c-28b3-5802-8717-a2896180efbd'])] + #[TestWith(['58b3c62e-a7df-5a82-93a6-fbe5fda681c1'])] public function testUuidV5OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -476,14 +416,12 @@ public function testUuidV5OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["b99ad578-fdd3-1135-9d3b-53afc49918d1"] - * ["b3ee3071-7a2b-3e17-afdf-6b6aec3acf85"] - * ["2ab4f5a7-6412-46c1-b3ab-1fe1ed391e27"] - * ["135fdd3d-e193-653e-865d-67e88cf12e44"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['b99ad578-fdd3-1135-9d3b-53afc49918d1'])] + #[TestWith(['b3ee3071-7a2b-3e17-afdf-6b6aec3acf85'])] + #[TestWith(['2ab4f5a7-6412-46c1-b3ab-1fe1ed391e27'])] + #[TestWith(['135fdd3d-e193-653e-865d-67e88cf12e44'])] public function testUuidV5KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -492,13 +430,11 @@ public function testUuidV5KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-6000-8000-000000000000"] - * ["ffffffff-ffff-6fff-bfff-ffffffffffff"] - * ["2c51caad-c72f-66b2-b6d7-8766d36c73df"] - * ["17941ebb-48fa-6bfe-9bbd-43929f8784f5"] - * ["1ecbc993-f6c2-67f2-8fbe-295ed594b344"] - */ + #[TestWith(['00000000-0000-6000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-6fff-bfff-ffffffffffff'])] + #[TestWith(['2c51caad-c72f-66b2-b6d7-8766d36c73df'])] + #[TestWith(['17941ebb-48fa-6bfe-9bbd-43929f8784f5'])] + #[TestWith(['1ecbc993-f6c2-67f2-8fbe-295ed594b344'])] public function testUuidV6OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -507,14 +443,12 @@ public function testUuidV6OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["821040f4-7b67-12a3-9770-53afc49918d1"] - * ["802dc245-aaaa-3649-98c6-31c549b0df86"] - * ["92d2e5ad-bc4e-4947-a8d9-77706172ca83"] - * ["6e124559-d260-511e-afdc-e57c7025fed0"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['821040f4-7b67-12a3-9770-53afc49918d1'])] + #[TestWith(['802dc245-aaaa-3649-98c6-31c549b0df86'])] + #[TestWith(['92d2e5ad-bc4e-4947-a8d9-77706172ca83'])] + #[TestWith(['6e124559-d260-511e-afdc-e57c7025fed0'])] public function testUuidV6KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -523,11 +457,9 @@ public function testUuidV6KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-7000-8000-000000000000"] - * ["ffffffff-ffff-7fff-bfff-ffffffffffff"] - * ["01910577-4898-7c47-966e-68d127dde2ac"] - */ + #[TestWith(['00000000-0000-7000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-7fff-bfff-ffffffffffff'])] + #[TestWith(['01910577-4898-7c47-966e-68d127dde2ac'])] public function testUuidV7OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -536,14 +468,12 @@ public function testUuidV7OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["15baaab2-f310-11d2-9ecf-53afc49918d1"] - * ["acd44dc8-d2cc-326c-9e3a-80a3305a25e8"] - * ["7fc2705f-a8a4-5b31-99a8-890686d64189"] - * ["1ecbc991-3552-6920-998e-efad54178a98"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['15baaab2-f310-11d2-9ecf-53afc49918d1'])] + #[TestWith(['acd44dc8-d2cc-326c-9e3a-80a3305a25e8'])] + #[TestWith(['7fc2705f-a8a4-5b31-99a8-890686d64189'])] + #[TestWith(['1ecbc991-3552-6920-998e-efad54178a98'])] public function testUuidV7KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( @@ -552,11 +482,9 @@ public function testUuidV7KO(string $uuid) ); } - /** - * @testWith ["00000000-0000-8000-8000-000000000000"] - * ["ffffffff-ffff-8fff-bfff-ffffffffffff"] - * ["01910577-4898-8c47-966e-68d127dde2ac"] - */ + #[TestWith(['00000000-0000-8000-8000-000000000000'])] + #[TestWith(['ffffffff-ffff-8fff-bfff-ffffffffffff'])] + #[TestWith(['01910577-4898-8c47-966e-68d127dde2ac'])] public function testUuidV8OK(string $uuid) { $this->assertMatchesRegularExpression( @@ -565,14 +493,12 @@ public function testUuidV8OK(string $uuid) ); } - /** - * @testWith [""] - * ["foo"] - * ["15baaab2-f310-11d2-9ecf-53afc49918d1"] - * ["acd44dc8-d2cc-326c-9e3a-80a3305a25e8"] - * ["7fc2705f-a8a4-5b31-99a8-890686d64189"] - * ["1ecbc991-3552-6920-998e-efad54178a98"] - */ + #[TestWith([''])] + #[TestWith(['foo'])] + #[TestWith(['15baaab2-f310-11d2-9ecf-53afc49918d1'])] + #[TestWith(['acd44dc8-d2cc-326c-9e3a-80a3305a25e8'])] + #[TestWith(['7fc2705f-a8a4-5b31-99a8-890686d64189'])] + #[TestWith(['1ecbc991-3552-6920-998e-efad54178a98'])] public function testUuidV8KO(string $uuid) { $this->assertDoesNotMatchRegularExpression( diff --git a/Tests/RouteCompilerTest.php b/Tests/RouteCompilerTest.php index 0a756593..af8941d6 100644 --- a/Tests/RouteCompilerTest.php +++ b/Tests/RouteCompilerTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Routing\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCompiler; class RouteCompilerTest extends TestCase { - /** - * @dataProvider provideCompileData - */ + #[DataProvider('provideCompileData')] public function testCompile($name, $arguments, $prefix, $regex, $variables, $tokens) { $r = new \ReflectionClass(Route::class); @@ -183,9 +182,7 @@ public static function provideCompileData() ]; } - /** - * @dataProvider provideCompileImplicitUtf8Data - */ + #[DataProvider('provideCompileImplicitUtf8Data')] public function testCompileImplicitUtf8Data($name, $arguments, $prefix, $regex, $variables, $tokens) { $this->expectException(\LogicException::class); @@ -277,9 +274,7 @@ public function testRouteWithFragmentAsPathParameter() $route->compile(); } - /** - * @dataProvider getVariableNamesStartingWithADigit - */ + #[DataProvider('getVariableNamesStartingWithADigit')] public function testRouteWithVariableNameStartingWithADigit(string $name) { $this->expectException(\DomainException::class); @@ -296,9 +291,7 @@ public static function getVariableNamesStartingWithADigit() ]; } - /** - * @dataProvider provideCompileWithHostData - */ + #[DataProvider('provideCompileWithHostData')] public function testCompileWithHost(string $name, array $arguments, string $prefix, string $regex, array $variables, array $pathVariables, array $tokens, string $hostRegex, array $hostVariables, array $hostTokens) { $r = new \ReflectionClass(Route::class); @@ -376,9 +369,7 @@ public function testRouteWithTooLongVariableName() $route->compile(); } - /** - * @dataProvider provideRemoveCapturingGroup - */ + #[DataProvider('provideRemoveCapturingGroup')] public function testRemoveCapturingGroup(string $regex, string $requirement) { $route = new Route('/{foo}', [], ['foo' => $requirement]); diff --git a/Tests/RouteTest.php b/Tests/RouteTest.php index 34728042..cc67c18c 100644 --- a/Tests/RouteTest.php +++ b/Tests/RouteTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Routing\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\CompiledRoute; use Symfony\Component\Routing\Route; @@ -141,9 +142,7 @@ public function testRequirementAlternativeStartAndEndRegexSyntax() $this->assertTrue($route->hasRequirement('foo')); } - /** - * @dataProvider getInvalidRequirements - */ + #[DataProvider('getInvalidRequirements')] public function testSetInvalidRequirement($req) { $route = new Route('/{foo}'); @@ -226,9 +225,7 @@ public function testSerialize() $this->assertNotSame($route, $unserialized); } - /** - * @dataProvider provideInlineDefaultAndRequirementCases - */ + #[DataProvider('provideInlineDefaultAndRequirementCases')] public function testInlineDefaultAndRequirement(Route $route, string $expectedPath, string $expectedHost, array $expectedDefaults, array $expectedRequirements) { self::assertSame($expectedPath, $route->getPath()); @@ -323,9 +320,7 @@ public function testSerializedRepresentationKeepsWorking() $this->assertNotSame($route, $unserialized); } - /** - * @dataProvider provideNonLocalizedRoutes - */ + #[DataProvider('provideNonLocalizedRoutes')] public function testLocaleDefaultWithNonLocalizedRoutes(Route $route) { $this->assertNotSame('fr', $route->getDefault('_locale')); @@ -333,9 +328,7 @@ public function testLocaleDefaultWithNonLocalizedRoutes(Route $route) $this->assertSame('fr', $route->getDefault('_locale')); } - /** - * @dataProvider provideLocalizedRoutes - */ + #[DataProvider('provideLocalizedRoutes')] public function testLocaleDefaultWithLocalizedRoutes(Route $route) { $expected = $route->getDefault('_locale'); @@ -345,9 +338,7 @@ public function testLocaleDefaultWithLocalizedRoutes(Route $route) $this->assertSame($expected, $route->getDefault('_locale')); } - /** - * @dataProvider provideNonLocalizedRoutes - */ + #[DataProvider('provideNonLocalizedRoutes')] public function testLocaleRequirementWithNonLocalizedRoutes(Route $route) { $this->assertNotSame('fr', $route->getRequirement('_locale')); @@ -355,9 +346,7 @@ public function testLocaleRequirementWithNonLocalizedRoutes(Route $route) $this->assertSame('fr', $route->getRequirement('_locale')); } - /** - * @dataProvider provideLocalizedRoutes - */ + #[DataProvider('provideLocalizedRoutes')] public function testLocaleRequirementWithLocalizedRoutes(Route $route) { $expected = $route->getRequirement('_locale'); From ae703383cbe591c9f9053a27dd03804ef4ba31af Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 10 Aug 2025 00:28:14 +0200 Subject: [PATCH 13/44] chore: heredoc indentation as of PHP 7.3 https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc --- .../Dumper/CompiledUrlGeneratorDumper.php | 10 +- Matcher/Dumper/CompiledUrlMatcherDumper.php | 28 ++--- .../Dumper/StaticPrefixCollectionTest.php | 110 +++++++++--------- 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/Generator/Dumper/CompiledUrlGeneratorDumper.php b/Generator/Dumper/CompiledUrlGeneratorDumper.php index 555c5bfb..881a6d50 100644 --- a/Generator/Dumper/CompiledUrlGeneratorDumper.php +++ b/Generator/Dumper/CompiledUrlGeneratorDumper.php @@ -91,14 +91,14 @@ public function getCompiledAliases(): array public function dump(array $options = []): string { return <<generateDeclaredRoutes()} -]; + return [{$this->generateDeclaredRoutes()} + ]; -EOF; + EOF; } /** diff --git a/Matcher/Dumper/CompiledUrlMatcherDumper.php b/Matcher/Dumper/CompiledUrlMatcherDumper.php index b719e755..4d0b9b6f 100644 --- a/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -37,17 +37,17 @@ class CompiledUrlMatcherDumper extends MatcherDumper public function dump(array $options = []): string { return <<generateCompiledRoutes()}]; + return [ + {$this->generateCompiledRoutes()}]; -EOF; + EOF; } public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider): void @@ -112,12 +112,12 @@ public function getCompiledRoutes(bool $forDump = false): array } $checkConditionCode = <<indent(implode("\n", $conditions), 3)} - } - } -EOF; + static function (\$condition, \$context, \$request, \$params) { // \$checkCondition + switch (\$condition) { + {$this->indent(implode("\n", $conditions), 3)} + } + } + EOF; $compiledRoutes[4] = $forDump ? $checkConditionCode.",\n" : eval('return '.$checkConditionCode.';'); } else { $compiledRoutes[4] = $forDump ? " null, // \$checkCondition\n" : null; diff --git a/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php b/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php index f88fcdba..939aee5b 100644 --- a/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php +++ b/Tests/Matcher/Dumper/StaticPrefixCollectionTest.php @@ -43,10 +43,10 @@ public static function routeProvider() ['/leading/segment/', 'leading_segment'], ], << [ [ @@ -55,11 +55,11 @@ public static function routeProvider() ['/prefix/segment/bb', 'leading_segment'], ], << prefix_segment --> leading_segment -EOF, + root + /prefix/segment/ + -> prefix_segment + -> leading_segment + EOF, ], 'Nested - contains item at intersection' => [ [ @@ -68,11 +68,11 @@ public static function routeProvider() ['/prefix/segment/bb', 'leading_segment'], ], << prefix_segment --> leading_segment -EOF, + root + /prefix/segment/ + -> prefix_segment + -> leading_segment + EOF, ], 'Simple one level nesting' => [ [ @@ -82,12 +82,12 @@ public static function routeProvider() ['/group/other/', 'other_segment'], ], << nested_segment --> some_segment --> other_segment -EOF, + root + /group/ + -> nested_segment + -> some_segment + -> other_segment + EOF, ], 'Retain matching order with groups' => [ [ @@ -100,16 +100,16 @@ public static function routeProvider() ['/group/ff/', 'ff'], ], << aa --> bb --> cc -root -/group/ --> dd --> ee --> ff -EOF, + /group/ + -> aa + -> bb + -> cc + root + /group/ + -> dd + -> ee + -> ff + EOF, ], 'Retain complex matching order with groups at base' => [ [ @@ -126,22 +126,22 @@ public static function routeProvider() ['/aaa/333/', 'third_aaa'], ], << first_aaa --> second_aaa --> third_aaa -/prefixed/ --> /prefixed/group/ --> -> aa --> -> bb --> -> cc --> root --> /prefixed/group/ --> -> dd --> -> ee --> -> ff --> parent -EOF, + /aaa/ + -> first_aaa + -> second_aaa + -> third_aaa + /prefixed/ + -> /prefixed/group/ + -> -> aa + -> -> bb + -> -> cc + -> root + -> /prefixed/group/ + -> -> dd + -> -> ee + -> -> ff + -> parent + EOF, ], 'Group regardless of segments' => [ @@ -154,15 +154,15 @@ public static function routeProvider() ['/group-cc/', 'g3'], ], << a1 --> a2 --> a3 -/group- --> g1 --> g2 --> g3 -EOF, + /aaa- + -> a1 + -> a2 + -> a3 + /group- + -> g1 + -> g2 + -> g3 + EOF, ], ]; } From 9adbd24c853c9ada6dd8f6ad2b7c7adacae057aa Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 1 Aug 2025 14:58:41 +0200 Subject: [PATCH 14/44] run tests with PHPUnit 12.3 --- Tests/RouteCompilerTest.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Tests/RouteCompilerTest.php b/Tests/RouteCompilerTest.php index af8941d6..51bc47ed 100644 --- a/Tests/RouteCompilerTest.php +++ b/Tests/RouteCompilerTest.php @@ -202,39 +202,47 @@ public static function provideCompileImplicitUtf8Data() [ 'Static UTF-8 route', ['/foé'], - '/foé', '{^/foé$}sDu', [], [ + '/foé', + '{^/foé$}sDu', + [], + [ ['text', '/foé'], ], - 'patterns', ], [ 'Route with an implicit UTF-8 requirement', ['/{bar}', ['bar' => null], ['bar' => 'é']], - '', '{^/(?Pé)?$}sDu', ['bar'], [ + '', + '{^/(?Pé)?$}sDu', + ['bar'], + [ ['variable', '/', 'é', 'bar', true], ], - 'requirements', ], [ 'Route with a UTF-8 class requirement', ['/{bar}', ['bar' => null], ['bar' => '\pM']], - '', '{^/(?P\pM)?$}sDu', ['bar'], [ + '', + '{^/(?P\pM)?$}sDu', + ['bar'], + [ ['variable', '/', '\pM', 'bar', true], ], - 'requirements', ], [ 'Route with a UTF-8 separator', ['/foo/{bar}§{_format}', [], [], ['compiler_class' => Utf8RouteCompiler::class]], - '/foo', '{^/foo/(?P[^/§]++)§(?P<_format>[^/]++)$}sDu', ['bar', '_format'], [ + '/foo', + '{^/foo/(?P[^/§]++)§(?P<_format>[^/]++)$}sDu', + ['bar', '_format'], + [ ['variable', '§', '[^/]++', '_format', true], ['variable', '/', '[^/§]++', 'bar', true], ['text', '/foo'], ], - 'patterns', ], ]; } From 1b8f64bfeb41c7244ce12b92784695fe6d4f8ee8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 14 Aug 2025 09:36:33 +0200 Subject: [PATCH 15/44] Replace __sleep/wakeup() by __(un)serialize() when BC isn't a concern --- Loader/Configurator/CollectionConfigurator.php | 4 ++-- Loader/Configurator/ImportConfigurator.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Loader/Configurator/CollectionConfigurator.php b/Loader/Configurator/CollectionConfigurator.php index 4b83b0ff..d1553213 100644 --- a/Loader/Configurator/CollectionConfigurator.php +++ b/Loader/Configurator/CollectionConfigurator.php @@ -36,12 +36,12 @@ public function __construct( $this->route = new Route(''); } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup(): void + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Loader/Configurator/ImportConfigurator.php b/Loader/Configurator/ImportConfigurator.php index 45d1f6dc..b10bdf05 100644 --- a/Loader/Configurator/ImportConfigurator.php +++ b/Loader/Configurator/ImportConfigurator.php @@ -29,12 +29,12 @@ public function __construct( $this->route = $route; } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup(): void + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } From 0565daeaf865898a9f8fccbe00e4100f4174a4db Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Thu, 7 Aug 2025 18:59:46 -0300 Subject: [PATCH 16/44] [Routing] allow setting multiple envs in `#[Route]` attribute --- Attribute/Route.php | 36 +++++++++++++++++-- CHANGELOG.md | 1 + Loader/AttributeClassLoader.php | 6 ++-- .../AttributeFixtures/RouteWithEnv.php | 15 ++++++++ Tests/Loader/AttributeClassLoaderTest.php | 4 ++- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Attribute/Route.php b/Attribute/Route.php index 003bbe64..1ad37e17 100644 --- a/Attribute/Route.php +++ b/Attribute/Route.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Routing\Attribute; +use Symfony\Component\Routing\Exception\LogicException; + /** * @author Fabien Potencier * @author Alexander M. Turek @@ -21,6 +23,8 @@ class Route private ?string $path = null; private array $localizedPaths = []; private array $methods; + /** @var string[] */ + private array $env; private array $schemes; /** * @var (string|DeprecatedAlias)[] @@ -42,7 +46,7 @@ class Route * @param string|null $format The format returned by the route (i.e. "json", "xml") * @param bool|null $utf8 Whether the route accepts UTF-8 in its parameters * @param bool|null $stateless Whether the route is defined as stateless or stateful, @see https://symfony.com/doc/current/routing.html#stateless-routes - * @param string|null $env The env in which the route is defined (i.e. "dev", "test", "prod") + * @param string|string[]|null $env The env(s) in which the route is defined (i.e. "dev", "test", "prod", ["dev", "test"]) * @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $alias The list of aliases for this route */ public function __construct( @@ -60,7 +64,7 @@ public function __construct( ?string $format = null, ?bool $utf8 = null, ?bool $stateless = null, - private ?string $env = null, + string|array|null $env = null, string|DeprecatedAlias|array $alias = [], ) { if (\is_array($path)) { @@ -71,6 +75,7 @@ public function __construct( $this->setMethods($methods); $this->setSchemes($schemes); $this->setAliases($alias); + $this->setEnvs((array) $env); if (null !== $locale) { $this->defaults['_locale'] = $locale; @@ -199,12 +204,37 @@ public function getPriority(): ?int return $this->priority; } + /** + * @deprecated since Symfony 7.4, use the {@see setEnvs()} method instead + */ public function setEnv(?string $env): void { - $this->env = $env; + trigger_deprecation('symfony/routing', '7.4', 'The "%s()" method is deprecated, use "setEnvs()" instead.', __METHOD__); + $this->env = (array) $env; } + /** + * @deprecated since Symfony 7.4, use {@see getEnvs()} method instead + */ public function getEnv(): ?string + { + trigger_deprecation('symfony/routing', '7.4', 'The "%s()" method is deprecated, use "getEnvs()" instead.', __METHOD__); + if (!$this->env) { + return null; + } + if (\count($this->env) > 1) { + throw new LogicException(\sprintf('The "env" property has %d environments. Use "getEnvs()" to get all of them.', \count($this->env))); + } + + return $this->env[0]; + } + + public function setEnvs(array|string $env): void + { + $this->env = (array) $env; + } + + public function getEnvs(): array { return $this->env; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef96d53..3496520b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Allow query-specific parameters in `UrlGenerator` using `_query` + * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute 7.3 --- diff --git a/Loader/AttributeClassLoader.php b/Loader/AttributeClassLoader.php index 254582bf..5fdd7906 100644 --- a/Loader/AttributeClassLoader.php +++ b/Loader/AttributeClassLoader.php @@ -105,7 +105,7 @@ public function load(mixed $class, ?string $type = null): RouteCollection $globals = $this->getGlobals($class); $collection = new RouteCollection(); $collection->addResource(new FileResource($class->getFileName())); - if ($globals['env'] && $this->env !== $globals['env']) { + if ($globals['env'] && !\in_array($this->env, $globals['env'], true)) { return $collection; } $fqcnAlias = false; @@ -161,7 +161,7 @@ public function load(mixed $class, ?string $type = null): RouteCollection */ protected function addRoute(RouteCollection $collection, object $attr, array $globals, \ReflectionClass $class, \ReflectionMethod $method): void { - if ($attr->getEnv() && $attr->getEnv() !== $this->env) { + if ($attr->getEnvs() && !\in_array($this->env, $attr->getEnvs(), true)) { return; } @@ -338,7 +338,7 @@ protected function getGlobals(\ReflectionClass $class): array } $globals['priority'] = $attr->getPriority() ?? 0; - $globals['env'] = $attr->getEnv(); + $globals['env'] = $attr->getEnvs(); foreach ($globals['requirements'] as $placeholder => $requirement) { if (\is_int($placeholder)) { diff --git a/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php b/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php index 31f6c39b..c8f82fdf 100644 --- a/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php +++ b/Tests/Fixtures/AttributeFixtures/RouteWithEnv.php @@ -16,4 +16,19 @@ public function action() public function action2() { } + + #[Route(path: '/path3', name: 'action3', env: ['some-other-env', 'some-other-env-two'])] + public function action3() + { + } + + #[Route(path: '/path4', name: 'action4', env: null)] + public function action4() + { + } + + #[Route(path: '/path5', name: 'action5', env: ['some-other-env', 'some-env'])] + public function action5() + { + } } diff --git a/Tests/Loader/AttributeClassLoaderTest.php b/Tests/Loader/AttributeClassLoaderTest.php index 2fa00e07..8ad0ede6 100644 --- a/Tests/Loader/AttributeClassLoaderTest.php +++ b/Tests/Loader/AttributeClassLoaderTest.php @@ -330,8 +330,10 @@ public function testWhenEnv() $this->setUp('some-env'); $routes = $this->loader->load(RouteWithEnv::class); - $this->assertCount(1, $routes); + $this->assertCount(3, $routes); $this->assertSame('/path', $routes->get('action')->getPath()); + $this->assertSame('/path4', $routes->get('action4')->getPath()); + $this->assertSame('/path5', $routes->get('action5')->getPath()); } public function testMethodsAndSchemes() From 782a740b84747e7f6acc04a4e69949325372b137 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Wed, 2 Apr 2025 14:07:22 +0200 Subject: [PATCH 17/44] [Routing] Add possibility to create a request context with parameters directly --- RequestContext.php | 3 ++- Tests/RequestContextTest.php | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/RequestContext.php b/RequestContext.php index 5e9e79d9..8c560370 100644 --- a/RequestContext.php +++ b/RequestContext.php @@ -33,7 +33,7 @@ class RequestContext private string $queryString; private array $parameters = []; - public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '') + public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '', ?array $parameters = null) { $this->setBaseUrl($baseUrl); $this->setMethod($method); @@ -43,6 +43,7 @@ public function __construct(string $baseUrl = '', string $method = 'GET', string $this->setHttpsPort($httpsPort); $this->setPathInfo($path); $this->setQueryString($queryString); + $this->parameters = $parameters ?? $this->parameters; } public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self diff --git a/Tests/RequestContextTest.php b/Tests/RequestContextTest.php index fcc42ff5..2d206be6 100644 --- a/Tests/RequestContextTest.php +++ b/Tests/RequestContextTest.php @@ -27,7 +27,10 @@ public function testConstruct() 8080, 444, '/baz', - 'bar=foobar' + 'bar=foobar', + [ + 'foo' => 'bar', + ] ); $this->assertEquals('foo', $requestContext->getBaseUrl()); @@ -38,6 +41,20 @@ public function testConstruct() $this->assertSame(444, $requestContext->getHttpsPort()); $this->assertEquals('/baz', $requestContext->getPathInfo()); $this->assertEquals('bar=foobar', $requestContext->getQueryString()); + $this->assertSame(['foo' => 'bar'], $requestContext->getParameters()); + } + + public function testConstructParametersBcLayer() + { + $requestContext = new class() extends RequestContext { + public function __construct() + { + $this->setParameters(['foo' => 'bar']); + parent::__construct(); + } + }; + + $this->assertSame(['foo' => 'bar'], $requestContext->getParameters()); } public function testFromUriWithBaseUrl() From c50e4233f4b7d9659873685fb1eb425ad69d3af3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 20 Aug 2025 09:21:32 +0200 Subject: [PATCH 18/44] Minor tweaks --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3496520b..d97b5738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Allow query-specific parameters in `UrlGenerator` using `_query` * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute + * Add argument `$parameter` to `RequestContext`'s constructor 7.3 --- From 5150c645f0ceb1f55dc44256bfb30824b4c8d783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anderson=20M=C3=BCller?= Date: Wed, 20 Aug 2025 09:49:19 +0200 Subject: [PATCH 19/44] [ROUTING] Fix Typo in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d97b5738..86af6723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ CHANGELOG * Allow query-specific parameters in `UrlGenerator` using `_query` * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute - * Add argument `$parameter` to `RequestContext`'s constructor + * Add argument `$parameters` to `RequestContext`'s constructor 7.3 --- From 5d3ebbb0a04056e46c4c3b8f87d3022098e5931e Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Wed, 20 Aug 2025 19:12:10 -0300 Subject: [PATCH 20/44] [Routing] Remove deprecated `getEnv` and `setEnv` methods --- Attribute/Route.php | 27 --------------------------- CHANGELOG.md | 1 + 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/Attribute/Route.php b/Attribute/Route.php index 1ad37e17..738387f1 100644 --- a/Attribute/Route.php +++ b/Attribute/Route.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Routing\Attribute; -use Symfony\Component\Routing\Exception\LogicException; - /** * @author Fabien Potencier * @author Alexander M. Turek @@ -204,31 +202,6 @@ public function getPriority(): ?int return $this->priority; } - /** - * @deprecated since Symfony 7.4, use the {@see setEnvs()} method instead - */ - public function setEnv(?string $env): void - { - trigger_deprecation('symfony/routing', '7.4', 'The "%s()" method is deprecated, use "setEnvs()" instead.', __METHOD__); - $this->env = (array) $env; - } - - /** - * @deprecated since Symfony 7.4, use {@see getEnvs()} method instead - */ - public function getEnv(): ?string - { - trigger_deprecation('symfony/routing', '7.4', 'The "%s()" method is deprecated, use "getEnvs()" instead.', __METHOD__); - if (!$this->env) { - return null; - } - if (\count($this->env) > 1) { - throw new LogicException(\sprintf('The "env" property has %d environments. Use "getEnvs()" to get all of them.', \count($this->env))); - } - - return $this->env[0]; - } - public function setEnvs(array|string $env): void { $this->env = (array) $env; diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4a156f..a12433f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,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 + * Remove `getEnv()` and `setEnv()` methods of the `Route` attribute; use the plurialized `getEnvs()` and `setEnvs()` methods instead 7.4 --- From 236276109ce9048eb2957b972858b8e4ef06cf1b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 31 Jul 2025 10:28:51 +0200 Subject: [PATCH 21/44] [DependencyInjection][Routing] Add JSON schema for validating and autocompleting YAML config files --- Loader/schema/routing.schema.json | 136 ++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Loader/schema/routing.schema.json diff --git a/Loader/schema/routing.schema.json b/Loader/schema/routing.schema.json new file mode 100644 index 00000000..889254b4 --- /dev/null +++ b/Loader/schema/routing.schema.json @@ -0,0 +1,136 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Symfony Routing Configuration", + "description": "Defines the application's URL routes, including imports and environment-specific conditionals.", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9_.-]+$": { + "oneOf": [ + { "$ref": "#/$defs/routeDefinition" }, + { "$ref": "#/$defs/routeImport" }, + { "$ref": "#/$defs/routeAlias" } + ] + }, + "^when@.+$": { + "$ref": "#", + "description": "A container for routes that are only loaded in a specific environment (e.g., 'when@dev')." + } + }, + "additionalProperties": false, + "$defs": { + "routeDefinition": { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ], + "description": "The URL path or a map of locale=>path for localized routes." + }, + "controller": { + "type": "string", + "description": "The controller that handles the request, e.g., 'App\\Controller\\BlogController::show'." + }, + "methods": { + "description": "The HTTP method(s) this route matches.", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "requirements": { + "type": "object", + "description": "Regular expression constraints for path parameters.", + "additionalProperties": { "type": "string" } + }, + "defaults": { "type": "object" }, + "options": { "type": "object" }, + "host": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ] + }, + "schemes": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "condition": { "type": "string" }, + "locale": { "type": "string" }, + "format": { "type": "string" }, + "utf8": { "type": "boolean" }, + "stateless": { "type": "boolean" } + }, + "required": ["path"], + "additionalProperties": false + }, + "routeImport": { + "type": "object", + "properties": { + "resource": { "type": "string", "description": "Path to the resource to import." }, + "type": { + "type": "string", + "description": "The type of the resource (e.g., 'attribute', 'annotation', 'yaml')." + }, + "prefix": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } } } + ], + "description": "A URL prefix to apply to all routes from the imported resource." + }, + "name_prefix": { + "type": "string", + "description": "A name prefix to apply to all routes from the imported resource." + }, + "requirements": { "type": "object", "additionalProperties": { "type": "string" } }, + "defaults": { "type": "object" }, + "options": { "type": "object" }, + "host": { + "oneOf": [ + { "type": "string" }, + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } + ] + }, + "schemes": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "condition": { "type": "string" }, + "trailing_slash_on_root": { "type": "boolean" }, + "methods": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }, + "locale": { "type": "string" }, + "format": { "type": "string" }, + "utf8": { "type": "boolean" }, + "exclude": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }, + "stateless": { "type": "boolean" }, + "controller": { "type": "string" } + }, + "required": ["resource"], + "additionalProperties": false + }, + "routeAlias": { + "type": "object", + "properties": { + "alias": { "type": "string" }, + "deprecated": { + "type": "object", + "properties": { + "package": { "type": "string" }, + "version": { "type": "string" }, + "message": { "type": "string" } + }, + "required": ["package", "version"], + "additionalProperties": false + } + }, + "required": ["alias"], + "additionalProperties": false + } + } +} From cab332ef1900e2c6222a32fd56436261e8c74759 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 21 Aug 2025 15:48:39 +0200 Subject: [PATCH 22/44] [Routing][Serializer] Deprecate annotation aliases and getters and setters in favor of public properties on attributes --- Annotation/Route.php | 3 + Attribute/DeprecatedAlias.php | 12 ++-- Attribute/Route.php | 109 +++++++++++++++++--------------- CHANGELOG.md | 2 + Loader/AttributeClassLoader.php | 79 +++++++++++------------ Tests/Attribute/RouteTest.php | 26 ++++---- 6 files changed, 123 insertions(+), 108 deletions(-) diff --git a/Annotation/Route.php b/Annotation/Route.php index dda3bdad..1a85a703 100644 --- a/Annotation/Route.php +++ b/Annotation/Route.php @@ -16,6 +16,9 @@ class_exists(\Symfony\Component\Routing\Attribute\Route::class); if (false) { + /** + * @deprecated since Symfony 7.4, use {@see \Symfony\Component\Routing\Attribute\Route} instead + */ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class Route extends \Symfony\Component\Routing\Attribute\Route { diff --git a/Attribute/DeprecatedAlias.php b/Attribute/DeprecatedAlias.php index ae5a6821..5861f2a2 100644 --- a/Attribute/DeprecatedAlias.php +++ b/Attribute/DeprecatedAlias.php @@ -17,28 +17,32 @@ class DeprecatedAlias { public function __construct( - private string $aliasName, - private string $package, - private string $version, - private string $message = '', + public readonly string $aliasName, + public readonly string $package, + public readonly string $version, + public readonly string $message = '', ) { } + #[\Deprecated('Use the "message" property instead', 'symfony/routing:7.4')] public function getMessage(): string { return $this->message; } + #[\Deprecated('Use the "aliasName" property instead', 'symfony/routing:7.4')] public function getAliasName(): string { return $this->aliasName; } + #[\Deprecated('Use the "package" property instead', 'symfony/routing:7.4')] public function getPackage(): string { return $this->package; } + #[\Deprecated('Use the "version" property instead', 'symfony/routing:7.4')] public function getVersion(): string { return $this->version; diff --git a/Attribute/Route.php b/Attribute/Route.php index 1ad37e17..172b621a 100644 --- a/Attribute/Route.php +++ b/Attribute/Route.php @@ -20,16 +20,17 @@ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class Route { - private ?string $path = null; - private array $localizedPaths = []; - private array $methods; /** @var string[] */ - private array $env; - private array $schemes; - /** - * @var (string|DeprecatedAlias)[] - */ - private array $aliases = []; + public array $methods; + + /** @var string[] */ + public array $envs; + + /** @var string[] */ + public array $schemes; + + /** @var (string|DeprecatedAlias)[] */ + public array $aliases = []; /** * @param string|array|null $path The route path (i.e. "/user/login") @@ -50,16 +51,16 @@ class Route * @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $alias The list of aliases for this route */ public function __construct( - string|array|null $path = null, - private ?string $name = null, - private array $requirements = [], - private array $options = [], - private array $defaults = [], - private ?string $host = null, + public string|array|null $path = null, + public ?string $name = null, + public array $requirements = [], + public array $options = [], + public array $defaults = [], + public ?string $host = null, array|string $methods = [], array|string $schemes = [], - private ?string $condition = null, - private ?int $priority = null, + public ?string $condition = null, + public ?int $priority = null, ?string $locale = null, ?string $format = null, ?bool $utf8 = null, @@ -67,15 +68,11 @@ public function __construct( string|array|null $env = null, string|DeprecatedAlias|array $alias = [], ) { - if (\is_array($path)) { - $this->localizedPaths = $path; - } else { - $this->path = $path; - } - $this->setMethods($methods); - $this->setSchemes($schemes); - $this->setAliases($alias); - $this->setEnvs((array) $env); + $this->path = $path; + $this->methods = (array) $methods; + $this->schemes = (array) $schemes; + $this->envs = (array) $env; + $this->aliases = \is_array($alias) ? $alias : [$alias]; if (null !== $locale) { $this->defaults['_locale'] = $locale; @@ -94,154 +91,161 @@ public function __construct( } } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function setPath(string $path): void { $this->path = $path; } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function getPath(): ?string { - return $this->path; + return \is_array($this->path) ? null : $this->path; } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function setLocalizedPaths(array $localizedPaths): void { - $this->localizedPaths = $localizedPaths; + $this->path = $localizedPaths; } + #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] public function getLocalizedPaths(): array { - return $this->localizedPaths; + return \is_array($this->path) ? $this->path : []; } + #[\Deprecated('Use the "host" property instead', 'symfony/routing:7.4')] public function setHost(string $pattern): void { $this->host = $pattern; } + #[\Deprecated('Use the "host" property instead', 'symfony/routing:7.4')] public function getHost(): ?string { return $this->host; } + #[\Deprecated('Use the "name" property instead', 'symfony/routing:7.4')] public function setName(string $name): void { $this->name = $name; } + #[\Deprecated('Use the "name" property instead', 'symfony/routing:7.4')] public function getName(): ?string { return $this->name; } + #[\Deprecated('Use the "requirements" property instead', 'symfony/routing:7.4')] public function setRequirements(array $requirements): void { $this->requirements = $requirements; } + #[\Deprecated('Use the "requirements" property instead', 'symfony/routing:7.4')] public function getRequirements(): array { return $this->requirements; } + #[\Deprecated('Use the "options" property instead', 'symfony/routing:7.4')] public function setOptions(array $options): void { $this->options = $options; } + #[\Deprecated('Use the "options" property instead', 'symfony/routing:7.4')] public function getOptions(): array { return $this->options; } + #[\Deprecated('Use the "defaults" property instead', 'symfony/routing:7.4')] public function setDefaults(array $defaults): void { $this->defaults = $defaults; } + #[\Deprecated('Use the "defaults" property instead', 'symfony/routing:7.4')] public function getDefaults(): array { return $this->defaults; } + #[\Deprecated('Use the "schemes" property instead', 'symfony/routing:7.4')] public function setSchemes(array|string $schemes): void { $this->schemes = (array) $schemes; } + #[\Deprecated('Use the "schemes" property instead', 'symfony/routing:7.4')] public function getSchemes(): array { return $this->schemes; } + #[\Deprecated('Use the "methods" property instead', 'symfony/routing:7.4')] public function setMethods(array|string $methods): void { $this->methods = (array) $methods; } + #[\Deprecated('Use the "methods" property instead', 'symfony/routing:7.4')] public function getMethods(): array { return $this->methods; } + #[\Deprecated('Use the "condition" property instead', 'symfony/routing:7.4')] public function setCondition(?string $condition): void { $this->condition = $condition; } + #[\Deprecated('Use the "condition" property instead', 'symfony/routing:7.4')] public function getCondition(): ?string { return $this->condition; } + #[\Deprecated('Use the "priority" property instead', 'symfony/routing:7.4')] public function setPriority(int $priority): void { $this->priority = $priority; } + #[\Deprecated('Use the "priority" property instead', 'symfony/routing:7.4')] public function getPriority(): ?int { return $this->priority; } - /** - * @deprecated since Symfony 7.4, use the {@see setEnvs()} method instead - */ + #[\Deprecated('Use the "envs" property instead', 'symfony/routing:7.4')] public function setEnv(?string $env): void { - trigger_deprecation('symfony/routing', '7.4', 'The "%s()" method is deprecated, use "setEnvs()" instead.', __METHOD__); - $this->env = (array) $env; + $this->envs = (array) $env; } - /** - * @deprecated since Symfony 7.4, use {@see getEnvs()} method instead - */ + #[\Deprecated('Use the "envs" property instead', 'symfony/routing:7.4')] public function getEnv(): ?string { - trigger_deprecation('symfony/routing', '7.4', 'The "%s()" method is deprecated, use "getEnvs()" instead.', __METHOD__); - if (!$this->env) { + if (!$this->envs) { return null; } - if (\count($this->env) > 1) { - throw new LogicException(\sprintf('The "env" property has %d environments. Use "getEnvs()" to get all of them.', \count($this->env))); + if (\count($this->envs) > 1) { + throw new LogicException(\sprintf('The "env" property has %d environments. Use "getEnvs()" to get all of them.', \count($this->envs))); } - return $this->env[0]; - } - - public function setEnvs(array|string $env): void - { - $this->env = (array) $env; - } - - public function getEnvs(): array - { - return $this->env; + return $this->envs[0]; } /** * @return (string|DeprecatedAlias)[] */ + #[\Deprecated('Use the "aliases" property instead', 'symfony/routing:7.4')] public function getAliases(): array { return $this->aliases; @@ -250,6 +254,7 @@ public function getAliases(): array /** * @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $aliases */ + #[\Deprecated('Use the "aliases" property instead', 'symfony/routing:7.4')] public function setAliases(string|DeprecatedAlias|array $aliases): void { $this->aliases = \is_array($aliases) ? $aliases : [$aliases]; diff --git a/CHANGELOG.md b/CHANGELOG.md index 86af6723..583fd145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG * Allow query-specific parameters in `UrlGenerator` using `_query` * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute * Add argument `$parameters` to `RequestContext`'s constructor + * Deprecate class aliases in the `Annotation` namespace, use attributes instead + * Deprecate getters and setters in attribute classes in favor of public properties 7.3 --- diff --git a/Loader/AttributeClassLoader.php b/Loader/AttributeClassLoader.php index 5fdd7906..9c133bd9 100644 --- a/Loader/AttributeClassLoader.php +++ b/Loader/AttributeClassLoader.php @@ -112,7 +112,7 @@ public function load(mixed $class, ?string $type = null): RouteCollection if (!$class->hasMethod('__invoke')) { foreach ($this->getAttributes($class) as $attr) { - if ($attr->getAliases()) { + if ($attr->aliases) { throw new InvalidArgumentException(\sprintf('Route aliases cannot be used on non-invokable class "%s".', $class->getName())); } } @@ -161,14 +161,14 @@ public function load(mixed $class, ?string $type = null): RouteCollection */ protected function addRoute(RouteCollection $collection, object $attr, array $globals, \ReflectionClass $class, \ReflectionMethod $method): void { - if ($attr->getEnvs() && !\in_array($this->env, $attr->getEnvs(), true)) { + if ($attr->envs && !\in_array($this->env, $attr->envs, true)) { return; } - $name = $attr->getName() ?? $this->getDefaultRouteName($class, $method); + $name = $attr->name ?? $this->getDefaultRouteName($class, $method); $name = $globals['name'].$name; - $requirements = $attr->getRequirements(); + $requirements = $attr->requirements; foreach ($requirements as $placeholder => $requirement) { if (\is_int($placeholder)) { @@ -176,17 +176,17 @@ protected function addRoute(RouteCollection $collection, object $attr, array $gl } } - $defaults = array_replace($globals['defaults'], $attr->getDefaults()); + $defaults = array_replace($globals['defaults'], $attr->defaults); $requirements = array_replace($globals['requirements'], $requirements); - $options = array_replace($globals['options'], $attr->getOptions()); - $schemes = array_unique(array_merge($globals['schemes'], $attr->getSchemes())); - $methods = array_unique(array_merge($globals['methods'], $attr->getMethods())); + $options = array_replace($globals['options'], $attr->options); + $schemes = array_unique(array_merge($globals['schemes'], $attr->schemes)); + $methods = array_unique(array_merge($globals['methods'], $attr->methods)); - $host = $attr->getHost() ?? $globals['host']; - $condition = $attr->getCondition() ?? $globals['condition']; - $priority = $attr->getPriority() ?? $globals['priority']; + $host = $attr->host ?? $globals['host']; + $condition = $attr->condition ?? $globals['condition']; + $priority = $attr->priority ?? $globals['priority']; - $path = $attr->getLocalizedPaths() ?: $attr->getPath(); + $path = $attr->path; $prefix = $globals['localized_paths'] ?: $globals['path']; $paths = []; @@ -241,13 +241,13 @@ protected function addRoute(RouteCollection $collection, object $attr, array $gl } else { $collection->add($name, $route, $priority); } - foreach ($attr->getAliases() as $aliasAttribute) { + foreach ($attr->aliases as $aliasAttribute) { if ($aliasAttribute instanceof DeprecatedAlias) { - $alias = $collection->addAlias($aliasAttribute->getAliasName(), $name); + $alias = $collection->addAlias($aliasAttribute->aliasName, $name); $alias->setDeprecated( - $aliasAttribute->getPackage(), - $aliasAttribute->getVersion(), - $aliasAttribute->getMessage() + $aliasAttribute->package, + $aliasAttribute->version, + $aliasAttribute->message ); continue; } @@ -299,46 +299,47 @@ protected function getGlobals(\ReflectionClass $class): array if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { $attr = $attribute->newInstance(); - if (null !== $attr->getName()) { - $globals['name'] = $attr->getName(); + if (null !== $attr->name) { + $globals['name'] = $attr->name; } - if (null !== $attr->getPath()) { - $globals['path'] = $attr->getPath(); + if (\is_string($attr->path)) { + $globals['path'] = $attr->path; + $globals['localized_paths'] = []; + } else { + $globals['localized_paths'] = $attr->path ?? []; } - $globals['localized_paths'] = $attr->getLocalizedPaths(); - - if (null !== $attr->getRequirements()) { - $globals['requirements'] = $attr->getRequirements(); + if (null !== $attr->requirements) { + $globals['requirements'] = $attr->requirements; } - if (null !== $attr->getOptions()) { - $globals['options'] = $attr->getOptions(); + if (null !== $attr->options) { + $globals['options'] = $attr->options; } - if (null !== $attr->getDefaults()) { - $globals['defaults'] = $attr->getDefaults(); + if (null !== $attr->defaults) { + $globals['defaults'] = $attr->defaults; } - if (null !== $attr->getSchemes()) { - $globals['schemes'] = $attr->getSchemes(); + if (null !== $attr->schemes) { + $globals['schemes'] = $attr->schemes; } - if (null !== $attr->getMethods()) { - $globals['methods'] = $attr->getMethods(); + if (null !== $attr->methods) { + $globals['methods'] = $attr->methods; } - if (null !== $attr->getHost()) { - $globals['host'] = $attr->getHost(); + if (null !== $attr->host) { + $globals['host'] = $attr->host; } - if (null !== $attr->getCondition()) { - $globals['condition'] = $attr->getCondition(); + if (null !== $attr->condition) { + $globals['condition'] = $attr->condition; } - $globals['priority'] = $attr->getPriority() ?? 0; - $globals['env'] = $attr->getEnvs(); + $globals['priority'] = $attr->priority ?? 0; + $globals['env'] = $attr->envs; foreach ($globals['requirements'] as $placeholder => $requirement) { if (\is_int($placeholder)) { diff --git a/Tests/Attribute/RouteTest.php b/Tests/Attribute/RouteTest.php index 8ff0a4dd..1ca2b98b 100644 --- a/Tests/Attribute/RouteTest.php +++ b/Tests/Attribute/RouteTest.php @@ -19,27 +19,27 @@ class RouteTest extends TestCase { #[DataProvider('getValidParameters')] - public function testLoadFromAttribute(string $methodName, string $getter, mixed $expectedReturn) + public function testLoadFromAttribute(string $methodName, string $property, mixed $expectedReturn) { $route = (new \ReflectionMethod(FooController::class, $methodName))->getAttributes(Route::class)[0]->newInstance(); - $this->assertEquals($route->$getter(), $expectedReturn); + $this->assertEquals($route->$property, $expectedReturn); } public static function getValidParameters(): iterable { return [ - ['simplePath', 'getPath', '/Blog'], - ['localized', 'getLocalizedPaths', ['nl' => '/hier', 'en' => '/here']], - ['requirements', 'getRequirements', ['locale' => 'en']], - ['options', 'getOptions', ['compiler_class' => 'RouteCompiler']], - ['name', 'getName', 'blog_index'], - ['defaults', 'getDefaults', ['_controller' => 'MyBlogBundle:Blog:index']], - ['schemes', 'getSchemes', ['https']], - ['methods', 'getMethods', ['GET', 'POST']], - ['host', 'getHost', '{locale}.example.com'], - ['condition', 'getCondition', 'context.getMethod() == \'GET\''], - ['alias', 'getAliases', ['alias', 'completely_different_name']], + ['simplePath', 'path', '/Blog'], + ['localized', 'path', ['nl' => '/hier', 'en' => '/here']], + ['requirements', 'requirements', ['locale' => 'en']], + ['options', 'options', ['compiler_class' => 'RouteCompiler']], + ['name', 'name', 'blog_index'], + ['defaults', 'defaults', ['_controller' => 'MyBlogBundle:Blog:index']], + ['schemes', 'schemes', ['https']], + ['methods', 'methods', ['GET', 'POST']], + ['host', 'host', '{locale}.example.com'], + ['condition', 'condition', 'context.getMethod() == \'GET\''], + ['alias', 'aliases', ['alias', 'completely_different_name']], ]; } } From 0ca1a7e73e29907843f683a54b58aa002f4107cf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 21 Aug 2025 16:05:23 +0200 Subject: [PATCH 23/44] [Routing][Serializer] Remove annotation aliases and getters and setters in favor of public properties on attributes --- Annotation/Route.php | 26 --- Attribute/DeprecatedAlias.php | 24 --- Attribute/Route.php | 173 ------------------ CHANGELOG.md | 3 +- .../RouteWithPriorityController.php | 2 +- 5 files changed, 3 insertions(+), 225 deletions(-) delete mode 100644 Annotation/Route.php diff --git a/Annotation/Route.php b/Annotation/Route.php deleted file mode 100644 index 1a85a703..00000000 --- a/Annotation/Route.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Routing\Annotation; - -// do not deprecate in 6.4/7.0, to make it easier for the ecosystem to support 6.4, 7.4 and 8.0 simultaneously - -class_exists(\Symfony\Component\Routing\Attribute\Route::class); - -if (false) { - /** - * @deprecated since Symfony 7.4, use {@see \Symfony\Component\Routing\Attribute\Route} instead - */ - #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] - class Route extends \Symfony\Component\Routing\Attribute\Route - { - } -} diff --git a/Attribute/DeprecatedAlias.php b/Attribute/DeprecatedAlias.php index 5861f2a2..50329117 100644 --- a/Attribute/DeprecatedAlias.php +++ b/Attribute/DeprecatedAlias.php @@ -23,28 +23,4 @@ public function __construct( public readonly string $message = '', ) { } - - #[\Deprecated('Use the "message" property instead', 'symfony/routing:7.4')] - public function getMessage(): string - { - return $this->message; - } - - #[\Deprecated('Use the "aliasName" property instead', 'symfony/routing:7.4')] - public function getAliasName(): string - { - return $this->aliasName; - } - - #[\Deprecated('Use the "package" property instead', 'symfony/routing:7.4')] - public function getPackage(): string - { - return $this->package; - } - - #[\Deprecated('Use the "version" property instead', 'symfony/routing:7.4')] - public function getVersion(): string - { - return $this->version; - } } diff --git a/Attribute/Route.php b/Attribute/Route.php index b0598b30..0d0fc807 100644 --- a/Attribute/Route.php +++ b/Attribute/Route.php @@ -88,177 +88,4 @@ public function __construct( $this->defaults['_stateless'] = $stateless; } } - - #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] - public function setPath(string $path): void - { - $this->path = $path; - } - - #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] - public function getPath(): ?string - { - return \is_array($this->path) ? null : $this->path; - } - - #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] - public function setLocalizedPaths(array $localizedPaths): void - { - $this->path = $localizedPaths; - } - - #[\Deprecated('Use the "path" property instead', 'symfony/routing:7.4')] - public function getLocalizedPaths(): array - { - return \is_array($this->path) ? $this->path : []; - } - - #[\Deprecated('Use the "host" property instead', 'symfony/routing:7.4')] - public function setHost(string $pattern): void - { - $this->host = $pattern; - } - - #[\Deprecated('Use the "host" property instead', 'symfony/routing:7.4')] - public function getHost(): ?string - { - return $this->host; - } - - #[\Deprecated('Use the "name" property instead', 'symfony/routing:7.4')] - public function setName(string $name): void - { - $this->name = $name; - } - - #[\Deprecated('Use the "name" property instead', 'symfony/routing:7.4')] - public function getName(): ?string - { - return $this->name; - } - - #[\Deprecated('Use the "requirements" property instead', 'symfony/routing:7.4')] - public function setRequirements(array $requirements): void - { - $this->requirements = $requirements; - } - - #[\Deprecated('Use the "requirements" property instead', 'symfony/routing:7.4')] - public function getRequirements(): array - { - return $this->requirements; - } - - #[\Deprecated('Use the "options" property instead', 'symfony/routing:7.4')] - public function setOptions(array $options): void - { - $this->options = $options; - } - - #[\Deprecated('Use the "options" property instead', 'symfony/routing:7.4')] - public function getOptions(): array - { - return $this->options; - } - - #[\Deprecated('Use the "defaults" property instead', 'symfony/routing:7.4')] - public function setDefaults(array $defaults): void - { - $this->defaults = $defaults; - } - - #[\Deprecated('Use the "defaults" property instead', 'symfony/routing:7.4')] - public function getDefaults(): array - { - return $this->defaults; - } - - #[\Deprecated('Use the "schemes" property instead', 'symfony/routing:7.4')] - public function setSchemes(array|string $schemes): void - { - $this->schemes = (array) $schemes; - } - - #[\Deprecated('Use the "schemes" property instead', 'symfony/routing:7.4')] - public function getSchemes(): array - { - return $this->schemes; - } - - #[\Deprecated('Use the "methods" property instead', 'symfony/routing:7.4')] - public function setMethods(array|string $methods): void - { - $this->methods = (array) $methods; - } - - #[\Deprecated('Use the "methods" property instead', 'symfony/routing:7.4')] - public function getMethods(): array - { - return $this->methods; - } - - #[\Deprecated('Use the "condition" property instead', 'symfony/routing:7.4')] - public function setCondition(?string $condition): void - { - $this->condition = $condition; - } - - #[\Deprecated('Use the "condition" property instead', 'symfony/routing:7.4')] - public function getCondition(): ?string - { - return $this->condition; - } - - #[\Deprecated('Use the "priority" property instead', 'symfony/routing:7.4')] - public function setPriority(int $priority): void - { - $this->priority = $priority; - } - - #[\Deprecated('Use the "priority" property instead', 'symfony/routing:7.4')] - public function getPriority(): ?int - { - return $this->priority; - } - - #[\Deprecated('Use the "envs" property instead', 'symfony/routing:7.4')] - public function setEnv(?string $env): void - { - $this->envs = (array) $env; - } - - #[\Deprecated('Use the "envs" property instead', 'symfony/routing:7.4')] - public function getEnv(): ?string - { - if (!$this->envs) { - return null; - } - if (\count($this->envs) > 1) { - throw new LogicException(\sprintf('The "env" property has %d environments. Use "getEnvs()" to get all of them.', \count($this->envs))); - } - - return $this->envs[0]; - } - - /** - * @return (string|DeprecatedAlias)[] - */ - #[\Deprecated('Use the "aliases" property instead', 'symfony/routing:7.4')] - public function getAliases(): array - { - return $this->aliases; - } - - /** - * @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $aliases - */ - #[\Deprecated('Use the "aliases" property instead', 'symfony/routing:7.4')] - public function setAliases(string|DeprecatedAlias|array $aliases): void - { - $this->aliases = \is_array($aliases) ? $aliases : [$aliases]; - } -} - -if (!class_exists(\Symfony\Component\Routing\Annotation\Route::class, false)) { - class_alias(Route::class, \Symfony\Component\Routing\Annotation\Route::class); } diff --git a/CHANGELOG.md b/CHANGELOG.md index f673aa04..222995e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ 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 - * Remove `getEnv()` and `setEnv()` methods of the `Route` attribute; use the plurialized `getEnvs()` and `setEnvs()` methods instead + * Remove class aliases in the `Annotation` namespace, use attributes instead + * Remove getters and setters in attribute classes in favor of public properties 7.4 --- diff --git a/Tests/Fixtures/AttributeFixtures/RouteWithPriorityController.php b/Tests/Fixtures/AttributeFixtures/RouteWithPriorityController.php index 0cc53242..ba041db7 100644 --- a/Tests/Fixtures/AttributeFixtures/RouteWithPriorityController.php +++ b/Tests/Fixtures/AttributeFixtures/RouteWithPriorityController.php @@ -2,7 +2,7 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; class RouteWithPriorityController { From 7b3ff9ae5c10aeae08116e89f11fe31d22394df8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 21 Aug 2025 17:52:30 +0200 Subject: [PATCH 24/44] [Routing][FrameworkBundle] Auto-register routes from attributes found on controller services --- CHANGELOG.md | 1 + DependencyInjection/RoutingControllerPass.php | 40 ++++++++++++ Loader/AttributeServicesLoader.php | 47 ++++++++++++++ .../RoutingControllerPassTest.php | 54 +++++++++++++++ Tests/Loader/AttributeServicesLoaderTest.php | 65 +++++++++++++++++++ 5 files changed, 207 insertions(+) create mode 100644 DependencyInjection/RoutingControllerPass.php create mode 100644 Loader/AttributeServicesLoader.php create mode 100644 Tests/DependencyInjection/RoutingControllerPassTest.php create mode 100644 Tests/Loader/AttributeServicesLoaderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 583fd145..19346f71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.4 --- + * Add `AttributeServicesLoader` and `RoutingControllerPass` to auto-register routes from attributes on services * Allow query-specific parameters in `UrlGenerator` using `_query` * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute * Add argument `$parameters` to `RequestContext`'s constructor diff --git a/DependencyInjection/RoutingControllerPass.php b/DependencyInjection/RoutingControllerPass.php new file mode 100644 index 00000000..9578faf2 --- /dev/null +++ b/DependencyInjection/RoutingControllerPass.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Nicolas Grekas + */ +class RoutingControllerPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('routing.loader.attribute.services')) { + return; + } + + $resolve = $container->getParameterBag()->resolveValue(...); + $taggedClasses = []; + foreach ($this->findAndSortTaggedServices('routing.controller', $container) as $id) { + $taggedClasses[$resolve($container->getDefinition($id)->getClass())] = true; + } + + $container->getDefinition('routing.loader.attribute.services') + ->replaceArgument(0, array_keys($taggedClasses)); + } +} diff --git a/Loader/AttributeServicesLoader.php b/Loader/AttributeServicesLoader.php new file mode 100644 index 00000000..d95d9532 --- /dev/null +++ b/Loader/AttributeServicesLoader.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\Loader; +use Symfony\Component\Routing\RouteCollection; + +/** + * Loads routes from a list of tagged classes by delegating to the attribute class loader. + * + * @author Nicolas Grekas + */ +class AttributeServicesLoader extends Loader +{ + /** + * @param class-string[] $taggedClasses + */ + public function __construct( + private array $taggedClasses = [], + ) { + } + + public function load(mixed $resource, ?string $type = null): RouteCollection + { + $collection = new RouteCollection(); + + foreach ($this->taggedClasses as $class) { + $collection->addCollection($this->import($class, 'attribute')); + } + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return 'tagged_services' === $type && 'attributes' === $resource; + } +} diff --git a/Tests/DependencyInjection/RoutingControllerPassTest.php b/Tests/DependencyInjection/RoutingControllerPassTest.php new file mode 100644 index 00000000..d743a338 --- /dev/null +++ b/Tests/DependencyInjection/RoutingControllerPassTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\Routing\DependencyInjection\RoutingControllerPass; + +class RoutingControllerPassTest extends TestCase +{ + public function testProcessInjectsTaggedControllerClassesOrderedAndUnique() + { + $container = new ContainerBuilder(); + $container->setParameter('ctrl_a.class', CtrlA::class); + + $container->register('routing.loader.attribute.services', \stdClass::class) + ->setArguments([null]); + + $container->register('ctrl_a', '%ctrl_a.class%')->addTag('routing.controller', ['priority' => 10]); + $container->register('ctrl_b', CtrlB::class)->addTag('routing.controller', ['priority' => 20]); + $container->register('ctrl_c', CtrlC::class)->addTag('routing.controller', ['priority' => -5]); + + (new RoutingControllerPass())->process($container); + + $this->assertSame([ + CtrlB::class, + CtrlA::class, + CtrlC::class, + ], $container->getDefinition('routing.loader.attribute.services')->getArgument(0)); + } + + public function testProcessWithNoTaggedControllersSetsEmptyList() + { + $container = new ContainerBuilder(); + + $loaderDef = new Definition(\stdClass::class); + $loaderDef->setArguments([['preexisting']]); + $container->setDefinition('routing.loader.attribute.services', $loaderDef); + + (new RoutingControllerPass())->process($container); + + $this->assertSame([], $container->getDefinition('routing.loader.attribute.services')->getArgument(0)); + } +} diff --git a/Tests/Loader/AttributeServicesLoaderTest.php b/Tests/Loader/AttributeServicesLoaderTest.php new file mode 100644 index 00000000..31d63ec7 --- /dev/null +++ b/Tests/Loader/AttributeServicesLoaderTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\Routing\Loader\AttributeServicesLoader; +use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\ActionPathController; +use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\MethodActionControllers; +use Symfony\Component\Routing\Tests\Fixtures\TraceableAttributeClassLoader; + +class AttributeServicesLoaderTest extends TestCase +{ + public function testSupports() + { + $loader = new AttributeServicesLoader(); + + $this->assertFalse($loader->supports('attributes', null)); + $this->assertFalse($loader->supports('attributes', 'attribute')); + $this->assertFalse($loader->supports('other', 'tagged_services')); + $this->assertTrue($loader->supports('attributes', 'tagged_services')); + } + + public function testDelegatesToAttributeLoaderAndMergesCollections() + { + $attributeLoader = new TraceableAttributeClassLoader(); + + $servicesLoader = new AttributeServicesLoader([ + ActionPathController::class, + MethodActionControllers::class, + ]); + + $resolver = new LoaderResolver([ + $attributeLoader, + $servicesLoader, + ]); + + $attributeLoader->setResolver($resolver); + $servicesLoader->setResolver($resolver); + + $collection = $servicesLoader->load('attributes', 'tagged_services'); + + $this->assertArrayHasKey('action', $collection->all()); + $this->assertArrayHasKey('put', $collection->all()); + $this->assertArrayHasKey('post', $collection->all()); + + $this->assertSame(['/path'], [$collection->get('action')->getPath()]); + $this->assertSame('/the/path', $collection->get('put')->getPath()); + $this->assertSame('/the/path', $collection->get('post')->getPath()); + + $this->assertSame([ + ActionPathController::class, + MethodActionControllers::class, + ], $attributeLoader->foundClasses); + } +} From d2e6f5c3f912b443d787ed31f0006dda82fcd471 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 25 Aug 2025 08:55:45 +0200 Subject: [PATCH 25/44] make RoutingControllerPass and AttributeServicesLoader final --- DependencyInjection/RoutingControllerPass.php | 2 +- Loader/AttributeServicesLoader.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/RoutingControllerPass.php b/DependencyInjection/RoutingControllerPass.php index 9578faf2..4074a6e0 100644 --- a/DependencyInjection/RoutingControllerPass.php +++ b/DependencyInjection/RoutingControllerPass.php @@ -18,7 +18,7 @@ /** * @author Nicolas Grekas */ -class RoutingControllerPass implements CompilerPassInterface +final class RoutingControllerPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; diff --git a/Loader/AttributeServicesLoader.php b/Loader/AttributeServicesLoader.php index d95d9532..a3c8f493 100644 --- a/Loader/AttributeServicesLoader.php +++ b/Loader/AttributeServicesLoader.php @@ -19,7 +19,7 @@ * * @author Nicolas Grekas */ -class AttributeServicesLoader extends Loader +final class AttributeServicesLoader extends Loader { /** * @param class-string[] $taggedClasses From 0d12667d0f1ffb762337c67650d1c24743dea9fa Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 9 Sep 2025 15:44:15 +0200 Subject: [PATCH 26/44] Remove needless calls to defined() --- Loader/AttributeFileLoader.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Loader/AttributeFileLoader.php b/Loader/AttributeFileLoader.php index 0013ec22..103ada2d 100644 --- a/Loader/AttributeFileLoader.php +++ b/Loader/AttributeFileLoader.php @@ -79,10 +79,7 @@ protected function findClass(string $file): string|false throw new \InvalidArgumentException(\sprintf('The file "%s" does not contain PHP code. Did you forget to add the " true, \T_STRING => true]; - if (\defined('T_NAME_QUALIFIED')) { - $nsTokens[\T_NAME_QUALIFIED] = true; - } + $nsTokens = [\T_NS_SEPARATOR => true, \T_STRING => true, \T_NAME_QUALIFIED => true]; for ($i = 0; isset($tokens[$i]); ++$i) { $token = $tokens[$i]; if (!isset($token[1])) { From 58eac8941c621649765970f037f1cddf5920a4a3 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 29 Sep 2025 14:24:49 +0200 Subject: [PATCH 27/44] do not use deprecated PHPUnit features --- Tests/Loader/DirectoryLoaderTest.php | 3 ++- Tests/Loader/PhpFileLoaderTest.php | 7 ++++--- Tests/Loader/XmlFileLoaderTest.php | 8 ++++---- Tests/Loader/YamlFileLoaderTest.php | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Tests/Loader/DirectoryLoaderTest.php b/Tests/Loader/DirectoryLoaderTest.php index 4315588f..706238f2 100644 --- a/Tests/Loader/DirectoryLoaderTest.php +++ b/Tests/Loader/DirectoryLoaderTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Routing\Loader\AttributeFileLoader; use Symfony\Component\Routing\Loader\DirectoryLoader; use Symfony\Component\Routing\Loader\YamlFileLoader; +use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Tests\Fixtures\TraceableAttributeClassLoader; @@ -53,7 +54,7 @@ private function verifyCollection(RouteCollection $collection) $routes = $collection->all(); $this->assertCount(3, $routes, 'Three routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); for ($i = 1; $i <= 3; ++$i) { $this->assertSame('/route/'.$i, $routes['route'.$i]->getPath()); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index a52b61b6..af5d13a5 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Routing\Loader\AttributeClassLoader; use Symfony\Component\Routing\Loader\PhpFileLoader; use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; @@ -43,7 +44,7 @@ public function testLoadWithRoute() $routes = $routeCollection->all(); $this->assertCount(1, $routes, 'One route is loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/blog/{slug}', $route->getPath()); @@ -63,7 +64,7 @@ public function testLoadWithImport() $routes = $routeCollection->all(); $this->assertCount(1, $routes, 'One route is loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/prefix/blog/{slug}', $route->getPath()); @@ -82,7 +83,7 @@ public function testThatDefiningVariableInConfigFileHasNoSideEffects() $routeCollection = $loader->load('with_define_path_variable.php'); $resources = $routeCollection->getResources(); $this->assertCount(1, $resources); - $this->assertContainsOnly('Symfony\Component\Config\Resource\ResourceInterface', $resources); + $this->assertContainsOnlyInstancesOf(ResourceInterface::class, $resources); $fileResource = reset($resources); $this->assertSame( realpath($locator->locate('with_define_path_variable.php')), diff --git a/Tests/Loader/XmlFileLoaderTest.php b/Tests/Loader/XmlFileLoaderTest.php index e8fe104d..6f206eb7 100644 --- a/Tests/Loader/XmlFileLoaderTest.php +++ b/Tests/Loader/XmlFileLoaderTest.php @@ -80,7 +80,7 @@ public function testLoadWithImport() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/{foo}/blog/{slug}', $route->getPath()); @@ -177,7 +177,7 @@ public function testLoadLocalized() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); $this->assertEquals('/route', $routeCollection->get('localized.fr')->getPath()); $this->assertEquals('/path', $routeCollection->get('localized.en')->getPath()); @@ -190,7 +190,7 @@ public function testLocalizedImports() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); $this->assertEquals('/le-prefix/le-suffix', $routeCollection->get('imported.fr')->getPath()); $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); @@ -206,7 +206,7 @@ public function testLocalizedImportsOfNotLocalizedRoutes() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); $this->assertEquals('/le-prefix/suffix', $routeCollection->get('imported.fr')->getPath()); $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); diff --git a/Tests/Loader/YamlFileLoaderTest.php b/Tests/Loader/YamlFileLoaderTest.php index beb15ae3..e9ccb966 100644 --- a/Tests/Loader/YamlFileLoaderTest.php +++ b/Tests/Loader/YamlFileLoaderTest.php @@ -108,7 +108,7 @@ public function testLoadWithResource() $routes = $routeCollection->all(); $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); + $this->assertContainsOnlyInstancesOf(Route::class, $routes); foreach ($routes as $route) { $this->assertSame('/{foo}/blog/{slug}', $route->getPath()); From a35dc747dd71fc68724490721e2d3d3568fa3228 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 26 Sep 2025 19:17:33 +0200 Subject: [PATCH 28/44] [DependencyInjection][Config][Routing] Deprecate using `$this` or the internal scope of the loader from PHP config files --- CHANGELOG.md | 1 + Loader/PhpFileLoader.php | 14 ++++++++++++-- Tests/Fixtures/legacy_internal_scope.php | 8 ++++++++ Tests/Loader/PhpFileLoaderTest.php | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 Tests/Fixtures/legacy_internal_scope.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 19346f71..7f866f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add argument `$parameters` to `RequestContext`'s constructor * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters and setters in attribute classes in favor of public properties + * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead 7.3 --- diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index adf7eed3..b8f66c93 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -39,9 +39,19 @@ public function load(mixed $file, ?string $type = null): RouteCollection $loader = $this; $load = \Closure::bind(static function ($file) use ($loader) { return include $file; - }, null, ProtectedPhpFileLoader::class); + }, null, null); - $result = $load($path); + try { + $result = $load($path); + } catch (\Error $e) { + $load = \Closure::bind(static function ($file) use ($loader) { + return include $file; + }, null, ProtectedPhpFileLoader::class); + + $result = $load($path); + + trigger_deprecation('symfony/routing', '7.4', 'Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in "%s" on line %d.', $e->getFile(), $e->getLine()); + } if (\is_object($result) && \is_callable($result)) { $collection = $this->callConfigurator($result, $path, $file); diff --git a/Tests/Fixtures/legacy_internal_scope.php b/Tests/Fixtures/legacy_internal_scope.php new file mode 100644 index 00000000..501fa576 --- /dev/null +++ b/Tests/Fixtures/legacy_internal_scope.php @@ -0,0 +1,8 @@ +callConfigurator(static fn () => 'dummy', 'dummy.php', 'dummy.php'); + +return new RouteCollection(); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index af5d13a5..0a085076 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Routing\Tests\Loader; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -258,6 +260,20 @@ public function testRoutingConfiguratorCanImportGlobPatterns() $this->assertSame('AppBundle:Baz:view', $route->getDefault('_controller')); } + #[IgnoreDeprecations] + #[Group('legacy')] + public function testTriggersDeprecationWhenAccessingLoaderInternalScope() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator); + + $this->expectUserDeprecationMessageMatches('{^Since symfony/routing 7.4: Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in ".+" on line \d+\.$}'); + + $routes = $loader->load('legacy_internal_scope.php'); + + $this->assertInstanceOf(RouteCollection::class, $routes); + } + public function testRoutingI18nConfigurator() { $locator = new FileLocator([__DIR__.'/../Fixtures']); From 0498566a4237973999dcc9607a9874547b8c1261 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 21 Aug 2025 14:47:46 +0200 Subject: [PATCH 29/44] [DependencyInjection][Routing] Handle declaring services and routes using PHP arrays that follow the same shape as corresponding yaml files --- CHANGELOG.md | 1 + Loader/PhpFileLoader.php | 74 ++++++++++++++++++++++-- Loader/YamlFileLoader.php | 2 +- Tests/Fixtures/array_routes.php | 6 ++ Tests/Fixtures/array_when_env.php | 8 +++ Tests/Fixtures/legacy_internal_scope.php | 2 +- Tests/Fixtures/when-env.php | 15 +++++ Tests/Loader/PhpFileLoaderTest.php | 28 +++++++++ 8 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 Tests/Fixtures/array_routes.php create mode 100644 Tests/Fixtures/array_when_env.php create mode 100644 Tests/Fixtures/when-env.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f866f69..0c91a0a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Allow query-specific parameters in `UrlGenerator` using `_query` * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute * Add argument `$parameters` to `RequestContext`'s constructor + * Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters and setters in attribute classes in favor of public properties * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index b8f66c93..9e005c83 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -13,6 +13,10 @@ use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; +use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; +use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator; +use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\RouteCollection; @@ -42,13 +46,17 @@ public function load(mixed $file, ?string $type = null): RouteCollection }, null, null); try { - $result = $load($path); + if (1 === $result = $load($path)) { + $result = null; + } } catch (\Error $e) { $load = \Closure::bind(static function ($file) use ($loader) { return include $file; }, null, ProtectedPhpFileLoader::class); - $result = $load($path); + if (1 === $result = $load($path)) { + $result = null; + } trigger_deprecation('symfony/routing', '7.4', 'Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in "%s" on line %d.', $e->getFile(), $e->getLine()); } @@ -56,7 +64,8 @@ public function load(mixed $file, ?string $type = null): RouteCollection if (\is_object($result) && \is_callable($result)) { $collection = $this->callConfigurator($result, $path, $file); } else { - $collection = $result; + $collection = new RouteCollection(); + $this->loadRoutes($collection, $result, $path, $file); } $collection->addResource(new FileResource($path)); @@ -69,14 +78,69 @@ public function supports(mixed $resource, ?string $type = null): bool return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type); } - protected function callConfigurator(callable $result, string $path, string $file): RouteCollection + protected function callConfigurator(callable $callback, string $path, string $file): RouteCollection { $collection = new RouteCollection(); - $result(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); + $result = $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); + $this->loadRoutes($collection, $result, $path, $file); return $collection; } + + private function loadRoutes(RouteCollection $collection, mixed $routes, string $path, string $file): void + { + if (null === $routes + || $routes instanceof RouteCollection + || $routes instanceof AliasConfigurator + || $routes instanceof CollectionConfigurator + || $routes instanceof ImportConfigurator + || $routes instanceof RouteConfigurator + || $routes instanceof RoutingConfigurator + ) { + if ($routes instanceof RouteCollection && $collection !== $routes) { + $collection->addCollection($routes); + } + + return; + } + + if (!is_iterable($routes)) { + throw new \InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); + } + + $loader = new YamlFileLoader($this->locator, $this->env); + + \Closure::bind(function () use ($collection, $routes, $path, $file) { + foreach ($routes as $name => $config) { + if (str_starts_with($name, 'when@')) { + if (!$this->env || 'when@'.$this->env !== $name) { + continue; + } + + foreach ($config as $name => $config) { + $this->validate($config, $name.'" when "@'.$this->env, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + + continue; + } + + $this->validate($config, $name, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + }, $loader, $loader::class)(); + } } /** diff --git a/Loader/YamlFileLoader.php b/Loader/YamlFileLoader.php index 3e40e8bb..d402c8c3 100644 --- a/Loader/YamlFileLoader.php +++ b/Loader/YamlFileLoader.php @@ -246,7 +246,7 @@ protected function parseImport(RouteCollection $collection, array $config, strin protected function validate(mixed $config, string $name, string $path): void { if (!\is_array($config)) { - throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); + throw new \InvalidArgumentException(\sprintf('The definition of "%s" in "%s" must be an array.', $name, $path)); } if (isset($config['alias'])) { $this->validateAlias($config, $name, $path); diff --git a/Tests/Fixtures/array_routes.php b/Tests/Fixtures/array_routes.php new file mode 100644 index 00000000..f3cd0b31 --- /dev/null +++ b/Tests/Fixtures/array_routes.php @@ -0,0 +1,6 @@ + ['path' => '/a'], + 'b' => ['path' => '/b', 'methods' => ['GET']], +]; diff --git a/Tests/Fixtures/array_when_env.php b/Tests/Fixtures/array_when_env.php new file mode 100644 index 00000000..79422109 --- /dev/null +++ b/Tests/Fixtures/array_when_env.php @@ -0,0 +1,8 @@ + [ + 'x' => ['path' => '/x'], + ], + 'a' => ['path' => '/a'], +]; diff --git a/Tests/Fixtures/legacy_internal_scope.php b/Tests/Fixtures/legacy_internal_scope.php index 501fa576..5de3a9e7 100644 --- a/Tests/Fixtures/legacy_internal_scope.php +++ b/Tests/Fixtures/legacy_internal_scope.php @@ -3,6 +3,6 @@ use Symfony\Component\Routing\RouteCollection; // access the loader's internal scope to trigger deprecation -$loader->callConfigurator(static fn () => 'dummy', 'dummy.php', 'dummy.php'); +$loader->callConfigurator(static fn () => [], 'dummy.php', 'dummy.php'); return new RouteCollection(); diff --git a/Tests/Fixtures/when-env.php b/Tests/Fixtures/when-env.php new file mode 100644 index 00000000..cc60e4c8 --- /dev/null +++ b/Tests/Fixtures/when-env.php @@ -0,0 +1,15 @@ +env()) { + $routes->add('b', '/b'); + $routes->add('a', '/a2'); + } elseif ('some-other-env' === $routes->env()) { + $routes->add('a', '/a3'); + $routes->add('c', '/c'); + } + + $routes->add('a', '/a1'); +}; diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index 0a085076..e033d7b7 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -355,6 +355,34 @@ public function testImportingAliases() $this->assertEquals($expectedRoutes('php'), $routes); } + public function testWhenEnv() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator, 'some-env'); + $routes = $loader->load('when-env.php'); + + $this->assertSame(['b', 'a'], array_keys($routes->all())); + $this->assertSame('/b', $routes->get('b')->getPath()); + } + + public function testLoadsArrayRoutes() + { + $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $routes = $loader->load('array_routes.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/b', $routes->get('b')->getPath()); + $this->assertSame(['GET'], $routes->get('b')->getMethods()); + } + + public function testWhenEnvWithArray() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator, 'some-env'); + $routes = $loader->load('array_when_env.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/x', $routes->get('x')->getPath()); + } + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { From 4c83abd7d862e9a9ed77e02eb8d7a23aaf2cddb7 Mon Sep 17 00:00:00 2001 From: matlec Date: Sat, 2 Aug 2025 18:45:11 +0200 Subject: [PATCH 30/44] [DependencyInjection][Routing] Deprecate XML configuration format --- CHANGELOG.md | 1 + Loader/XmlFileLoader.php | 4 ++++ Tests/Loader/XmlFileLoaderTest.php | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c91a0a0..351680a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters and setters in attribute classes in favor of public properties * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead + * Deprecate XML configuration format, use YAML, PHP or attributes instead 7.3 --- diff --git a/Loader/XmlFileLoader.php b/Loader/XmlFileLoader.php index c7275962..1ee2f813 100644 --- a/Loader/XmlFileLoader.php +++ b/Loader/XmlFileLoader.php @@ -24,6 +24,8 @@ * * @author Fabien Potencier * @author Tobias Schultze + * + * @deprecated since Symfony 7.4, use another loader instead */ class XmlFileLoader extends FileLoader { @@ -40,6 +42,8 @@ class XmlFileLoader extends FileLoader */ public function load(mixed $file, ?string $type = null): RouteCollection { + trigger_deprecation('symfony/routing', '7.4', 'XML configuration format is deprecated, use YAML, PHP or attributes instead.'); + $path = $this->locator->locate($file); $xml = $this->loadFile($path); diff --git a/Tests/Loader/XmlFileLoaderTest.php b/Tests/Loader/XmlFileLoaderTest.php index 6f206eb7..5c41009f 100644 --- a/Tests/Loader/XmlFileLoaderTest.php +++ b/Tests/Loader/XmlFileLoaderTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Routing\Tests\Loader; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -24,6 +26,8 @@ use Symfony\Component\Routing\Tests\Fixtures\CustomXmlFileLoader; use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; +#[IgnoreDeprecations] +#[Group('legacy')] class XmlFileLoaderTest extends TestCase { public function testSupports() From 6994e2651432524cc88386aef21a253dfbd1cef7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 2 Oct 2025 16:59:03 +0200 Subject: [PATCH 31/44] Ensure branch 7.4 will remain compatible with 8.0 once XML loaders are removed --- Tests/RouteCollectionTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/RouteCollectionTest.php b/Tests/RouteCollectionTest.php index 7625bcf5..9ffe162b 100644 --- a/Tests/RouteCollectionTest.php +++ b/Tests/RouteCollectionTest.php @@ -105,9 +105,9 @@ public function testAddCollection() public function testAddCollectionWithResources() { $collection = new RouteCollection(); - $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml')); + $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/empty.yml')); $collection1 = new RouteCollection(); - $collection1->addResource($foo1 = new FileResource(__DIR__.'/Fixtures/foo1.xml')); + $collection1->addResource($foo1 = new FileResource(__DIR__.'/Fixtures/file_resource.yml')); $collection->addCollection($collection1); $this->assertEquals([$foo, $foo1], $collection->getResources(), '->addCollection() merges resources'); } @@ -176,9 +176,9 @@ public function testAddPrefixOverridesDefaultsAndRequirements() public function testResource() { $collection = new RouteCollection(); - $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml')); - $collection->addResource($bar = new FileResource(__DIR__.'/Fixtures/bar.xml')); - $collection->addResource(new FileResource(__DIR__.'/Fixtures/foo.xml')); + $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/empty.yml')); + $collection->addResource($bar = new FileResource(__DIR__.'/Fixtures/file_resource.yml')); + $collection->addResource(new FileResource(__DIR__.'/Fixtures/empty.yml')); $this->assertEquals([$foo, $bar], $collection->getResources(), '->addResource() adds a resource and getResources() only returns unique ones by comparing the string representation'); From 87f95ed5e149ae649d0a66d1ca5129a181148604 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 2 Oct 2025 15:38:03 +0200 Subject: [PATCH 32/44] [DependencyInjection][Routing] Remove support for the XML configuration format --- CHANGELOG.md | 1 + Loader/XmlFileLoader.php | 470 ------------- Tests/Fixtures/CustomXmlFileLoader.php | 26 - Tests/Fixtures/alias/alias.xml | 20 - .../alias/invalid-deprecated-no-package.xml | 10 - .../alias/invalid-deprecated-no-version.xml | 10 - Tests/Fixtures/bar.xml | 0 Tests/Fixtures/class-attributes.xml | 9 - .../controller/import__controller.xml | 10 - .../Fixtures/controller/import_controller.xml | 8 - .../controller/import_override_defaults.xml | 10 - .../Fixtures/controller/override_defaults.xml | 10 - Tests/Fixtures/controller/routing.xml | 14 - Tests/Fixtures/defaults.xml | 8 - Tests/Fixtures/foo.xml | 0 Tests/Fixtures/foo1.xml | 0 Tests/Fixtures/glob/bar.xml | 8 - Tests/Fixtures/glob/baz.xml | 8 - Tests/Fixtures/glob/import_multiple.xml | 8 - Tests/Fixtures/glob/import_single.xml | 8 - .../import_with_name_prefix/routing.xml | 10 - .../import_with_no_trailing_slash/routing.xml | 10 - Tests/Fixtures/imported-with-defaults.xml | 11 - Tests/Fixtures/importer-with-defaults.xml | 11 - Tests/Fixtures/list_defaults.xml | 20 - Tests/Fixtures/list_in_list_defaults.xml | 22 - Tests/Fixtures/list_in_map_defaults.xml | 22 - Tests/Fixtures/list_null_values.xml | 22 - Tests/Fixtures/locale_and_host/imported.xml | 19 - .../locale_and_host/importer-with-host.xml | 10 - .../importer-with-locale-and-host.xml | 12 - .../importer-with-single-host.xml | 8 - .../locale_and_host/importer-without-host.xml | 8 - .../locale_and_host/route-with-hosts.xml | 10 - Tests/Fixtures/localized.xml | 13 - ...imported-with-locale-but-not-localized.xml | 9 - .../localized/imported-with-locale.xml | 11 - .../Fixtures/localized/imported-with-utf8.xml | 8 - ...ith-locale-imports-non-localized-route.xml | 10 - .../localized/importer-with-locale.xml | 10 - .../Fixtures/localized/importer-with-utf8.xml | 7 - Tests/Fixtures/localized/utf8.xml | 13 - Tests/Fixtures/map_defaults.xml | 20 - Tests/Fixtures/map_in_list_defaults.xml | 22 - Tests/Fixtures/map_in_map_defaults.xml | 22 - Tests/Fixtures/map_null_values.xml | 22 - Tests/Fixtures/missing_id.xml | 8 - Tests/Fixtures/missing_path.xml | 8 - Tests/Fixtures/namespaceprefix.xml | 16 - Tests/Fixtures/nonvalid-deprecated-route.xml | 10 - Tests/Fixtures/nonvalid.xml | 10 - Tests/Fixtures/nonvalidnode.xml | 8 - Tests/Fixtures/nonvalidroute.xml | 12 - Tests/Fixtures/null_values.xml | 12 - Tests/Fixtures/psr4-attributes.xml | 10 - .../Fixtures/psr4-controllers-redirection.xml | 8 - .../psr4-attributes.xml | 10 - Tests/Fixtures/scalar_defaults.xml | 33 - Tests/Fixtures/validpattern.xml | 18 - Tests/Fixtures/validresource.xml | 13 - Tests/Fixtures/when-env.xml | 17 - Tests/Fixtures/withdoctype.xml | 3 - Tests/Loader/XmlFileLoaderTest.php | 663 ------------------ 63 files changed, 1 insertion(+), 1858 deletions(-) delete mode 100644 Loader/XmlFileLoader.php delete mode 100644 Tests/Fixtures/CustomXmlFileLoader.php delete mode 100644 Tests/Fixtures/alias/alias.xml delete mode 100644 Tests/Fixtures/alias/invalid-deprecated-no-package.xml delete mode 100644 Tests/Fixtures/alias/invalid-deprecated-no-version.xml delete mode 100644 Tests/Fixtures/bar.xml delete mode 100644 Tests/Fixtures/class-attributes.xml delete mode 100644 Tests/Fixtures/controller/import__controller.xml delete mode 100644 Tests/Fixtures/controller/import_controller.xml delete mode 100644 Tests/Fixtures/controller/import_override_defaults.xml delete mode 100644 Tests/Fixtures/controller/override_defaults.xml delete mode 100644 Tests/Fixtures/controller/routing.xml delete mode 100644 Tests/Fixtures/defaults.xml delete mode 100644 Tests/Fixtures/foo.xml delete mode 100644 Tests/Fixtures/foo1.xml delete mode 100644 Tests/Fixtures/glob/bar.xml delete mode 100644 Tests/Fixtures/glob/baz.xml delete mode 100644 Tests/Fixtures/glob/import_multiple.xml delete mode 100644 Tests/Fixtures/glob/import_single.xml delete mode 100644 Tests/Fixtures/import_with_name_prefix/routing.xml delete mode 100644 Tests/Fixtures/import_with_no_trailing_slash/routing.xml delete mode 100644 Tests/Fixtures/imported-with-defaults.xml delete mode 100644 Tests/Fixtures/importer-with-defaults.xml delete mode 100644 Tests/Fixtures/list_defaults.xml delete mode 100644 Tests/Fixtures/list_in_list_defaults.xml delete mode 100644 Tests/Fixtures/list_in_map_defaults.xml delete mode 100644 Tests/Fixtures/list_null_values.xml delete mode 100644 Tests/Fixtures/locale_and_host/imported.xml delete mode 100644 Tests/Fixtures/locale_and_host/importer-with-host.xml delete mode 100644 Tests/Fixtures/locale_and_host/importer-with-locale-and-host.xml delete mode 100644 Tests/Fixtures/locale_and_host/importer-with-single-host.xml delete mode 100644 Tests/Fixtures/locale_and_host/importer-without-host.xml delete mode 100644 Tests/Fixtures/locale_and_host/route-with-hosts.xml delete mode 100644 Tests/Fixtures/localized.xml delete mode 100644 Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml delete mode 100644 Tests/Fixtures/localized/imported-with-locale.xml delete mode 100644 Tests/Fixtures/localized/imported-with-utf8.xml delete mode 100644 Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml delete mode 100644 Tests/Fixtures/localized/importer-with-locale.xml delete mode 100644 Tests/Fixtures/localized/importer-with-utf8.xml delete mode 100644 Tests/Fixtures/localized/utf8.xml delete mode 100644 Tests/Fixtures/map_defaults.xml delete mode 100644 Tests/Fixtures/map_in_list_defaults.xml delete mode 100644 Tests/Fixtures/map_in_map_defaults.xml delete mode 100644 Tests/Fixtures/map_null_values.xml delete mode 100644 Tests/Fixtures/missing_id.xml delete mode 100644 Tests/Fixtures/missing_path.xml delete mode 100644 Tests/Fixtures/namespaceprefix.xml delete mode 100644 Tests/Fixtures/nonvalid-deprecated-route.xml delete mode 100644 Tests/Fixtures/nonvalid.xml delete mode 100644 Tests/Fixtures/nonvalidnode.xml delete mode 100644 Tests/Fixtures/nonvalidroute.xml delete mode 100644 Tests/Fixtures/null_values.xml delete mode 100644 Tests/Fixtures/psr4-attributes.xml delete mode 100644 Tests/Fixtures/psr4-controllers-redirection.xml delete mode 100644 Tests/Fixtures/psr4-controllers-redirection/psr4-attributes.xml delete mode 100644 Tests/Fixtures/scalar_defaults.xml delete mode 100644 Tests/Fixtures/validpattern.xml delete mode 100644 Tests/Fixtures/validresource.xml delete mode 100644 Tests/Fixtures/when-env.xml delete mode 100644 Tests/Fixtures/withdoctype.xml delete mode 100644 Tests/Loader/XmlFileLoaderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d07b9938..e5a1712e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Remove the protected `AttributeClassLoader::$routeAnnotationClass` property and the `setRouteAnnotationClass()` method, use `AttributeClassLoader::setRouteAttributeClass()` instead * Remove class aliases in the `Annotation` namespace, use attributes instead * Remove getters and setters in attribute classes in favor of public properties + * Remove support for the XML configuration format 7.4 --- diff --git a/Loader/XmlFileLoader.php b/Loader/XmlFileLoader.php deleted file mode 100644 index 1ee2f813..00000000 --- a/Loader/XmlFileLoader.php +++ /dev/null @@ -1,470 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Routing\Loader; - -use Symfony\Component\Config\Loader\FileLoader; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Config\Util\XmlUtils; -use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait; -use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait; -use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait; -use Symfony\Component\Routing\RouteCollection; - -/** - * XmlFileLoader loads XML routing files. - * - * @author Fabien Potencier - * @author Tobias Schultze - * - * @deprecated since Symfony 7.4, use another loader instead - */ -class XmlFileLoader extends FileLoader -{ - use HostTrait; - use LocalizedRouteTrait; - use PrefixTrait; - - public const NAMESPACE_URI = 'http://symfony.com/schema/routing'; - public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd'; - - /** - * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be - * parsed because it does not validate against the scheme - */ - public function load(mixed $file, ?string $type = null): RouteCollection - { - trigger_deprecation('symfony/routing', '7.4', 'XML configuration format is deprecated, use YAML, PHP or attributes instead.'); - - $path = $this->locator->locate($file); - - $xml = $this->loadFile($path); - - $collection = new RouteCollection(); - $collection->addResource(new FileResource($path)); - - // process routes and imports - foreach ($xml->documentElement->childNodes as $node) { - if (!$node instanceof \DOMElement) { - continue; - } - - $this->parseNode($collection, $node, $path, $file); - } - - return $collection; - } - - /** - * Parses a node from a loaded XML file. - * - * @throws \InvalidArgumentException When the XML is invalid - */ - protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file): void - { - if (self::NAMESPACE_URI !== $node->namespaceURI) { - return; - } - - switch ($node->localName) { - case 'route': - $this->parseRoute($collection, $node, $path); - break; - case 'import': - $this->parseImport($collection, $node, $path, $file); - break; - case 'when': - if (!$this->env || $node->getAttribute('env') !== $this->env) { - break; - } - foreach ($node->childNodes as $node) { - if ($node instanceof \DOMElement) { - $this->parseNode($collection, $node, $path, $file); - } - } - break; - default: - throw new \InvalidArgumentException(\sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path)); - } - } - - public function supports(mixed $resource, ?string $type = null): bool - { - return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type); - } - - /** - * Parses a route and adds it to the RouteCollection. - * - * @throws \InvalidArgumentException When the XML is invalid - */ - protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path): void - { - if ('' === $id = $node->getAttribute('id')) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must have an "id" attribute.', $path)); - } - - if ('' !== $alias = $node->getAttribute('alias')) { - $alias = $collection->addAlias($id, $alias); - - if ($deprecationInfo = $this->parseDeprecation($node, $path)) { - $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']); - } - - return; - } - - $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY); - $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY); - - [$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path); - - if (!$paths && '' === $node->getAttribute('path')) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must have a "path" attribute or child nodes.', $path)); - } - - if ($paths && '' !== $node->getAttribute('path')) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must not have both a "path" attribute and child nodes.', $path)); - } - - $routes = $this->createLocalizedRoute(new RouteCollection(), $id, $paths ?: $node->getAttribute('path')); - $routes->addDefaults($defaults); - $routes->addRequirements($requirements); - $routes->addOptions($options); - $routes->setSchemes($schemes); - $routes->setMethods($methods); - $routes->setCondition($condition); - - if (null !== $hosts) { - $this->addHost($routes, $hosts); - } - - $collection->addCollection($routes); - } - - /** - * Parses an import and adds the routes in the resource to the RouteCollection. - * - * @throws \InvalidArgumentException When the XML is invalid - */ - protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file): void - { - /** @var \DOMElement $resourceElement */ - if (!($resource = $node->getAttribute('resource') ?: null) && $resourceElement = $node->getElementsByTagName('resource')[0] ?? null) { - $resource = []; - /** @var \DOMAttr $attribute */ - foreach ($resourceElement->attributes as $attribute) { - $resource[$attribute->name] = $attribute->value; - } - } - - if (!$resource) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must have a "resource" attribute or element.', $path)); - } - - $type = $node->getAttribute('type'); - $prefix = $node->getAttribute('prefix'); - $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null; - $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null; - $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true; - $namePrefix = $node->getAttribute('name-prefix') ?: null; - - [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path); - - if ('' !== $prefix && $prefixes) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must not have both a "prefix" attribute and child nodes.', $path)); - } - - $exclude = []; - foreach ($node->childNodes as $child) { - if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) { - $exclude[] = $child->nodeValue; - } - } - - if ($node->hasAttribute('exclude')) { - if ($exclude) { - throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and tags at the same time.'); - } - $exclude = [$node->getAttribute('exclude')]; - } - - $this->setCurrentDir(\dirname($path)); - - /** @var RouteCollection[] $imported */ - $imported = $this->import($resource, '' !== $type ? $type : null, false, $file, $exclude) ?: []; - - if (!\is_array($imported)) { - $imported = [$imported]; - } - - foreach ($imported as $subCollection) { - $this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot); - - if (null !== $hosts) { - $this->addHost($subCollection, $hosts); - } - - if (null !== $condition) { - $subCollection->setCondition($condition); - } - if (null !== $schemes) { - $subCollection->setSchemes($schemes); - } - if (null !== $methods) { - $subCollection->setMethods($methods); - } - if (null !== $namePrefix) { - $subCollection->addNamePrefix($namePrefix); - } - $subCollection->addDefaults($defaults); - $subCollection->addRequirements($requirements); - $subCollection->addOptions($options); - - $collection->addCollection($subCollection); - } - } - - /** - * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors - * or when the XML structure is not as expected by the scheme - - * see validate() - */ - protected function loadFile(string $file): \DOMDocument - { - return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH); - } - - /** - * Parses the config elements (default, requirement, option). - * - * @throws \InvalidArgumentException When the XML is invalid - */ - private function parseConfigs(\DOMElement $node, string $path): array - { - $defaults = []; - $requirements = []; - $options = []; - $condition = null; - $prefixes = []; - $paths = []; - $hosts = []; - - /** @var \DOMElement $n */ - foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) { - if ($node !== $n->parentNode) { - continue; - } - - switch ($n->localName) { - case 'path': - $paths[$n->getAttribute('locale')] = trim($n->textContent); - break; - case 'host': - $hosts[$n->getAttribute('locale')] = trim($n->textContent); - break; - case 'prefix': - $prefixes[$n->getAttribute('locale')] = trim($n->textContent); - break; - case 'default': - if ($this->isElementValueNull($n)) { - $defaults[$n->getAttribute('key')] = null; - } else { - $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path); - } - - break; - case 'requirement': - $requirements[$n->getAttribute('key')] = trim($n->textContent); - break; - case 'option': - $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent)); - break; - case 'condition': - $condition = trim($n->textContent); - break; - case 'resource': - break; - default: - throw new \InvalidArgumentException(\sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path)); - } - } - - if ($controller = $node->getAttribute('controller')) { - if (isset($defaults['_controller'])) { - $name = $node->hasAttribute('id') ? \sprintf('"%s".', $node->getAttribute('id')) : \sprintf('the "%s" tag.', $node->tagName); - - throw new \InvalidArgumentException(\sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ', $path).$name); - } - - $defaults['_controller'] = $controller; - } - if ($node->hasAttribute('locale')) { - $defaults['_locale'] = $node->getAttribute('locale'); - } - if ($node->hasAttribute('format')) { - $defaults['_format'] = $node->getAttribute('format'); - } - if ($node->hasAttribute('utf8')) { - $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8')); - } - if ($stateless = $node->getAttribute('stateless')) { - if (isset($defaults['_stateless'])) { - $name = $node->hasAttribute('id') ? \sprintf('"%s".', $node->getAttribute('id')) : \sprintf('the "%s" tag.', $node->tagName); - - throw new \InvalidArgumentException(\sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name); - } - - $defaults['_stateless'] = XmlUtils::phpize($stateless); - } - - if (!$hosts) { - $hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null; - } - - return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts]; - } - - /** - * Parses the "default" elements. - */ - private function parseDefaultsConfig(\DOMElement $element, string $path): array|bool|float|int|string|null - { - if ($this->isElementValueNull($element)) { - return null; - } - - // Check for existing element nodes in the default element. There can - // only be a single element inside a default element. So this element - // (if one was found) can safely be returned. - foreach ($element->childNodes as $child) { - if (!$child instanceof \DOMElement) { - continue; - } - - if (self::NAMESPACE_URI !== $child->namespaceURI) { - continue; - } - - return $this->parseDefaultNode($child, $path); - } - - // If the default element doesn't contain a nested "bool", "int", "float", - // "string", "list", or "map" element, the element contents will be treated - // as the string value of the associated default option. - return trim($element->textContent); - } - - /** - * Recursively parses the value of a "default" element. - * - * @throws \InvalidArgumentException when the XML is invalid - */ - private function parseDefaultNode(\DOMElement $node, string $path): array|bool|float|int|string|null - { - if ($this->isElementValueNull($node)) { - return null; - } - - switch ($node->localName) { - case 'bool': - return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue); - case 'int': - return (int) trim($node->nodeValue); - case 'float': - return (float) trim($node->nodeValue); - case 'string': - return trim($node->nodeValue); - case 'list': - $list = []; - - foreach ($node->childNodes as $element) { - if (!$element instanceof \DOMElement) { - continue; - } - - if (self::NAMESPACE_URI !== $element->namespaceURI) { - continue; - } - - $list[] = $this->parseDefaultNode($element, $path); - } - - return $list; - case 'map': - $map = []; - - foreach ($node->childNodes as $element) { - if (!$element instanceof \DOMElement) { - continue; - } - - if (self::NAMESPACE_URI !== $element->namespaceURI) { - continue; - } - - $map[$element->getAttribute('key')] = $this->parseDefaultNode($element, $path); - } - - return $map; - default: - throw new \InvalidArgumentException(\sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".', $node->localName, $path)); - } - } - - private function isElementValueNull(\DOMElement $element): bool - { - $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance'; - - if (!$element->hasAttributeNS($namespaceUri, 'nil')) { - return false; - } - - return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil'); - } - - /** - * Parses the deprecation elements. - * - * @throws \InvalidArgumentException When the XML is invalid - */ - private function parseDeprecation(\DOMElement $node, string $path): array - { - $deprecatedNode = null; - foreach ($node->childNodes as $child) { - if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) { - continue; - } - if ('deprecated' !== $child->localName) { - throw new \InvalidArgumentException(\sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path)); - } - - $deprecatedNode = $child; - } - - if (null === $deprecatedNode) { - return []; - } - - if (!$deprecatedNode->hasAttribute('package')) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must have a "package" attribute.', $path)); - } - if (!$deprecatedNode->hasAttribute('version')) { - throw new \InvalidArgumentException(\sprintf('The element in file "%s" must have a "version" attribute.', $path)); - } - - return [ - 'package' => $deprecatedNode->getAttribute('package'), - 'version' => $deprecatedNode->getAttribute('version'), - 'message' => trim($deprecatedNode->nodeValue), - ]; - } -} diff --git a/Tests/Fixtures/CustomXmlFileLoader.php b/Tests/Fixtures/CustomXmlFileLoader.php deleted file mode 100644 index dfb79d1d..00000000 --- a/Tests/Fixtures/CustomXmlFileLoader.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Routing\Tests\Fixtures; - -use Symfony\Component\Config\Util\XmlUtils; -use Symfony\Component\Routing\Loader\XmlFileLoader; - -/** - * XmlFileLoader with schema validation turned off. - */ -class CustomXmlFileLoader extends XmlFileLoader -{ - protected function loadFile(string $file): \DOMDocument - { - return XmlUtils::loadFile($file, fn () => true); - } -} diff --git a/Tests/Fixtures/alias/alias.xml b/Tests/Fixtures/alias/alias.xml deleted file mode 100644 index 70dac391..00000000 --- a/Tests/Fixtures/alias/alias.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - foo %alias_id%. - - - - - diff --git a/Tests/Fixtures/alias/invalid-deprecated-no-package.xml b/Tests/Fixtures/alias/invalid-deprecated-no-package.xml deleted file mode 100644 index ef2bda75..00000000 --- a/Tests/Fixtures/alias/invalid-deprecated-no-package.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/Fixtures/alias/invalid-deprecated-no-version.xml b/Tests/Fixtures/alias/invalid-deprecated-no-version.xml deleted file mode 100644 index 90406b4d..00000000 --- a/Tests/Fixtures/alias/invalid-deprecated-no-version.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/Fixtures/bar.xml b/Tests/Fixtures/bar.xml deleted file mode 100644 index e69de29b..00000000 diff --git a/Tests/Fixtures/class-attributes.xml b/Tests/Fixtures/class-attributes.xml deleted file mode 100644 index 55d1a92f..00000000 --- a/Tests/Fixtures/class-attributes.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/controller/import__controller.xml b/Tests/Fixtures/controller/import__controller.xml deleted file mode 100644 index 5c62914f..00000000 --- a/Tests/Fixtures/controller/import__controller.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - FrameworkBundle:Template:template - - diff --git a/Tests/Fixtures/controller/import_controller.xml b/Tests/Fixtures/controller/import_controller.xml deleted file mode 100644 index 8f52c4ec..00000000 --- a/Tests/Fixtures/controller/import_controller.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/controller/import_override_defaults.xml b/Tests/Fixtures/controller/import_override_defaults.xml deleted file mode 100644 index e092500f..00000000 --- a/Tests/Fixtures/controller/import_override_defaults.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - AppBundle:Blog:index - - diff --git a/Tests/Fixtures/controller/override_defaults.xml b/Tests/Fixtures/controller/override_defaults.xml deleted file mode 100644 index f665ddbb..00000000 --- a/Tests/Fixtures/controller/override_defaults.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - AppBundle:Blog:index - - diff --git a/Tests/Fixtures/controller/routing.xml b/Tests/Fixtures/controller/routing.xml deleted file mode 100644 index f35e3e0a..00000000 --- a/Tests/Fixtures/controller/routing.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - AppBundle:Blog:list - - - - diff --git a/Tests/Fixtures/defaults.xml b/Tests/Fixtures/defaults.xml deleted file mode 100644 index bd30c246..00000000 --- a/Tests/Fixtures/defaults.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/foo.xml b/Tests/Fixtures/foo.xml deleted file mode 100644 index e69de29b..00000000 diff --git a/Tests/Fixtures/foo1.xml b/Tests/Fixtures/foo1.xml deleted file mode 100644 index e69de29b..00000000 diff --git a/Tests/Fixtures/glob/bar.xml b/Tests/Fixtures/glob/bar.xml deleted file mode 100644 index 8a0410c5..00000000 --- a/Tests/Fixtures/glob/bar.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/glob/baz.xml b/Tests/Fixtures/glob/baz.xml deleted file mode 100644 index 4b48a0f3..00000000 --- a/Tests/Fixtures/glob/baz.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/glob/import_multiple.xml b/Tests/Fixtures/glob/import_multiple.xml deleted file mode 100644 index a08e96d6..00000000 --- a/Tests/Fixtures/glob/import_multiple.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/glob/import_single.xml b/Tests/Fixtures/glob/import_single.xml deleted file mode 100644 index 3b26eef1..00000000 --- a/Tests/Fixtures/glob/import_single.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/import_with_name_prefix/routing.xml b/Tests/Fixtures/import_with_name_prefix/routing.xml deleted file mode 100644 index 8699f399..00000000 --- a/Tests/Fixtures/import_with_name_prefix/routing.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/Fixtures/import_with_no_trailing_slash/routing.xml b/Tests/Fixtures/import_with_no_trailing_slash/routing.xml deleted file mode 100644 index 8e768136..00000000 --- a/Tests/Fixtures/import_with_no_trailing_slash/routing.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/Fixtures/imported-with-defaults.xml b/Tests/Fixtures/imported-with-defaults.xml deleted file mode 100644 index 64fd35b7..00000000 --- a/Tests/Fixtures/imported-with-defaults.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - imported - - diff --git a/Tests/Fixtures/importer-with-defaults.xml b/Tests/Fixtures/importer-with-defaults.xml deleted file mode 100644 index f9106904..00000000 --- a/Tests/Fixtures/importer-with-defaults.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/list_defaults.xml b/Tests/Fixtures/list_defaults.xml deleted file mode 100644 index 01be7c18..00000000 --- a/Tests/Fixtures/list_defaults.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - true - 1 - 3.5 - foo - - - - diff --git a/Tests/Fixtures/list_in_list_defaults.xml b/Tests/Fixtures/list_in_list_defaults.xml deleted file mode 100644 index 65baabe3..00000000 --- a/Tests/Fixtures/list_in_list_defaults.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - - true - 1 - 3.5 - foo - - - - - diff --git a/Tests/Fixtures/list_in_map_defaults.xml b/Tests/Fixtures/list_in_map_defaults.xml deleted file mode 100644 index f0d972d1..00000000 --- a/Tests/Fixtures/list_in_map_defaults.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - - true - 1 - 3.5 - foo - - - - - diff --git a/Tests/Fixtures/list_null_values.xml b/Tests/Fixtures/list_null_values.xml deleted file mode 100644 index 45bdd9da..00000000 --- a/Tests/Fixtures/list_null_values.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - - - - - - - - - - diff --git a/Tests/Fixtures/locale_and_host/imported.xml b/Tests/Fixtures/locale_and_host/imported.xml deleted file mode 100644 index 30ff6811..00000000 --- a/Tests/Fixtures/locale_and_host/imported.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - ImportedController::someAction - /voorbeeld - /example - www.custom.nl - www.custom.com - - - ImportedController::someAction - - - ImportedController::someAction - - diff --git a/Tests/Fixtures/locale_and_host/importer-with-host.xml b/Tests/Fixtures/locale_and_host/importer-with-host.xml deleted file mode 100644 index e06136d8..00000000 --- a/Tests/Fixtures/locale_and_host/importer-with-host.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - www.example.nl - www.example.com - - diff --git a/Tests/Fixtures/locale_and_host/importer-with-locale-and-host.xml b/Tests/Fixtures/locale_and_host/importer-with-locale-and-host.xml deleted file mode 100644 index 71904bd2..00000000 --- a/Tests/Fixtures/locale_and_host/importer-with-locale-and-host.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - /nl - /en - www.example.nl - www.example.com - - diff --git a/Tests/Fixtures/locale_and_host/importer-with-single-host.xml b/Tests/Fixtures/locale_and_host/importer-with-single-host.xml deleted file mode 100644 index 121a78b2..00000000 --- a/Tests/Fixtures/locale_and_host/importer-with-single-host.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/locale_and_host/importer-without-host.xml b/Tests/Fixtures/locale_and_host/importer-without-host.xml deleted file mode 100644 index a8fb3d8e..00000000 --- a/Tests/Fixtures/locale_and_host/importer-without-host.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/locale_and_host/route-with-hosts.xml b/Tests/Fixtures/locale_and_host/route-with-hosts.xml deleted file mode 100644 index f4b16e4d..00000000 --- a/Tests/Fixtures/locale_and_host/route-with-hosts.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - www.example.nl - www.example.com - - diff --git a/Tests/Fixtures/localized.xml b/Tests/Fixtures/localized.xml deleted file mode 100644 index d3585c46..00000000 --- a/Tests/Fixtures/localized.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - MyBundle:Blog:show - /path - /route - - - diff --git a/Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml b/Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml deleted file mode 100644 index e618b9ce..00000000 --- a/Tests/Fixtures/localized/imported-with-locale-but-not-localized.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - MyBundle:Blog:show - - diff --git a/Tests/Fixtures/localized/imported-with-locale.xml b/Tests/Fixtures/localized/imported-with-locale.xml deleted file mode 100644 index 3fe52321..00000000 --- a/Tests/Fixtures/localized/imported-with-locale.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - MyBundle:Blog:show - /suffix - /le-suffix - - diff --git a/Tests/Fixtures/localized/imported-with-utf8.xml b/Tests/Fixtures/localized/imported-with-utf8.xml deleted file mode 100644 index 751d5c54..00000000 --- a/Tests/Fixtures/localized/imported-with-utf8.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml b/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml deleted file mode 100644 index 7acbe689..00000000 --- a/Tests/Fixtures/localized/importer-with-locale-imports-non-localized-route.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - /le-prefix - /the-prefix - - diff --git a/Tests/Fixtures/localized/importer-with-locale.xml b/Tests/Fixtures/localized/importer-with-locale.xml deleted file mode 100644 index c3af1098..00000000 --- a/Tests/Fixtures/localized/importer-with-locale.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - /le-prefix - /the-prefix - - diff --git a/Tests/Fixtures/localized/importer-with-utf8.xml b/Tests/Fixtures/localized/importer-with-utf8.xml deleted file mode 100644 index 20f8e38e..00000000 --- a/Tests/Fixtures/localized/importer-with-utf8.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/Tests/Fixtures/localized/utf8.xml b/Tests/Fixtures/localized/utf8.xml deleted file mode 100644 index 95aff20c..00000000 --- a/Tests/Fixtures/localized/utf8.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/Tests/Fixtures/map_defaults.xml b/Tests/Fixtures/map_defaults.xml deleted file mode 100644 index 9f080443..00000000 --- a/Tests/Fixtures/map_defaults.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - true - 1 - 3.5 - foo - - - - diff --git a/Tests/Fixtures/map_in_list_defaults.xml b/Tests/Fixtures/map_in_list_defaults.xml deleted file mode 100644 index 1c8b87df..00000000 --- a/Tests/Fixtures/map_in_list_defaults.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - - true - 1 - 3.5 - foo - - - - - diff --git a/Tests/Fixtures/map_in_map_defaults.xml b/Tests/Fixtures/map_in_map_defaults.xml deleted file mode 100644 index a3af4928..00000000 --- a/Tests/Fixtures/map_in_map_defaults.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - - true - 1 - 3.5 - foo - - - - - diff --git a/Tests/Fixtures/map_null_values.xml b/Tests/Fixtures/map_null_values.xml deleted file mode 100644 index a8153ad1..00000000 --- a/Tests/Fixtures/map_null_values.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - - - - - - - - - - diff --git a/Tests/Fixtures/missing_id.xml b/Tests/Fixtures/missing_id.xml deleted file mode 100644 index fa42e98b..00000000 --- a/Tests/Fixtures/missing_id.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/Tests/Fixtures/missing_path.xml b/Tests/Fixtures/missing_path.xml deleted file mode 100644 index d048adb1..00000000 --- a/Tests/Fixtures/missing_path.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/Tests/Fixtures/namespaceprefix.xml b/Tests/Fixtures/namespaceprefix.xml deleted file mode 100644 index f303464d..00000000 --- a/Tests/Fixtures/namespaceprefix.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - MyBundle:Blog:show - \w+ - en|fr|de - RouteCompiler - - 1 - - - diff --git a/Tests/Fixtures/nonvalid-deprecated-route.xml b/Tests/Fixtures/nonvalid-deprecated-route.xml deleted file mode 100644 index 354685b0..00000000 --- a/Tests/Fixtures/nonvalid-deprecated-route.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/Tests/Fixtures/nonvalid.xml b/Tests/Fixtures/nonvalid.xml deleted file mode 100644 index 07f5f801..00000000 --- a/Tests/Fixtures/nonvalid.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - MyBundle:Blog:show - - diff --git a/Tests/Fixtures/nonvalidnode.xml b/Tests/Fixtures/nonvalidnode.xml deleted file mode 100644 index 71093a4d..00000000 --- a/Tests/Fixtures/nonvalidnode.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - bar - diff --git a/Tests/Fixtures/nonvalidroute.xml b/Tests/Fixtures/nonvalidroute.xml deleted file mode 100644 index f0b84496..00000000 --- a/Tests/Fixtures/nonvalidroute.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - MyBundle:Blog:show - - baz - - diff --git a/Tests/Fixtures/null_values.xml b/Tests/Fixtures/null_values.xml deleted file mode 100644 index 078d49ae..00000000 --- a/Tests/Fixtures/null_values.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - foo - bar - - diff --git a/Tests/Fixtures/psr4-attributes.xml b/Tests/Fixtures/psr4-attributes.xml deleted file mode 100644 index 5f778842..00000000 --- a/Tests/Fixtures/psr4-attributes.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/Fixtures/psr4-controllers-redirection.xml b/Tests/Fixtures/psr4-controllers-redirection.xml deleted file mode 100644 index 1de9a270..00000000 --- a/Tests/Fixtures/psr4-controllers-redirection.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Tests/Fixtures/psr4-controllers-redirection/psr4-attributes.xml b/Tests/Fixtures/psr4-controllers-redirection/psr4-attributes.xml deleted file mode 100644 index 170bb664..00000000 --- a/Tests/Fixtures/psr4-controllers-redirection/psr4-attributes.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/Fixtures/scalar_defaults.xml b/Tests/Fixtures/scalar_defaults.xml deleted file mode 100644 index e3f81cc4..00000000 --- a/Tests/Fixtures/scalar_defaults.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - AcmeBlogBundle:Blog:index - - - - true - - - 1 - - - 3.5 - - - false - - - 1 - - - 0 - - - - - diff --git a/Tests/Fixtures/validpattern.xml b/Tests/Fixtures/validpattern.xml deleted file mode 100644 index 5c6f88ab..00000000 --- a/Tests/Fixtures/validpattern.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - MyBundle:Blog:show - - true - - \w+ - - context.getMethod() == "GET" - - - - diff --git a/Tests/Fixtures/validresource.xml b/Tests/Fixtures/validresource.xml deleted file mode 100644 index 02dfe6be..00000000 --- a/Tests/Fixtures/validresource.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - 123 - \d+ - - context.getMethod() == "POST" - - diff --git a/Tests/Fixtures/when-env.xml b/Tests/Fixtures/when-env.xml deleted file mode 100644 index 50d1fd8b..00000000 --- a/Tests/Fixtures/when-env.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Tests/Fixtures/withdoctype.xml b/Tests/Fixtures/withdoctype.xml deleted file mode 100644 index f217d5bc..00000000 --- a/Tests/Fixtures/withdoctype.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Tests/Loader/XmlFileLoaderTest.php b/Tests/Loader/XmlFileLoaderTest.php deleted file mode 100644 index 5c41009f..00000000 --- a/Tests/Loader/XmlFileLoaderTest.php +++ /dev/null @@ -1,663 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Routing\Tests\Loader; - -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\IgnoreDeprecations; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\Config\Loader\LoaderResolver; -use Symfony\Component\Config\Resource\FileResource; -use Symfony\Component\Routing\Loader\AttributeClassLoader; -use Symfony\Component\Routing\Loader\Psr4DirectoryLoader; -use Symfony\Component\Routing\Loader\XmlFileLoader; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\Tests\Fixtures\CustomXmlFileLoader; -use Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController; - -#[IgnoreDeprecations] -#[Group('legacy')] -class XmlFileLoaderTest extends TestCase -{ - public function testSupports() - { - $loader = new XmlFileLoader($this->createMock(FileLocator::class)); - - $this->assertTrue($loader->supports('foo.xml'), '->supports() returns true if the resource is loadable'); - $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable'); - - $this->assertTrue($loader->supports('foo.xml', 'xml'), '->supports() checks the resource type if specified'); - $this->assertFalse($loader->supports('foo.xml', 'foo'), '->supports() checks the resource type if specified'); - } - - public function testLoadWithRoute() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('validpattern.xml'); - $route = $routeCollection->get('blog_show'); - - $this->assertInstanceOf(Route::class, $route); - $this->assertSame('/blog/{slug}', $route->getPath()); - $this->assertSame('{locale}.example.com', $route->getHost()); - $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller')); - $this->assertSame('\w+', $route->getRequirement('locale')); - $this->assertSame('RouteCompiler', $route->getOption('compiler_class')); - $this->assertEquals(['GET', 'POST', 'PUT', 'OPTIONS'], $route->getMethods()); - $this->assertEquals(['https'], $route->getSchemes()); - $this->assertEquals('context.getMethod() == "GET"', $route->getCondition()); - $this->assertTrue($route->getDefault('_stateless')); - } - - public function testLoadWithNamespacePrefix() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('namespaceprefix.xml'); - - $this->assertCount(1, $routeCollection->all(), 'One route is loaded'); - - $route = $routeCollection->get('blog_show'); - $this->assertSame('/blog/{slug}', $route->getPath()); - $this->assertSame('{_locale}.example.com', $route->getHost()); - $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller')); - $this->assertSame('\w+', $route->getRequirement('slug')); - $this->assertSame('en|fr|de', $route->getRequirement('_locale')); - $this->assertNull($route->getDefault('slug')); - $this->assertSame('RouteCompiler', $route->getOption('compiler_class')); - $this->assertSame(1, $route->getDefault('page')); - } - - public function testLoadWithImport() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('validresource.xml'); - $routes = $routeCollection->all(); - - $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnlyInstancesOf(Route::class, $routes); - - foreach ($routes as $route) { - $this->assertSame('/{foo}/blog/{slug}', $route->getPath()); - $this->assertSame('123', $route->getDefault('foo')); - $this->assertSame('\d+', $route->getRequirement('foo')); - $this->assertSame('bar', $route->getOption('foo')); - $this->assertSame('', $route->getHost()); - $this->assertSame('context.getMethod() == "POST"', $route->getCondition()); - } - } - - public function testLoadingRouteWithDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routes = $loader->load('defaults.xml'); - - $this->assertCount(1, $routes); - - $defaultsRoute = $routes->get('defaults'); - - $this->assertSame('/defaults', $defaultsRoute->getPath()); - $this->assertSame('en', $defaultsRoute->getDefault('_locale')); - $this->assertSame('html', $defaultsRoute->getDefault('_format')); - $this->assertTrue($defaultsRoute->getDefault('_stateless')); - } - - public function testLoadingImportedRoutesWithDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routes = $loader->load('importer-with-defaults.xml'); - - $this->assertCount(2, $routes); - - $expectedRoutes = new RouteCollection(); - $expectedRoutes->add('one', $localeRoute = new Route('/defaults/one')); - $localeRoute->setDefault('_locale', 'g_locale'); - $localeRoute->setDefault('_format', 'g_format'); - $localeRoute->setDefault('_stateless', true); - $expectedRoutes->add('two', $formatRoute = new Route('/defaults/two')); - $formatRoute->setDefault('_locale', 'g_locale'); - $formatRoute->setDefault('_format', 'g_format'); - $formatRoute->setDefault('_stateless', true); - $formatRoute->setDefault('specific', 'imported'); - - $expectedRoutes->addResource(new FileResource(__DIR__.'/../Fixtures/imported-with-defaults.xml')); - $expectedRoutes->addResource(new FileResource(__DIR__.'/../Fixtures/importer-with-defaults.xml')); - - $this->assertEquals($expectedRoutes, $routes); - } - - public function testLoadingUtf8Route() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/localized'])); - $routes = $loader->load('utf8.xml'); - - $this->assertCount(2, $routes); - - $expectedRoutes = new RouteCollection(); - $expectedRoutes->add('app_utf8', $route = new Route('/utf8')); - $route->setOption('utf8', true); - - $expectedRoutes->add('app_no_utf8', $route = new Route('/no-utf8')); - $route->setOption('utf8', false); - - $expectedRoutes->addResource(new FileResource(__DIR__.'/../Fixtures/localized/utf8.xml')); - - $this->assertEquals($expectedRoutes, $routes); - } - - public function testLoadingUtf8ImportedRoutes() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/localized'])); - $routes = $loader->load('importer-with-utf8.xml'); - - $this->assertCount(2, $routes); - - $expectedRoutes = new RouteCollection(); - $expectedRoutes->add('utf8_one', $one = new Route('/one')); - $one->setOption('utf8', true); - - $expectedRoutes->add('utf8_two', $two = new Route('/two')); - $two->setOption('utf8', true); - - $expectedRoutes->addResource(new FileResource(__DIR__.'/../Fixtures/localized/imported-with-utf8.xml')); - $expectedRoutes->addResource(new FileResource(__DIR__.'/../Fixtures/localized/importer-with-utf8.xml')); - - $this->assertEquals($expectedRoutes, $routes); - } - - public function testLoadLocalized() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('localized.xml'); - $routes = $routeCollection->all(); - - $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnlyInstancesOf(Route::class, $routes); - - $this->assertEquals('/route', $routeCollection->get('localized.fr')->getPath()); - $this->assertEquals('/path', $routeCollection->get('localized.en')->getPath()); - } - - public function testLocalizedImports() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/localized'])); - $routeCollection = $loader->load('importer-with-locale.xml'); - $routes = $routeCollection->all(); - - $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnlyInstancesOf(Route::class, $routes); - - $this->assertEquals('/le-prefix/le-suffix', $routeCollection->get('imported.fr')->getPath()); - $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); - - $this->assertEquals('fr', $routeCollection->get('imported.fr')->getRequirement('_locale')); - $this->assertEquals('en', $routeCollection->get('imported.en')->getRequirement('_locale')); - } - - public function testLocalizedImportsOfNotLocalizedRoutes() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/localized'])); - $routeCollection = $loader->load('importer-with-locale-imports-non-localized-route.xml'); - $routes = $routeCollection->all(); - - $this->assertCount(2, $routes, 'Two routes are loaded'); - $this->assertContainsOnlyInstancesOf(Route::class, $routes); - - $this->assertEquals('/le-prefix/suffix', $routeCollection->get('imported.fr')->getPath()); - $this->assertEquals('/the-prefix/suffix', $routeCollection->get('imported.en')->getPath()); - - $this->assertSame('fr', $routeCollection->get('imported.fr')->getRequirement('_locale')); - $this->assertSame('en', $routeCollection->get('imported.en')->getRequirement('_locale')); - } - - #[DataProvider('getPathsToInvalidFiles')] - public function testLoadThrowsExceptionWithInvalidFile($filePath) - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - - $this->expectException(\InvalidArgumentException::class); - - $loader->load($filePath); - } - - #[DataProvider('getPathsToInvalidFiles')] - public function testLoadThrowsExceptionWithInvalidFileEvenWithoutSchemaValidation(string $filePath) - { - $loader = new CustomXmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - - $this->expectException(\InvalidArgumentException::class); - - $loader->load($filePath); - } - - public static function getPathsToInvalidFiles() - { - return [ - ['nonvalidnode.xml'], - ['nonvalidroute.xml'], - ['nonvalid.xml'], - ['missing_id.xml'], - ['missing_path.xml'], - ['nonvalid-deprecated-route.xml'], - ['alias/invalid-deprecated-no-package.xml'], - ['alias/invalid-deprecated-no-version.xml'], - ]; - } - - public function testDocTypeIsNotAllowed() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Document types are not allowed.'); - - $loader->load('withdoctype.xml'); - } - - public function testNullValues() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('null_values.xml'); - $route = $routeCollection->get('blog_show'); - - $this->assertTrue($route->hasDefault('foo')); - $this->assertNull($route->getDefault('foo')); - $this->assertTrue($route->hasDefault('bar')); - $this->assertNull($route->getDefault('bar')); - $this->assertEquals('foo', $route->getDefault('foobar')); - $this->assertEquals('bar', $route->getDefault('baz')); - } - - public function testScalarDataTypeDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('scalar_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'slug' => null, - 'published' => true, - 'page' => 1, - 'price' => 3.5, - 'archived' => false, - 'free' => true, - 'locked' => false, - 'foo' => null, - 'bar' => null, - ], - $route->getDefaults() - ); - } - - public function testListDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('list_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'values' => [true, 1, 3.5, 'foo'], - ], - $route->getDefaults() - ); - } - - public function testListInListDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('list_in_list_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'values' => [[true, 1, 3.5, 'foo']], - ], - $route->getDefaults() - ); - } - - public function testListInMapDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('list_in_map_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'values' => ['list' => [true, 1, 3.5, 'foo']], - ], - $route->getDefaults() - ); - } - - public function testMapDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('map_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'values' => [ - 'public' => true, - 'page' => 1, - 'price' => 3.5, - 'title' => 'foo', - ], - ], - $route->getDefaults() - ); - } - - public function testMapInListDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('map_in_list_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'values' => [[ - 'public' => true, - 'page' => 1, - 'price' => 3.5, - 'title' => 'foo', - ]], - ], - $route->getDefaults() - ); - } - - public function testMapInMapDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('map_in_map_defaults.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - '_controller' => 'AcmeBlogBundle:Blog:index', - 'values' => ['map' => [ - 'public' => true, - 'page' => 1, - 'price' => 3.5, - 'title' => 'foo', - ]], - ], - $route->getDefaults() - ); - } - - public function testNullValuesInList() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('list_null_values.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame([null, null, null, null, null, null], $route->getDefault('list')); - } - - public function testNullValuesInMap() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routeCollection = $loader->load('map_null_values.xml'); - $route = $routeCollection->get('blog'); - - $this->assertSame( - [ - 'boolean' => null, - 'integer' => null, - 'float' => null, - 'string' => null, - 'list' => null, - 'map' => null, - ], - $route->getDefault('map') - ); - } - - public function testLoadRouteWithControllerAttribute() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); - $routeCollection = $loader->load('routing.xml'); - - $route = $routeCollection->get('app_homepage'); - - $this->assertSame('AppBundle:Homepage:show', $route->getDefault('_controller')); - } - - public function testLoadRouteWithoutControllerAttribute() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); - $routeCollection = $loader->load('routing.xml'); - - $route = $routeCollection->get('app_logout'); - - $this->assertNull($route->getDefault('_controller')); - } - - public function testLoadRouteWithControllerSetInDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); - $routeCollection = $loader->load('routing.xml'); - - $route = $routeCollection->get('app_blog'); - - $this->assertSame('AppBundle:Blog:list', $route->getDefault('_controller')); - } - - public function testOverrideControllerInDefaults() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/The routing file "[^"]*" must not specify both the "controller" attribute and the defaults key "_controller" for "app_blog"/'); - - $loader->load('override_defaults.xml'); - } - - #[DataProvider('provideFilesImportingRoutesWithControllers')] - public function testImportRouteWithController(string $file) - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); - $routeCollection = $loader->load($file); - - $route = $routeCollection->get('app_homepage'); - $this->assertSame('FrameworkBundle:Template:template', $route->getDefault('_controller')); - - $route = $routeCollection->get('app_blog'); - $this->assertSame('FrameworkBundle:Template:template', $route->getDefault('_controller')); - - $route = $routeCollection->get('app_logout'); - $this->assertSame('FrameworkBundle:Template:template', $route->getDefault('_controller')); - } - - public static function provideFilesImportingRoutesWithControllers() - { - yield ['import_controller.xml']; - yield ['import__controller.xml']; - } - - public function testImportWithOverriddenController() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/controller'])); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches('/The routing file "[^"]*" must not specify both the "controller" attribute and the defaults key "_controller" for the "import" tag/'); - - $loader->load('import_override_defaults.xml'); - } - - public function testImportRouteWithGlobMatchingSingleFile() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/glob'])); - $routeCollection = $loader->load('import_single.xml'); - - $route = $routeCollection->get('bar_route'); - $this->assertSame('AppBundle:Bar:view', $route->getDefault('_controller')); - } - - public function testImportRouteWithGlobMatchingMultipleFiles() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/glob'])); - $routeCollection = $loader->load('import_multiple.xml'); - - $route = $routeCollection->get('bar_route'); - $this->assertSame('AppBundle:Bar:view', $route->getDefault('_controller')); - - $route = $routeCollection->get('baz_route'); - $this->assertSame('AppBundle:Baz:view', $route->getDefault('_controller')); - } - - public function testImportRouteWithNamePrefix() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/import_with_name_prefix'])); - $routeCollection = $loader->load('routing.xml'); - - $this->assertNotNull($routeCollection->get('app_blog')); - $this->assertEquals('/blog', $routeCollection->get('app_blog')->getPath()); - $this->assertNotNull($routeCollection->get('api_app_blog')); - $this->assertEquals('/api/blog', $routeCollection->get('api_app_blog')->getPath()); - } - - public function testImportRouteWithNoTrailingSlash() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/import_with_no_trailing_slash'])); - $routeCollection = $loader->load('routing.xml'); - - $this->assertEquals('/slash/', $routeCollection->get('a_app_homepage')->getPath()); - $this->assertEquals('/no-slash', $routeCollection->get('b_app_homepage')->getPath()); - } - - public function testImportingRoutesWithHostsInImporter() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/locale_and_host'])); - $routes = $loader->load('importer-with-host.xml'); - - $expectedRoutes = require __DIR__.'/../Fixtures/locale_and_host/import-with-host-expected-collection.php'; - - $this->assertEquals($expectedRoutes('xml'), $routes); - } - - public function testImportingRoutesWithLocalesAndHostInImporter() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/locale_and_host'])); - $routes = $loader->load('importer-with-locale-and-host.xml'); - - $expectedRoutes = require __DIR__.'/../Fixtures/locale_and_host/import-with-locale-and-host-expected-collection.php'; - - $this->assertEquals($expectedRoutes('xml'), $routes); - } - - public function testImportingRoutesWithoutHostsInImporter() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/locale_and_host'])); - $routes = $loader->load('importer-without-host.xml'); - - $expectedRoutes = require __DIR__.'/../Fixtures/locale_and_host/import-without-host-expected-collection.php'; - - $this->assertEquals($expectedRoutes('xml'), $routes); - } - - public function testImportingRoutesWithSingleHostsInImporter() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/locale_and_host'])); - $routes = $loader->load('importer-with-single-host.xml'); - - $expectedRoutes = require __DIR__.'/../Fixtures/locale_and_host/import-with-single-host-expected-collection.php'; - - $this->assertEquals($expectedRoutes('xml'), $routes); - } - - public function testAddingRouteWithHosts() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/locale_and_host'])); - $routes = $loader->load('route-with-hosts.xml'); - - $expectedRoutes = require __DIR__.'/../Fixtures/locale_and_host/route-with-hosts-expected-collection.php'; - - $this->assertEquals($expectedRoutes('xml'), $routes); - } - - public function testWhenEnv() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'some-env'); - $routes = $loader->load('when-env.xml'); - - $this->assertSame(['b', 'a'], array_keys($routes->all())); - $this->assertSame('/b', $routes->get('b')->getPath()); - $this->assertSame('/a1', $routes->get('a')->getPath()); - } - - public function testImportingAliases() - { - $loader = new XmlFileLoader(new FileLocator([__DIR__.'/../Fixtures/alias'])); - $routes = $loader->load('alias.xml'); - - $expectedRoutes = require __DIR__.'/../Fixtures/alias/expected.php'; - - $this->assertEquals($expectedRoutes('xml'), $routes); - } - - #[DataProvider('providePsr4ConfigFiles')] - public function testImportAttributesWithPsr4Prefix(string $configFile) - { - $locator = new FileLocator(\dirname(__DIR__).'/Fixtures'); - new LoaderResolver([ - $loader = new XmlFileLoader($locator), - new Psr4DirectoryLoader($locator), - new class extends AttributeClassLoader { - protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $attr): void - { - $route->setDefault('_controller', $class->getName().'::'.$method->getName()); - } - }, - ]); - - $route = $loader->load($configFile)->get('my_route'); - $this->assertSame('/my-prefix/my/route', $route->getPath()); - $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); - } - - public static function providePsr4ConfigFiles(): array - { - return [ - ['psr4-attributes.xml'], - ['psr4-controllers-redirection.xml'], - ]; - } - - public function testImportAttributesFromClass() - { - new LoaderResolver([ - $loader = new XmlFileLoader(new FileLocator(\dirname(__DIR__).'/Fixtures')), - new class extends AttributeClassLoader { - protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $attr): void - { - $route->setDefault('_controller', $class->getName().'::'.$method->getName()); - } - }, - ]); - - $route = $loader->load('class-attributes.xml')->get('my_route'); - $this->assertSame('/my-prefix/my/route', $route->getPath()); - $this->assertSame(MyController::class.'::__invoke', $route->getDefault('_controller')); - } -} From 8292d3f977dd964683ffe4cc5ba98c361d0e4e7c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 6 Oct 2025 19:57:49 +0200 Subject: [PATCH 33/44] remove ability to access $this and the loader's internal state --- CHANGELOG.md | 1 + Loader/PhpFileLoader.php | 16 ++-------------- Tests/Loader/PhpFileLoaderTest.php | 16 ---------------- 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a1712e..59336de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 8.0 --- + * Remove support for accessing the internal scope of the loader in PHP config files, use only its public API instead * 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 * Remove class aliases in the `Annotation` namespace, use attributes instead diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index 9e005c83..23778e1f 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -45,20 +45,8 @@ public function load(mixed $file, ?string $type = null): RouteCollection return include $file; }, null, null); - try { - if (1 === $result = $load($path)) { - $result = null; - } - } catch (\Error $e) { - $load = \Closure::bind(static function ($file) use ($loader) { - return include $file; - }, null, ProtectedPhpFileLoader::class); - - if (1 === $result = $load($path)) { - $result = null; - } - - trigger_deprecation('symfony/routing', '7.4', 'Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in "%s" on line %d.', $e->getFile(), $e->getLine()); + if (1 === $result = $load($path)) { + $result = null; } if (\is_object($result) && \is_callable($result)) { diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index e033d7b7..e1d51802 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Routing\Tests\Loader; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderResolver; @@ -260,20 +258,6 @@ public function testRoutingConfiguratorCanImportGlobPatterns() $this->assertSame('AppBundle:Baz:view', $route->getDefault('_controller')); } - #[IgnoreDeprecations] - #[Group('legacy')] - public function testTriggersDeprecationWhenAccessingLoaderInternalScope() - { - $locator = new FileLocator([__DIR__.'/../Fixtures']); - $loader = new PhpFileLoader($locator); - - $this->expectUserDeprecationMessageMatches('{^Since symfony/routing 7.4: Accessing the internal scope of the loader in config files is deprecated, use only its public API instead in ".+" on line \d+\.$}'); - - $routes = $loader->load('legacy_internal_scope.php'); - - $this->assertInstanceOf(RouteCollection::class, $routes); - } - public function testRoutingI18nConfigurator() { $locator = new FileLocator([__DIR__.'/../Fixtures']); From fac639d384b97c3f310900b9a2d244ffd91cf38a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 21 Aug 2025 14:47:46 +0200 Subject: [PATCH 34/44] [DependencyInjection][Routing] Define array-shapes to help writing PHP configs using yaml-like arrays --- CHANGELOG.md | 1 + Loader/Config/RoutesConfig.php | 65 ++++++++++++++++++++++++++++++ Loader/PhpFileLoader.php | 45 ++++++++++++--------- Tests/Fixtures/routes_object.php | 13 ++++++ Tests/Loader/PhpFileLoaderTest.php | 9 +++++ composer.json | 5 ++- 6 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 Loader/Config/RoutesConfig.php create mode 100644 Tests/Fixtures/routes_object.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 351680a2..cc2f7ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute * Add argument `$parameters` to `RequestContext`'s constructor * Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files + * Add `RoutesConfig` to help writing PHP configs using yaml-like array-shapes * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters and setters in attribute classes in favor of public properties * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead diff --git a/Loader/Config/RoutesConfig.php b/Loader/Config/RoutesConfig.php new file mode 100644 index 00000000..7d482e36 --- /dev/null +++ b/Loader/Config/RoutesConfig.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Config; + +/** + * @psalm-type Route = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type Import = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type Alias = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type Routes = array + */ +class RoutesConfig +{ + /** + * @param Routes $routes + */ + public function __construct( + public readonly array $routes, + ) { + } +} diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index 9e005c83..5fe3efac 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -13,12 +13,14 @@ use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Exception\InvalidArgumentException; use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator; use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\RouteCollection; +use Symfony\Config\RoutesConfig; /** * PhpFileLoader loads routes from a PHP file. @@ -105,38 +107,45 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ return; } - if (!is_iterable($routes)) { - throw new \InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); + if ($routes instanceof RoutesConfig) { + $routes = [$routes]; + } elseif (!is_iterable($routes)) { + throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); } $loader = new YamlFileLoader($this->locator, $this->env); \Closure::bind(function () use ($collection, $routes, $path, $file) { foreach ($routes as $name => $config) { + $when = $name; if (str_starts_with($name, 'when@')) { if (!$this->env || 'when@'.$this->env !== $name) { continue; } + $when .= '" when "@'.$this->env; + } elseif (!$config instanceof RoutesConfig) { + $config = [$name => $config]; + } elseif (!\is_int($name)) { + throw new InvalidArgumentException(\sprintf('Invalid key "%s" returned for the "%s" config builder; none or "when@%%env%%" expected in file "%s".', $name, get_debug_type($config), $path)); + } - foreach ($config as $name => $config) { - $this->validate($config, $name.'" when "@'.$this->env, $path); - - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); - } - } - - continue; + if ($config instanceof RoutesConfig) { + $config = $config->routes; + } elseif (!is_iterable($config)) { + throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $name, $path)); } - $this->validate($config, $name, $path); + foreach ($config as $name => $config) { + if (str_starts_with($name, 'when@')) { + throw new InvalidArgumentException(\sprintf('A route name cannot start with "when@" in "%s".', $path)); + } + $this->validate($config, $when, $path); - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } } } }, $loader, $loader::class)(); diff --git a/Tests/Fixtures/routes_object.php b/Tests/Fixtures/routes_object.php new file mode 100644 index 00000000..440633e9 --- /dev/null +++ b/Tests/Fixtures/routes_object.php @@ -0,0 +1,13 @@ + [ + 'path' => '/a', + ], + 'b' => [ + 'path' => '/b', + 'methods' => ['GET'], + ], +]); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index e033d7b7..73dae767 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -374,6 +374,15 @@ public function testLoadsArrayRoutes() $this->assertSame(['GET'], $routes->get('b')->getMethods()); } + public function testLoadsObjectRoutes() + { + $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $routes = $loader->load('routes_object.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/b', $routes->get('b')->getPath()); + $this->assertSame(['GET'], $routes->get('b')->getMethods()); + } + public function testWhenEnvWithArray() { $locator = new FileLocator([__DIR__.'/../Fixtures']); diff --git a/composer.json b/composer.json index 1fcc24b6..3ea442bb 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,10 @@ "symfony/yaml": "<6.4" }, "autoload": { - "psr-4": { "Symfony\\Component\\Routing\\": "" }, + "psr-4": { + "Symfony\\Component\\Routing\\": "", + "Symfony\\Config\\": "Loader/Config/" + }, "exclude-from-classmap": [ "/Tests/" ] From 9c041a2a0ee579612b774ceb45cf77ff5ffd6276 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 16 Oct 2025 15:47:23 +0200 Subject: [PATCH 35/44] [DependencyInjection][Routing] Fix nested config imports when returning PHP arrays --- Loader/PhpFileLoader.php | 4 +++- .../importer-php-returns-array-with-import.yml | 3 +++ Tests/Fixtures/importer-php-returns-array.php | 10 ++++++++++ Tests/Loader/PhpFileLoaderTest.php | 11 +++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Tests/Fixtures/importer-php-returns-array-with-import.yml create mode 100644 Tests/Fixtures/importer-php-returns-array.php diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index 5fe3efac..f702ea3c 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Routing\Loader; use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Routing\Exception\InvalidArgumentException; use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; @@ -114,6 +115,7 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ } $loader = new YamlFileLoader($this->locator, $this->env); + $loader->setResolver(new LoaderResolver([$this])); \Closure::bind(function () use ($collection, $routes, $path, $file) { foreach ($routes as $name => $config) { @@ -148,7 +150,7 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ } } } - }, $loader, $loader::class)(); + }, $loader, YamlFileLoader::class)(); } } diff --git a/Tests/Fixtures/importer-php-returns-array-with-import.yml b/Tests/Fixtures/importer-php-returns-array-with-import.yml new file mode 100644 index 00000000..21fbbbbd --- /dev/null +++ b/Tests/Fixtures/importer-php-returns-array-with-import.yml @@ -0,0 +1,3 @@ +routes_from_php: + resource: validpattern.php + type: php diff --git a/Tests/Fixtures/importer-php-returns-array.php b/Tests/Fixtures/importer-php-returns-array.php new file mode 100644 index 00000000..2ca32369 --- /dev/null +++ b/Tests/Fixtures/importer-php-returns-array.php @@ -0,0 +1,10 @@ + [ + 'resource' => 'importer-php-returns-array-with-import.yml', + ], + 'direct' => [ + 'path' => '/direct', + ], +]; diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index 73dae767..07edb728 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -392,6 +392,17 @@ public function testWhenEnvWithArray() $this->assertSame('/x', $routes->get('x')->getPath()); } + public function testYamlImportsAreResolvedWhenProcessingPhpReturnedArrays() + { + $locator = new FileLocator([__DIR__.'/../Fixtures']); + $loader = new PhpFileLoader($locator); + + $routes = $loader->load('importer-php-returns-array.php'); + + $this->assertSame('/blog/{slug}', $routes->get('blog_show')->getPath()); + $this->assertSame('/direct', $routes->get('direct')->getPath()); + } + #[DataProvider('providePsr4ConfigFiles')] public function testImportAttributesWithPsr4Prefix(string $configFile) { From ba77be3a2b5bb2dd6746632d30e6316395613272 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 19 Oct 2025 10:26:25 +0200 Subject: [PATCH 36/44] [Routing] Allow when@env inside `new RoutesConfig()` trees --- Loader/Config/RoutesConfig.php | 2 +- Loader/PhpFileLoader.php | 20 +++--------- Loader/YamlFileLoader.php | 51 ++++++++++++++---------------- Tests/Fixtures/routes_object.php | 10 ++++++ Tests/Loader/PhpFileLoaderTest.php | 14 ++++++++ 5 files changed, 53 insertions(+), 44 deletions(-) diff --git a/Loader/Config/RoutesConfig.php b/Loader/Config/RoutesConfig.php index 7d482e36..2b7b0d47 100644 --- a/Loader/Config/RoutesConfig.php +++ b/Loader/Config/RoutesConfig.php @@ -51,7 +51,7 @@ * alias: string, * deprecated?: array{package:string, version:string, message?:string}, * } - * @psalm-type Routes = array + * @psalm-type Routes = array> */ class RoutesConfig { diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index f702ea3c..2cd8993e 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -109,7 +109,7 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ } if ($routes instanceof RoutesConfig) { - $routes = [$routes]; + $routes = $routes->routes; } elseif (!is_iterable($routes)) { throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); } @@ -119,8 +119,7 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ \Closure::bind(function () use ($collection, $routes, $path, $file) { foreach ($routes as $name => $config) { - $when = $name; - if (str_starts_with($name, 'when@')) { + if (str_starts_with($when = $name, 'when@')) { if (!$this->env || 'when@'.$this->env !== $name) { continue; } @@ -133,22 +132,11 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ if ($config instanceof RoutesConfig) { $config = $config->routes; - } elseif (!is_iterable($config)) { + } elseif (!\is_array($config)) { throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $name, $path)); } - foreach ($config as $name => $config) { - if (str_starts_with($name, 'when@')) { - throw new InvalidArgumentException(\sprintf('A route name cannot start with "when@" in "%s".', $path)); - } - $this->validate($config, $when, $path); - - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); - } - } + $this->loadContent($collection, $config, $path, $file); } }, $loader, YamlFileLoader::class)(); } diff --git a/Loader/YamlFileLoader.php b/Loader/YamlFileLoader.php index d402c8c3..63a96c99 100644 --- a/Loader/YamlFileLoader.php +++ b/Loader/YamlFileLoader.php @@ -74,33 +74,7 @@ public function load(mixed $file, ?string $type = null): RouteCollection throw new \InvalidArgumentException(\sprintf('The file "%s" must contain a YAML array.', $path)); } - foreach ($parsedConfig as $name => $config) { - if (str_starts_with($name, 'when@')) { - if (!$this->env || 'when@'.$this->env !== $name) { - continue; - } - - foreach ($config as $name => $config) { - $this->validate($config, $name.'" when "@'.$this->env, $path); - - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); - } - } - - continue; - } - - $this->validate($config, $name, $path); - - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); - } - } + $this->loadContent($collection, $parsedConfig, $path, $file); return $collection; } @@ -273,6 +247,29 @@ protected function validate(mixed $config, string $name, string $path): void } } + private function loadContent(RouteCollection $collection, array $config, string $path, string $file): void + { + foreach ($config as $name => $config) { + if (!str_starts_with($when = $name, 'when@')) { + $config = [$name => $config]; + } elseif (!$this->env || 'when@'.$this->env !== $name) { + continue; + } else { + $when .= '" when "@'.$this->env; + } + + foreach ($config as $name => $config) { + $this->validate($config, $when, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + } + } + /** * @throws \InvalidArgumentException If one of the provided config keys is not supported, * something is missing or the combination is nonsense diff --git a/Tests/Fixtures/routes_object.php b/Tests/Fixtures/routes_object.php index 440633e9..a8de0d74 100644 --- a/Tests/Fixtures/routes_object.php +++ b/Tests/Fixtures/routes_object.php @@ -10,4 +10,14 @@ 'path' => '/b', 'methods' => ['GET'], ], + 'when@dev' => new RoutesConfig([ + 'c' => [ + 'path' => '/c', + ], + ]), + 'when@test' => [ + 'd' => [ + 'path' => '/d', + ], + ], ]); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index 07edb728..71a6ae8d 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -381,6 +381,20 @@ public function testLoadsObjectRoutes() $this->assertSame('/a', $routes->get('a')->getPath()); $this->assertSame('/b', $routes->get('b')->getPath()); $this->assertSame(['GET'], $routes->get('b')->getMethods()); + $this->assertNull($routes->get('c')); + $this->assertNull($routes->get('d')); + + $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'dev'); + $routes = $loader->load('routes_object.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/b', $routes->get('b')->getPath()); + $this->assertSame('/c', $routes->get('c')->getPath()); + $this->assertNull($routes->get('d')); + + $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'test'); + $routes = $loader->load('routes_object.php'); + $this->assertNull($routes->get('c')); + $this->assertSame('/d', $routes->get('d')->getPath()); } public function testWhenEnvWithArray() From b76bdfd78bbc9261ea4577ffa4bc7384ddb78ea7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Oct 2025 16:41:08 +0200 Subject: [PATCH 37/44] [FrameworkBundle] Auto-generate `config/reference.php` to assist in writing and discovering app's configuration --- CHANGELOG.md | 2 +- .../RoutesReference.php} | 42 ++++++++--- Loader/PhpFileLoader.php | 73 ++++--------------- Tests/Fixtures/array_routes.php | 6 +- Tests/Fixtures/array_when_env.php | 6 +- Tests/Fixtures/routes_object.php | 23 ------ Tests/Loader/PhpFileLoaderTest.php | 23 ------ composer.json | 5 +- 8 files changed, 56 insertions(+), 124 deletions(-) rename Loader/{Config/RoutesConfig.php => Configurator/RoutesReference.php} (55%) delete mode 100644 Tests/Fixtures/routes_object.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2f7ec1..08126cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ CHANGELOG * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute * Add argument `$parameters` to `RequestContext`'s constructor * Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files - * Add `RoutesConfig` to help writing PHP configs using yaml-like array-shapes + * Add `RoutesReference` to help writing PHP configs using yaml-like array-shapes * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters and setters in attribute classes in favor of public properties * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead diff --git a/Loader/Config/RoutesConfig.php b/Loader/Configurator/RoutesReference.php similarity index 55% rename from Loader/Config/RoutesConfig.php rename to Loader/Configurator/RoutesReference.php index 2b7b0d47..3d82d05b 100644 --- a/Loader/Config/RoutesConfig.php +++ b/Loader/Configurator/RoutesReference.php @@ -9,10 +9,30 @@ * file that was distributed with this source code. */ -namespace Symfony\Config; +namespace Symfony\Component\Routing\Loader\Configurator; + +// For the phpdoc to remain compatible with the generation of per-app Routes class, +// this file should have no "use" statements: all symbols referenced by +// the phpdoc need to be in the current namespace or be root-scoped. /** - * @psalm-type Route = array{ + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'attributes', + * 'type' => 'tagged_services', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ * path: string|array, * controller?: string, * methods?: string|list, @@ -27,7 +47,7 @@ * utf8?: bool, * stateless?: bool, * } - * @psalm-type Import = array{ + * @psalm-type ImportConfig = array{ * resource: string, * type?: string, * exclude?: string|list, @@ -47,19 +67,21 @@ * utf8?: bool, * stateless?: bool, * } - * @psalm-type Alias = array{ + * @psalm-type AliasConfig = array{ * alias: string, * deprecated?: array{package:string, version:string, message?:string}, * } - * @psalm-type Routes = array> + * @psalm-type RoutesConfig = array> */ -class RoutesConfig +class RoutesReference { /** - * @param Routes $routes + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig */ - public function __construct( - public readonly array $routes, - ) { + public static function config(array $config): array + { + return $config; } } diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index 2cd8993e..311a2a66 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -15,13 +15,10 @@ use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Routing\Exception\InvalidArgumentException; -use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; -use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; -use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator; -use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; +use Symfony\Component\Routing\Loader\Configurator\Routes; +use Symfony\Component\Routing\Loader\Configurator\RoutesReference; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\RouteCollection; -use Symfony\Config\RoutesConfig; /** * PhpFileLoader loads routes from a PHP file. @@ -42,6 +39,11 @@ public function load(mixed $file, ?string $type = null): RouteCollection $path = $this->locator->locate($file); $this->setCurrentDir(\dirname($path)); + // Expose RoutesReference::config() as Routes::config() + if (!class_exists(Routes::class)) { + class_alias(RoutesReference::class, Routes::class); + } + // the closure forbids access to the private scope in the included file $loader = $this; $load = \Closure::bind(static function ($file) use ($loader) { @@ -66,9 +68,13 @@ public function load(mixed $file, ?string $type = null): RouteCollection if (\is_object($result) && \is_callable($result)) { $collection = $this->callConfigurator($result, $path, $file); - } else { + } elseif (\is_array($result)) { $collection = new RouteCollection(); - $this->loadRoutes($collection, $result, $path, $file); + $loader = new YamlFileLoader($this->locator, $this->env); + $loader->setResolver($this->resolver ?? new LoaderResolver([$this])); + (new \ReflectionMethod(YamlFileLoader::class, 'loadContent'))->invoke($loader, $collection, $result, $path, $file); + } elseif (!($collection = $result) instanceof RouteCollection) { + throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is expected to be a RouteCollection, an array or a configurator callable, but got "%s".', $path, get_debug_type($result))); } $collection->addResource(new FileResource($path)); @@ -85,61 +91,10 @@ protected function callConfigurator(callable $callback, string $path, string $fi { $collection = new RouteCollection(); - $result = $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); - $this->loadRoutes($collection, $result, $path, $file); + $callback(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); return $collection; } - - private function loadRoutes(RouteCollection $collection, mixed $routes, string $path, string $file): void - { - if (null === $routes - || $routes instanceof RouteCollection - || $routes instanceof AliasConfigurator - || $routes instanceof CollectionConfigurator - || $routes instanceof ImportConfigurator - || $routes instanceof RouteConfigurator - || $routes instanceof RoutingConfigurator - ) { - if ($routes instanceof RouteCollection && $collection !== $routes) { - $collection->addCollection($routes); - } - - return; - } - - if ($routes instanceof RoutesConfig) { - $routes = $routes->routes; - } elseif (!is_iterable($routes)) { - throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); - } - - $loader = new YamlFileLoader($this->locator, $this->env); - $loader->setResolver(new LoaderResolver([$this])); - - \Closure::bind(function () use ($collection, $routes, $path, $file) { - foreach ($routes as $name => $config) { - if (str_starts_with($when = $name, 'when@')) { - if (!$this->env || 'when@'.$this->env !== $name) { - continue; - } - $when .= '" when "@'.$this->env; - } elseif (!$config instanceof RoutesConfig) { - $config = [$name => $config]; - } elseif (!\is_int($name)) { - throw new InvalidArgumentException(\sprintf('Invalid key "%s" returned for the "%s" config builder; none or "when@%%env%%" expected in file "%s".', $name, get_debug_type($config), $path)); - } - - if ($config instanceof RoutesConfig) { - $config = $config->routes; - } elseif (!\is_array($config)) { - throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $name, $path)); - } - - $this->loadContent($collection, $config, $path, $file); - } - }, $loader, YamlFileLoader::class)(); - } } /** diff --git a/Tests/Fixtures/array_routes.php b/Tests/Fixtures/array_routes.php index f3cd0b31..3a4af646 100644 --- a/Tests/Fixtures/array_routes.php +++ b/Tests/Fixtures/array_routes.php @@ -1,6 +1,8 @@ ['path' => '/a'], 'b' => ['path' => '/b', 'methods' => ['GET']], -]; +]); diff --git a/Tests/Fixtures/array_when_env.php b/Tests/Fixtures/array_when_env.php index 79422109..e0f20ab5 100644 --- a/Tests/Fixtures/array_when_env.php +++ b/Tests/Fixtures/array_when_env.php @@ -1,8 +1,10 @@ [ 'x' => ['path' => '/x'], ], 'a' => ['path' => '/a'], -]; +]); diff --git a/Tests/Fixtures/routes_object.php b/Tests/Fixtures/routes_object.php deleted file mode 100644 index a8de0d74..00000000 --- a/Tests/Fixtures/routes_object.php +++ /dev/null @@ -1,23 +0,0 @@ - [ - 'path' => '/a', - ], - 'b' => [ - 'path' => '/b', - 'methods' => ['GET'], - ], - 'when@dev' => new RoutesConfig([ - 'c' => [ - 'path' => '/c', - ], - ]), - 'when@test' => [ - 'd' => [ - 'path' => '/d', - ], - ], -]); diff --git a/Tests/Loader/PhpFileLoaderTest.php b/Tests/Loader/PhpFileLoaderTest.php index 71a6ae8d..b28a656d 100644 --- a/Tests/Loader/PhpFileLoaderTest.php +++ b/Tests/Loader/PhpFileLoaderTest.php @@ -374,29 +374,6 @@ public function testLoadsArrayRoutes() $this->assertSame(['GET'], $routes->get('b')->getMethods()); } - public function testLoadsObjectRoutes() - { - $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); - $routes = $loader->load('routes_object.php'); - $this->assertSame('/a', $routes->get('a')->getPath()); - $this->assertSame('/b', $routes->get('b')->getPath()); - $this->assertSame(['GET'], $routes->get('b')->getMethods()); - $this->assertNull($routes->get('c')); - $this->assertNull($routes->get('d')); - - $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'dev'); - $routes = $loader->load('routes_object.php'); - $this->assertSame('/a', $routes->get('a')->getPath()); - $this->assertSame('/b', $routes->get('b')->getPath()); - $this->assertSame('/c', $routes->get('c')->getPath()); - $this->assertNull($routes->get('d')); - - $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures']), 'test'); - $routes = $loader->load('routes_object.php'); - $this->assertNull($routes->get('c')); - $this->assertSame('/d', $routes->get('d')->getPath()); - } - public function testWhenEnvWithArray() { $locator = new FileLocator([__DIR__.'/../Fixtures']); diff --git a/composer.json b/composer.json index 3ea442bb..1fcc24b6 100644 --- a/composer.json +++ b/composer.json @@ -33,10 +33,7 @@ "symfony/yaml": "<6.4" }, "autoload": { - "psr-4": { - "Symfony\\Component\\Routing\\": "", - "Symfony\\Config\\": "Loader/Config/" - }, + "psr-4": { "Symfony\\Component\\Routing\\": "" }, "exclude-from-classmap": [ "/Tests/" ] From 797ed211dd956390708d41d15008dc3df6c0d313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 23 Oct 2025 16:51:50 +0200 Subject: [PATCH 38/44] [Routing] Indicate type of rejected object in CompiledUrlMatcherDumper --- Matcher/Dumper/CompiledUrlMatcherDumper.php | 7 +++---- Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Matcher/Dumper/CompiledUrlMatcherDumper.php b/Matcher/Dumper/CompiledUrlMatcherDumper.php index 4d0b9b6f..4589982f 100644 --- a/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -465,11 +465,10 @@ public static function export(mixed $value): string if (null === $value) { return 'null'; } + if (\is_object($value)) { + throw new \InvalidArgumentException(\sprintf('Symfony\Component\Routing\Route cannot contain objects, but "%s" given.', get_debug_type($value))); + } if (!\is_array($value)) { - if (\is_object($value)) { - throw new \InvalidArgumentException('Symfony\Component\Routing\Route cannot contain objects.'); - } - return str_replace("\n", '\'."\n".\'', var_export($value, true)); } if (!$value) { diff --git a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php index cca2c365..0068d5bb 100644 --- a/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php +++ b/Tests/Matcher/Dumper/CompiledUrlMatcherDumperTest.php @@ -492,7 +492,7 @@ public function testGenerateDumperMatcherWithObject() $routeCollection->add('_', new Route('/', [new \stdClass()])); $dumper = new CompiledUrlMatcherDumper($routeCollection); - $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects'); + $this->expectExceptionMessage('Symfony\Component\Routing\Route cannot contain objects, but "stdClass" given.'); $this->expectException(\InvalidArgumentException::class); $dumper->dump(); From 95fe9d4d6fb2d0f3a227ed978a94aa38601faee5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 20 Oct 2025 11:51:04 +0200 Subject: [PATCH 39/44] [DependencyInjection][Config] Remove support for the fluent PHP config format --- Loader/PhpFileLoader.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Loader/PhpFileLoader.php b/Loader/PhpFileLoader.php index 21750155..46f77a4a 100644 --- a/Loader/PhpFileLoader.php +++ b/Loader/PhpFileLoader.php @@ -84,10 +84,3 @@ protected function callConfigurator(callable $callback, string $path, string $fi return $collection; } } - -/** - * @internal - */ -final class ProtectedPhpFileLoader extends PhpFileLoader -{ -} From 28a70b1a79d19e45337f252149f7a8b13eb4a0fd Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 26 Oct 2025 16:40:20 +0100 Subject: [PATCH 40/44] clean up legacy groups in tests In the past, we needed the legacy group with PHPUnit < 10 to indicate that a test was expected to trigger deprecations. With our update to PHPUnit 12 this is no longer necessary. We now use the group only to skip these tests when running high deps tests with different major versions. The tests from which the legacy group is dropped do not handle deprecated Symfony features but deal with features that users can use to trigger deprecations leveraging our components. They don't have to be skipped in cross-version test runs. --- Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php | 4 ---- Tests/Generator/UrlGeneratorTest.php | 3 --- 2 files changed, 7 deletions(-) diff --git a/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php b/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php index d0e35e81..c461cad1 100644 --- a/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php +++ b/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Routing\Tests\Generator\Dumper; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\Exception\RouteCircularReferenceException; @@ -338,7 +337,6 @@ public function testIndirectCircularReferenceShouldThrowAnException() } #[IgnoreDeprecations] - #[Group('legacy')] public function testDeprecatedAlias() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: The "b" route alias is deprecated. You should stop using it, as it will be removed in the future.'); @@ -355,7 +353,6 @@ public function testDeprecatedAlias() } #[IgnoreDeprecations] - #[Group('legacy')] public function testDeprecatedAliasWithCustomMessage() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -372,7 +369,6 @@ public function testDeprecatedAliasWithCustomMessage() } #[IgnoreDeprecations] - #[Group('legacy')] public function testTargettingADeprecatedAliasShouldTriggerDeprecation() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index b6798cfe..e4c6d0c6 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -803,7 +803,6 @@ public function testAliasWhichTargetRouteDoesntExist() } #[IgnoreDeprecations] - #[Group('legacy')] public function testDeprecatedAlias() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: The "b" route alias is deprecated. You should stop using it, as it will be removed in the future.'); @@ -817,7 +816,6 @@ public function testDeprecatedAlias() } #[IgnoreDeprecations] - #[Group('legacy')] public function testDeprecatedAliasWithCustomMessage() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); @@ -831,7 +829,6 @@ public function testDeprecatedAliasWithCustomMessage() } #[IgnoreDeprecations] - #[Group('legacy')] public function testTargettingADeprecatedAliasShouldTriggerDeprecation() { $this->expectUserDeprecationMessage('Since foo/bar 1.0.0: foo b.'); From 210bbaedaf7fd8414ea2ea9ca598dcfd2e455a12 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 4 Nov 2025 14:31:53 +0100 Subject: [PATCH 41/44] [Routing] Simplify importing routes defined on controller services --- Loader/AttributeServicesLoader.php | 2 +- Loader/Configurator/RoutesReference.php | 3 +-- Tests/Loader/AttributeServicesLoaderTest.php | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Loader/AttributeServicesLoader.php b/Loader/AttributeServicesLoader.php index a3c8f493..b2eea818 100644 --- a/Loader/AttributeServicesLoader.php +++ b/Loader/AttributeServicesLoader.php @@ -42,6 +42,6 @@ public function load(mixed $resource, ?string $type = null): RouteCollection public function supports(mixed $resource, ?string $type = null): bool { - return 'tagged_services' === $type && 'attributes' === $resource; + return 'routing.controllers' === $resource; } } diff --git a/Loader/Configurator/RoutesReference.php b/Loader/Configurator/RoutesReference.php index 3d82d05b..4a48ffc9 100644 --- a/Loader/Configurator/RoutesReference.php +++ b/Loader/Configurator/RoutesReference.php @@ -26,8 +26,7 @@ * * return Routes::config([ * 'controllers' => [ - * 'resource' => 'attributes', - * 'type' => 'tagged_services', + * 'resource' => 'routing.controllers', * ], * ]); * ``` diff --git a/Tests/Loader/AttributeServicesLoaderTest.php b/Tests/Loader/AttributeServicesLoaderTest.php index 31d63ec7..90591b0d 100644 --- a/Tests/Loader/AttributeServicesLoaderTest.php +++ b/Tests/Loader/AttributeServicesLoaderTest.php @@ -26,8 +26,8 @@ public function testSupports() $this->assertFalse($loader->supports('attributes', null)); $this->assertFalse($loader->supports('attributes', 'attribute')); - $this->assertFalse($loader->supports('other', 'tagged_services')); - $this->assertTrue($loader->supports('attributes', 'tagged_services')); + $this->assertFalse($loader->supports('other', 'routing.controllers')); + $this->assertTrue($loader->supports('routing.controllers')); } public function testDelegatesToAttributeLoaderAndMergesCollections() @@ -47,7 +47,7 @@ public function testDelegatesToAttributeLoaderAndMergesCollections() $attributeLoader->setResolver($resolver); $servicesLoader->setResolver($resolver); - $collection = $servicesLoader->load('attributes', 'tagged_services'); + $collection = $servicesLoader->load('routing.controllers'); $this->assertArrayHasKey('action', $collection->all()); $this->assertArrayHasKey('put', $collection->all()); From 127a199021947581b44db102651aa2d999cc5bc0 Mon Sep 17 00:00:00 2001 From: Younes ENNAJI Date: Thu, 13 Nov 2025 17:19:13 +0100 Subject: [PATCH 42/44] [Routing] Align routing.schema.json with YamlFileLoader behavior --- Loader/schema/routing.schema.json | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Loader/schema/routing.schema.json b/Loader/schema/routing.schema.json index 889254b4..f2e2664f 100644 --- a/Loader/schema/routing.schema.json +++ b/Loader/schema/routing.schema.json @@ -62,7 +62,17 @@ "locale": { "type": "string" }, "format": { "type": "string" }, "utf8": { "type": "boolean" }, - "stateless": { "type": "boolean" } + "stateless": { "type": "boolean" }, + "deprecated": { + "type": "object", + "properties": { + "package": { "type": "string" }, + "version": { "type": "string" }, + "message": { "type": "string" } + }, + "required": ["package", "version"], + "additionalProperties": false + } }, "required": ["path"], "additionalProperties": false @@ -70,7 +80,22 @@ "routeImport": { "type": "object", "properties": { - "resource": { "type": "string", "description": "Path to the resource to import." }, + "resource": { + "description": "Path to the resource to import (commonly a string or {path, namespace}), array of paths, or custom value for loaders (additional properties allowed for extensions).", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } }, + { + "type": "object", + "properties": { + "path": { "type": "string", "description": "The directory path to the resource." }, + "namespace": { "type": "string", "description": "The namespace of the controllers in the imported resource (e.g., 'App\\Availability\\UserInterface\\Api')." } + }, + "required": ["path"], + "additionalProperties": true + } + ] + }, "type": { "type": "string", "description": "The type of the resource (e.g., 'attribute', 'annotation', 'yaml')." @@ -78,7 +103,7 @@ "prefix": { "oneOf": [ { "type": "string" }, - { "type": "object", "patternProperties": { "^.+$": { "type": "string" } } } + { "type": "object", "patternProperties": { "^.+$": { "type": "string" } }, "additionalProperties": false } ], "description": "A URL prefix to apply to all routes from the imported resource." }, From b1d727a7ceb01f00f142cae7ee7ddf8ac261f235 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Nov 2025 16:26:00 +0100 Subject: [PATCH 43/44] =?UTF-8?q?[Security]=20Keep=20SymfonyCasts=20as=20b?= =?UTF-8?q?ackers=20of=20the=20Security=20components=20=F0=9F=A4=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 75580363..4f8634a7 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,6 @@ $url = $generator->generate('blog_show', [ Sponsor ------- -The Routing component for Symfony 7.1 is [backed][1] by [redirection.io][2]. - -redirection.io logs all your website’s HTTP traffic, and lets you fix errors -with redirect rules in seconds. Give your marketing, SEO and IT teams the -right tool to manage your website traffic efficiently! - Help Symfony by [sponsoring][3] its development! Resources @@ -61,6 +55,4 @@ Resources [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) -[1]: https://symfony.com/backers -[2]: https://redirection.io [3]: https://symfony.com/sponsor From 9e42e0c30a8a1902e01ccd4cc806e6edfc34909d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 29 Nov 2025 10:41:47 +0100 Subject: [PATCH 44/44] add legacy group to test testing a deprecated feature We need this group in the future to properly skip the test in the high deps job once we start the development of Symfony 8.4. --- Tests/Generator/UrlGeneratorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Generator/UrlGeneratorTest.php b/Tests/Generator/UrlGeneratorTest.php index d7dd9bb6..588632a3 100644 --- a/Tests/Generator/UrlGeneratorTest.php +++ b/Tests/Generator/UrlGeneratorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Routing\Tests\Generator; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface;