diff --git a/CHANGELOG.md b/CHANGELOG.md index a4fc79751..40dc68c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 4.36.2 +- Support of attribute MapQueryParameter with a regexp has been improved, it now converts the regexp from PCRE to ECMA-262 for better compliance with OpenApi. + ## 4.36.1 - Passing an array key `value` with a list of strings to the `Areas` annotation/attribute is deprecated. Pass the list of strings directly. ```diff diff --git a/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php b/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php index 6cc5ea9f8..130f94ff8 100644 --- a/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php +++ b/src/RouteDescriber/RouteArgumentDescriber/SymfonyMapQueryParameterDescriber.php @@ -128,7 +128,7 @@ private function describeValidateFilter(?int $filter, int $flags, array $options } if (FILTER_VALIDATE_REGEXP === $filter) { - return ['type' => 'string', 'pattern' => $options['regexp']]; + return ['type' => 'string', 'pattern' => $this->getEcmaRegexpFromPCRE($options['regexp'])]; } if (FILTER_VALIDATE_URL === $filter) { @@ -141,4 +141,35 @@ private function describeValidateFilter(?int $filter, int $flags, array $options return []; } + + private function getEcmaRegexpFromPCRE(string $pcreRegex): string + { + // Check if PCRE regex has delimiters + if (!preg_match('/^(.)(.*)\1([a-zA-Z]*)$/s', $pcreRegex, $matches)) { + throw new \InvalidArgumentException('Invalid PCRE regex format. Missing delimiters.'); + } + + [$fullMatch, $delimiter, $pattern, $flags] = $matches; + + // Check for unsupported PCRE specific constructs + $unsupportedFeatures = [ + '\A', // Start of string (use ^ in JavaScript) + '\z', // End of string (use $ in JavaScript) + '\Z', // End of string before newline (not supported in JavaScript) + '\R', // Any Unicode newline sequence (not supported in JavaScript) + '\K', // Resets the start of the current match (not supported in JavaScript) + ]; + + foreach ($unsupportedFeatures as $feature) { + if (str_contains($pattern, $feature)) { + throw new \InvalidArgumentException("Unsupported PCRE feature found: {$feature}"); + } + } + + // Remove escaped delimiters in the pattern + $pattern = str_replace('\\'.$delimiter, $delimiter, $pattern); + + // Return only the pattern (without flags or delimiters) + return $pattern; + } } diff --git a/tests/Functional/Controller/MapQueryParameterController.php b/tests/Functional/Controller/MapQueryParameterController.php index 4cc128c8c..a09385a0d 100644 --- a/tests/Functional/Controller/MapQueryParameterController.php +++ b/tests/Functional/Controller/MapQueryParameterController.php @@ -11,6 +11,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; +use OpenApi\Attributes as OA; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Annotation\Route; diff --git a/tests/Functional/Controller/MapQueryParameterWithInvalidPCREController.php b/tests/Functional/Controller/MapQueryParameterWithInvalidPCREController.php new file mode 100644 index 000000000..1334f9389 --- /dev/null +++ b/tests/Functional/Controller/MapQueryParameterWithInvalidPCREController.php @@ -0,0 +1,27 @@ + 'This is not a valid regexp'])] + string $regexp, + ) { + } +} diff --git a/tests/Functional/Controller/MapQueryParameterWithUnsupportedFlagInPCREController.php b/tests/Functional/Controller/MapQueryParameterWithUnsupportedFlagInPCREController.php new file mode 100644 index 000000000..77ded159a --- /dev/null +++ b/tests/Functional/Controller/MapQueryParameterWithUnsupportedFlagInPCREController.php @@ -0,0 +1,27 @@ + 'This is not a valid regexp'])] + string $regexp, + ) { + } +} diff --git a/tests/Functional/ControllerTest.php b/tests/Functional/ControllerTest.php index f4b1189d0..4b4f96c14 100644 --- a/tests/Functional/ControllerTest.php +++ b/tests/Functional/ControllerTest.php @@ -77,6 +77,35 @@ public function testControllers(?array $controller, ?string $fixtureName = null, ); } + /** + * @dataProvider provideExceptionsTestCases + * + * @param array{name: string, type: string}|null $controller + * @param Bundle[] $extraBundles + * @param string[] $extraConfigs + */ + public function testControllersThrowingExceptions(?array $controller, array $extraBundles = [], array $extraConfigs = []): void + { + if (version_compare(Kernel::VERSION, '6.3.0', '<')) { + $this->markTestSkipped(); + } + $controllerName = $controller['name'] ?? null; + $controllerType = $controller['type'] ?? null; + + $routingConfiguration = function (RoutingConfigurator $routes) use ($controllerName, $controllerType) { + if (null === $controllerName) { + return; + } + + $routes->withPath('/')->import(__DIR__."/Controller/$controllerName.php", $controllerType); + }; + + $this->configurableContainerFactory->create($extraBundles, $routingConfiguration, $extraConfigs); + + $this->expectException(\InvalidArgumentException::class); + $this->getOpenApiDefinition(); + } + public static function provideAttributeTestCases(): \Generator { if (PHP_VERSION_ID < 80100) { @@ -212,6 +241,24 @@ public static function provideUniversalTestCases(): \Generator ]; } + public static function provideExceptionsTestCases(): \Generator + { + $type = 'attribute'; + + yield 'Symfony 6.3 MapQueryParameter attribute with invalid PCRE' => [ + [ + 'name' => 'MapQueryParameterWithInvalidPCREController', + 'type' => $type, + ], + ]; + yield 'Symfony 6.3 MapQueryParameter attribute with unsupported flag in PCRE' => [ + [ + 'name' => 'MapQueryParameterWithUnsupportedFlagInPCREController', + 'type' => $type, + ], + ]; + } + private static function getFixture(string $fixture): string { if (!file_exists($fixture)) { diff --git a/tests/Functional/Fixtures/MapQueryParameterController.json b/tests/Functional/Fixtures/MapQueryParameterController.json index 9d626a46a..a3c133f2a 100644 --- a/tests/Functional/Fixtures/MapQueryParameterController.json +++ b/tests/Functional/Fixtures/MapQueryParameterController.json @@ -53,7 +53,7 @@ } ], "responses": { - "default": { + "200": { "description": "" } } @@ -133,7 +133,7 @@ "required": true, "schema": { "type": "string", - "pattern": "/^test/" + "pattern": "^test" } }, { @@ -147,7 +147,7 @@ } ], "responses": { - "default": { + "200": { "description": "" } } @@ -168,7 +168,7 @@ } ], "responses": { - "default": { + "200": { "description": "" } } @@ -189,7 +189,7 @@ } ], "responses": { - "default": { + "200": { "description": "" } } @@ -202,24 +202,28 @@ { "name": "id", "in": "query", + "description": "Query parameter id description", "required": false, "schema": { "type": "integer", "nullable": true - } + }, + "example": 123 }, { "name": "changedType", "in": "query", + "description": "Incorrectly described query parameter", "required": false, "schema": { - "type": "string", - "nullable": true - } + "type": "int", + "nullable": false + }, + "example": 123 } ], "responses": { - "default": { + "200": { "description": "" } }