From f1888318cee1c13ac0337cbc4eef82df2f738119 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 12 Aug 2026 14:39:02 +0200 Subject: [PATCH 1/2] [Symfony 6.1] Add RouteRequirementStringToConstantRector Replace regex strings in #[Route] requirements with Symfony\Component\Routing\Requirement\Requirement constants. --- config/sets/symfony/composer-based.php | 4 + .../Fixture/skip_unknown_requirement.php.inc | 15 ++ .../Fixture/uuid_requirement.php.inc | 35 ++++ ...eRequirementStringToConstantRectorTest.php | 28 +++ .../config/configured_rule.php | 10 + ...RouteRequirementStringToConstantRector.php | 182 ++++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/skip_unknown_requirement.php.inc create mode 100644 rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/uuid_requirement.php.inc create mode 100644 rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/RouteRequirementStringToConstantRectorTest.php create mode 100644 rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/config/configured_rule.php create mode 100644 rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php diff --git a/config/sets/symfony/composer-based.php b/config/sets/symfony/composer-based.php index 38a85257..618fe921 100644 --- a/config/sets/symfony/composer-based.php +++ b/config/sets/symfony/composer-based.php @@ -89,6 +89,7 @@ use Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector; use Rector\Symfony\Symfony60\Rector\FuncCall\ContainerInterfaceServiceToServiceContainerRector; use Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector; +use Rector\Symfony\Symfony61\Rector\Attribute\RouteRequirementStringToConstantRector; use Rector\Symfony\Symfony61\Rector\Class_\CommandConfigureToAttributeRector; use Rector\Symfony\Symfony61\Rector\Class_\CommandPropertyToAttributeRector; use Rector\Symfony\Symfony61\Rector\Class_\MagicClosureTwigExtensionToNativeMethodsRector; @@ -262,6 +263,9 @@ CommandConfigureToAttributeRector::class, CommandPropertyToAttributeRector::class, + // symfony/routing 6.1 + RouteRequirementStringToConstantRector::class, + // symfony/twig-bridge 6.1 MagicClosureTwigExtensionToNativeMethodsRector::class, diff --git a/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/skip_unknown_requirement.php.inc b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/skip_unknown_requirement.php.inc new file mode 100644 index 00000000..fbca35f1 --- /dev/null +++ b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/skip_unknown_requirement.php.inc @@ -0,0 +1,15 @@ + '[a-z]{3}', + ])] + public function detail() + { + } +} diff --git a/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/uuid_requirement.php.inc b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/uuid_requirement.php.inc new file mode 100644 index 00000000..3c7b7f9c --- /dev/null +++ b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/Fixture/uuid_requirement.php.inc @@ -0,0 +1,35 @@ + '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}', + ])] + public function detail() + { + } +} + +?> +----- + \Symfony\Component\Routing\Requirement\Requirement::UUID_V4, + ])] + public function detail() + { + } +} + +?> diff --git a/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/RouteRequirementStringToConstantRectorTest.php b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/RouteRequirementStringToConstantRectorTest.php new file mode 100644 index 00000000..a5ccbad4 --- /dev/null +++ b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/RouteRequirementStringToConstantRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/config/configured_rule.php b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/config/configured_rule.php new file mode 100644 index 00000000..eb3ccb93 --- /dev/null +++ b/rules-tests/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(RouteRequirementStringToConstantRector::class); +}; diff --git a/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php b/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php new file mode 100644 index 00000000..6b53c963 --- /dev/null +++ b/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php @@ -0,0 +1,182 @@ +=6.1'); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replace regex string in #[Route] requirements with a Requirement constant', + [ + new CodeSample( + <<<'CODE_SAMPLE' +use Symfony\Component\Routing\Attribute\Route; + +final class SomeController +{ + #[Route('/detail/{id}', requirements: [ + 'id' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}', + ])] + public function detail() + { + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Requirement\Requirement; + +final class SomeController +{ + #[Route('/detail/{id}', requirements: [ + 'id' => Requirement::UUID_V4, + ])] + public function detail() + { + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Attribute::class]; + } + + /** + * @param Attribute $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isNames($node->name, [SymfonyAttribute::ROUTE, SymfonyAnnotation::ROUTE])) { + return null; + } + + $requirementsArray = $this->resolveRequirementsArray($node); + if (! $requirementsArray instanceof Array_) { + return null; + } + + $constantNamesByValue = $this->resolveConstantNamesByValue(); + if ($constantNamesByValue === []) { + return null; + } + + $hasChanged = false; + + foreach ($requirementsArray->items as $arrayItem) { + if (! $arrayItem->value instanceof String_) { + continue; + } + + $constantName = $constantNamesByValue[$arrayItem->value->value] ?? null; + if ($constantName === null) { + continue; + } + + $arrayItem->value = $this->nodeFactory->createClassConstFetch(self::REQUIREMENT_CLASS, $constantName); + $hasChanged = true; + } + + if (! $hasChanged) { + return null; + } + + return $node; + } + + private function resolveRequirementsArray(Attribute $attribute): ?Array_ + { + foreach ($attribute->args as $arg) { + if (! $arg instanceof Arg) { + continue; + } + + if (! $arg->name instanceof Node\Identifier) { + continue; + } + + if (! $this->isName($arg->name, 'requirements')) { + continue; + } + + if (! $arg->value instanceof Array_) { + return null; + } + + return $arg->value; + } + + return null; + } + + /** + * @return array + */ + private function resolveConstantNamesByValue(): array + { + if (! $this->reflectionProvider->hasClass(self::REQUIREMENT_CLASS)) { + return []; + } + + $classReflection = $this->reflectionProvider->getClass(self::REQUIREMENT_CLASS); + + $constantNamesByValue = []; + + foreach ($classReflection->getNativeReflection()->getConstants() as $constantName => $constantValue) { + // skip enum cases and non-regex constants + if (! is_string($constantValue)) { + continue; + } + + $constantNamesByValue[$constantValue] = $constantName; + } + + return $constantNamesByValue; + } +} From 3269ddb4a44fa11849d392c0804ca1d7300ced5b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 12 Aug 2026 12:40:04 +0000 Subject: [PATCH 2/2] [rector] Rector fixes --- .../Attribute/RouteRequirementStringToConstantRector.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php b/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php index 6b53c963..e1d8d8d0 100644 --- a/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php +++ b/rules/Symfony61/Rector/Attribute/RouteRequirementStringToConstantRector.php @@ -4,6 +4,7 @@ namespace Rector\Symfony\Symfony61\Rector\Attribute; +use PhpParser\Node\Identifier; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Attribute; @@ -26,10 +27,7 @@ */ final class RouteRequirementStringToConstantRector extends AbstractRector implements ComposerPackageConstraintInterface { - /** - * @var string - */ - private const REQUIREMENT_CLASS = 'Symfony\Component\Routing\Requirement\Requirement'; + private const string REQUIREMENT_CLASS = 'Symfony\Component\Routing\Requirement\Requirement'; public function __construct( private readonly ReflectionProvider $reflectionProvider, @@ -137,7 +135,7 @@ private function resolveRequirementsArray(Attribute $attribute): ?Array_ continue; } - if (! $arg->name instanceof Node\Identifier) { + if (! $arg->name instanceof Identifier) { continue; }