diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fe4b79..df4bdd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,4 +76,4 @@ jobs: - name: Run tests continue-on-error: true - run: vendor/bin/simple-phpunit tests + run: vendor/bin/simple-phpunit diff --git a/.gitignore b/.gitignore index 533803b..0fbf908 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .phpunit.result.cache composer.lock vendor/ -docker-compose.override.yml diff --git a/README.md b/README.md index 90a9b32..761addf 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Version < 1.2 requires zircote/swagger-php 2.x which works with the OpenAPI Spec So tobion/openapi-symfony-routing can be used with both OpenAPI v2 and v3 and composer will select the compatible one for your dependencies. Route loading stays the same between those versions. You just need to update the annotations when migrating from OpenAPI v2 to v3. -Version >= 1.3 requires zircote/swagger-php 4.x which is compatible with the OpenAPI Specification version 3.0.1 and supports attributes. This package need php >= 8.1 for attributes support from zircote/swagger-php. +Version >= 1.3 requires zircote/swagger-php >=4.1 which is compatible with the OpenAPI Specification version 3.0.1 and supports attributes. This package need php >= 8.1 for attributes support from zircote/swagger-php. ## Basic Usage diff --git a/add_composer.sh b/add_composer.sh new file mode 100644 index 0000000..3dc47eb --- /dev/null +++ b/add_composer.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +export COMPOSER_HOME=/tmp/composer + +php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" +php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" +php composer-setup.php +php -r "unlink('composer-setup.php');" +chmod +x composer.phar +mv composer.phar composer diff --git a/composer.json b/composer.json index 4bc5a5b..0fcaf40 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "symfony/finder": "^4.4|^5.0|^6.0", "symfony/framework-bundle": "^4.4|^5.0|^6.0", "symfony/routing": "^4.4|^5.0|^6.0", - "zircote/swagger-php": "^3.0.3|^4.0" + "zircote/swagger-php": "^3.0.3|^4.1" }, "require-dev": { "symfony/phpunit-bridge": "^5.2|^6.0" diff --git a/phpunit.xml b/phpunit.xml index 7c908cb..b069900 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,7 +9,7 @@ > - tests/Annotations + tests/ diff --git a/src/OpenApiRouteLoader.php b/src/OpenApiRouteLoader.php index 62f8ac0..8f057ef 100644 --- a/src/OpenApiRouteLoader.php +++ b/src/OpenApiRouteLoader.php @@ -4,9 +4,6 @@ namespace Tobion\OpenApiSymfonyRouting; -use OpenApi\Analysers\AttributeAnnotationFactory; -use OpenApi\Analysers\DocBlockAnnotationFactory; -use OpenApi\Analysers\ReflectionAnalyser; use OpenApi\Analysis; use OpenApi\Annotations\OpenApi; use OpenApi\Annotations\Operation; @@ -30,6 +27,11 @@ class OpenApiRouteLoader implements RouteLoaderInterface */ private $routeNames = []; + /** + * @var null|Generator + */ + private $generator = null; + /** * @var string */ @@ -89,23 +91,33 @@ private function createOpenApi(): OpenApi return \OpenApi\scan($this->finder); } + if (null !== $this->generator) { + return $this->generator->generate($this>finder); + } + if (method_exists(Analysis::class, 'processors')) { $processors = array_filter(Analysis::processors(), static function ($processor): bool { // remove OperationId processor which would hash the controller starting in 3.2.2 breaking the default route name logic return !$processor instanceof OperationId && !$processor instanceof DocBlockDescriptions; }); - return (new Generator())->setProcessors($processors)->generate($this->finder); + $this->generator = (new Generator())->setProcessors($processors); + + return $this->generator->generate($this->finder); } - $analyser = new ReflectionAnalyser([ - new AttributeAnnotationFactory(), - new DocBlockAnnotationFactory()] - ); + $this->generator = new Generator(); + $processors = $this->generator->getProcessors(); + + foreach ($processors as $key => $processor) { + if ($processor instanceof OperationId) { + unset($processors[$key]); + } + } + + $this->generator->setProcessors($processors); - return (new Generator()) - ->setAnalyser($analyser) - ->generate($this->finder); + return $this->generator->generate($this->finder); } /** @@ -142,8 +154,14 @@ private function createRoute(Operation $operation, string $controller, FormatSuf } if (self::$openApiUndefined !== $operation->parameters) { foreach ($operation->parameters as $parameter) { - if ('path' === $parameter->in && self::$openApiUndefined !== $parameter->schema && self::$openApiUndefined !== $parameter->schema->pattern) { - $route->setRequirement($parameter->name, $parameter->schema->pattern); + if ('path' === $parameter->in && self::$openApiUndefined !== $parameter->schema) { + if (self::$openApiUndefined !== $parameter->schema->pattern) { + $route->setRequirement($parameter->name, $parameter->schema->pattern); + } + + if (self::$openApiUndefined !== $parameter->schema->enum) { + $route->setRequirement($parameter->name, implode('|', $parameter->schema->enum)); + } } } } diff --git a/tests/Annotations/OpenApiRouteLoaderAnnotationsTest.php b/tests/Annotations/OpenApiRouteLoaderAnnotationsTest.php index e75a448..59e2ac1 100644 --- a/tests/Annotations/OpenApiRouteLoaderAnnotationsTest.php +++ b/tests/Annotations/OpenApiRouteLoaderAnnotationsTest.php @@ -20,9 +20,9 @@ use Tobion\OpenApiSymfonyRouting\Tests\Annotations\Fixtures\SeveralHttpMethods\Controller as SeveralHttpMethodsController; use Tobion\OpenApiSymfonyRouting\Tests\Annotations\Fixtures\SeveralRoutesOnOneAction\Controller as SeveralRoutesOnOneActionController; -class OpenApiRouteLoaderAnnotationsTest extends TestCase +final class OpenApiRouteLoaderAnnotationsTest extends TestCase { - private const FIXTURES_ROUTE_NAME_PREFIX = 'tobion_openapisymfonyrouting_tests_fixtures_annotations_'; + private const FIXTURES_ROUTE_NAME_PREFIX = 'tobion_openapisymfonyrouting_tests_annotations_fixtures_'; public function testBasic(): void { @@ -30,14 +30,13 @@ public function testBasic(): void $routes = $routeLoader->__invoke(); - foreach ($routes as $route) { - $this->assertEquals( - $route, - (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', BasicController::class.'::__invoke') - ); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'basic__invoke', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', BasicController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testFormatSuffix(): void @@ -46,24 +45,21 @@ public function testFormatSuffix(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/a.{_format}')) - ->setDefault('_format', null) - ->setMethods('GET') - ->setDefault('_controller', FormatSuffixController::class.'::inheritEnabledFormatSuffix'); - $expectedRoutes[] = (new Route('/b.{_format}')) - ->setDefault('_format', null) - ->setRequirement('_format', 'json|xml') - ->setMethods('GET') - ->setDefault('_controller', FormatSuffixController::class.'::defineFormatPattern'); - $expectedRoutes[] = (new Route('/c')) - ->setMethods('GET') - ->setDefault('_controller', FormatSuffixController::class.'::disableFormatSuffix'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'formatsuffix_inheritenabledformatsuffix', + (new Route('/a.{_format}'))->setDefault('_format', null)->setMethods('GET')->setDefault('_controller', FormatSuffixController::class.'::inheritEnabledFormatSuffix') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'formatsuffix_defineformatpattern', + (new Route('/b.{_format}'))->setDefault('_format', null)->setRequirement('_format', 'json|xml')->setMethods('GET')->setDefault('_controller', FormatSuffixController::class.'::defineFormatPattern') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'formatsuffix_disableformatsuffix', + (new Route('/c'))->setMethods('GET')->setDefault('_controller', FormatSuffixController::class.'::disableFormatSuffix') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testOperationId(): void @@ -87,22 +83,22 @@ public function testPathParameterPattern(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foo/{id}')) - ->setMethods('GET') - ->setDefault('_controller', PathParameterPatternController::class.'::noPattern'); - $expectedRoutes[] = (new Route('/baz/{id}')) - ->setMethods('GET') - ->setDefault('_controller', PathParameterPatternController::class.'::noSchema'); - $expectedRoutes[] = (new Route('/bar/{id}')) - ->setRequirement('id', '^[a-zA-Z0-9]+$') - ->setMethods('GET') - ->setDefault('_controller', PathParameterPatternController::class.'::withPattern'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'pathparameterpattern_nopattern', + (new Route('/foo/{id}'))->setMethods('GET')->setDefault('_controller', PathParameterPatternController::class.'::noPattern') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'pathparameterpattern_noschema', + (new Route('/baz/{id}'))->setMethods('GET')->setDefault('_controller', PathParameterPatternController::class.'::noSchema') + ); + // OpenAPI needs the param pattern to be anchored (^$) to have the desired effect. Symfony automatically trims those to get a valid full path regex. + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'pathparameterpattern_withpattern', + (new Route('/bar/{id}'))->setRequirement('id', '^[a-zA-Z0-9]+$')->setMethods('GET')->setDefault('_controller', PathParameterPatternController::class.'::withPattern') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testPriority(): void @@ -111,21 +107,23 @@ public function testPriority(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes['/bar'] = (new Route('/bar')) - ->setMethods('GET') - ->setDefault('_controller', PriorityController::class.'::bar'); - $expectedRoutes['/foo'] = (new Route('/foo')) - ->setMethods('GET') - ->setDefault('_controller', PriorityController::class.'::foo'); - $expectedRoutes['/{catchall}'] = (new Route('/{catchall}')) - ->setMethods('GET') - ->setDefault('_controller', PriorityController::class.'::catchall'); - - foreach ($routes as $route) { - $this->assertArrayHasKey($route->getPath(), $expectedRoutes); - $this->assertEquals($route, $expectedRoutes[$route->getPath()]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'priority_foo', + (new Route('/foo'))->setMethods('GET')->setDefault('_controller', PriorityController::class.'::foo') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'priority_catchall', + (new Route('/{catchall}'))->setMethods('GET')->setDefault('_controller', PriorityController::class.'::catchall'), + -100 + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'priority_bar', + (new Route('/bar'))->setMethods('GET')->setDefault('_controller', PriorityController::class.'::bar'), + 10 + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralClasses(): void @@ -134,21 +132,21 @@ public function testSeveralClasses(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/bar')) - ->setMethods('GET') - ->setDefault('_controller', BarController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/foo')) - ->setMethods('GET') - ->setDefault('_controller', FooController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/sub')) - ->setMethods('GET') - ->setDefault('_controller', SubController::class.'::__invoke'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_bar__invoke', + (new Route('/bar'))->setMethods('GET')->setDefault('_controller', BarController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_foo__invoke', + (new Route('/foo'))->setMethods('GET')->setDefault('_controller', FooController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_subnamespace_sub__invoke', + (new Route('/sub'))->setMethods('GET')->setDefault('_controller', SubController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralHttpMethods(): void @@ -157,24 +155,25 @@ public function testSeveralHttpMethods(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::get'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('PUT') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::put'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('POST') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::post'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('DELETE') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::delete'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_get', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', SeveralHttpMethodsController::class.'::get') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_put', + (new Route('/foobar'))->setMethods('PUT')->setDefault('_controller', SeveralHttpMethodsController::class.'::put') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_post', + (new Route('/foobar'))->setMethods('POST')->setDefault('_controller', SeveralHttpMethodsController::class.'::post') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_delete', + (new Route('/foobar'))->setMethods('DELETE')->setDefault('_controller', SeveralHttpMethodsController::class.'::delete') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralRoutesOnOneAction(): void @@ -183,44 +182,40 @@ public function testSeveralRoutesOnOneAction(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('POST') - ->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/foo-bar')) - ->setMethods('GET') - ->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalroutesononeaction__invoke', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalroutesononeaction__invoke_1', + (new Route('/foobar'))->setMethods('POST')->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke') + ); + $expectedRoutes->add( + 'my-name', + (new Route('/foo-bar'))->setMethods('GET')->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralDirectories(): void { - $routeLoader = OpenApiRouteLoader::fromDirectories( - __DIR__.'/Fixtures/Basic', - __DIR__.'/Fixtures/SeveralClasses/SubNamespace' - ); + $routeLoader = OpenApiRouteLoader::fromDirectories(__DIR__.'/Fixtures/Basic', __DIR__.'/Fixtures/SeveralClasses/SubNamespace'); $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', BasicController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/sub')) - ->setMethods('GET') - ->setDefault('_controller', SubController::class.'::__invoke'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'basic__invoke', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', BasicController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_subnamespace_sub__invoke', + (new Route('/sub'))->setMethods('GET')->setDefault('_controller', SubController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSrcDirectoryDoesNotExist(): void diff --git a/tests/Attributes/Fixtures/Basic/Controller.php b/tests/Attributes/Fixtures/Basic/Controller.php index d59c712..c72d368 100644 --- a/tests/Attributes/Fixtures/Basic/Controller.php +++ b/tests/Attributes/Fixtures/Basic/Controller.php @@ -4,16 +4,13 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\Basic; -use OpenApi\Attributes as OAT; - -#[OAT\Info(version: "1.0", title: "My API")] -class OpenApiSpec -{} +use OpenApi\Attributes as OA; +#[OA\Info(version: "1.0", title: "My API")] class Controller { - #[OAT\Get(path: "/foobar")] - #[OAT\Response(response: 200, description: "OK")] + #[OA\Get(path: "/foobar")] + #[OA\Response(response: 200, description: "OK")] public function __invoke(): void { } diff --git a/tests/Attributes/Fixtures/FormatSuffix/Controller.php b/tests/Attributes/Fixtures/FormatSuffix/Controller.php index 9a10298..4cd547d 100644 --- a/tests/Attributes/Fixtures/FormatSuffix/Controller.php +++ b/tests/Attributes/Fixtures/FormatSuffix/Controller.php @@ -4,10 +4,10 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\FormatSuffix; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; -#[OAT\OpenApi( - info: new OAT\Info( +#[OA\OpenApi( + info: new OA\Info( title: "My API", version: "1.0" ), @@ -19,20 +19,20 @@ )] class Controller { - #[OAT\Get(path: "/a",)] - #[OAT\Response(response: "200", description: "Success")] + #[OA\Get(path: "/a",)] + #[OA\Response(response: "200", description: "Success")] public function inheritEnabledFormatSuffix(): void { } - #[OAT\Get(path: "/b", x: ["format-suffix" => ["pattern" => "json|xml"]])] - #[OAT\Response(response: "200", description: "Success")] + #[OA\Get(path: "/b", x: ["format-suffix" => ["pattern" => "json|xml"]])] + #[OA\Response(response: "200", description: "Success")] public function defineFormatPattern(): void { } - #[OAT\Get(path: "/c", x: ["format-suffix" => false])] - #[OAT\Response(response: "200", description: "Success")] + #[OA\Get(path: "/c", x: ["format-suffix" => false])] + #[OA\Response(response: "200", description: "Success")] public function disableFormatSuffix(): void { } diff --git a/tests/Attributes/Fixtures/OperationId/Controller.php b/tests/Attributes/Fixtures/OperationId/Controller.php index 223b55a..8fc22ea 100644 --- a/tests/Attributes/Fixtures/OperationId/Controller.php +++ b/tests/Attributes/Fixtures/OperationId/Controller.php @@ -4,13 +4,13 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\OperationId; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; -#[OAT\Info(title: "My API", version: "1.0")] +#[OA\Info(title: "My API", version: "1.0")] class Controller { - #[OAT\Get(path: "/foobar", operationId: "my-name")] - #[OAT\Response(response: "200", description: "Success")] + #[OA\Get(path: "/foobar", operationId: "my-name")] + #[OA\Response(response: "200", description: "Success")] public function __invoke(): void { } diff --git a/tests/Attributes/Fixtures/PathParameterPattern/Controller.php b/tests/Attributes/Fixtures/PathParameterPattern/Controller.php index f62855c..8d74212 100644 --- a/tests/Attributes/Fixtures/PathParameterPattern/Controller.php +++ b/tests/Attributes/Fixtures/PathParameterPattern/Controller.php @@ -4,34 +4,36 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\PathParameterPattern; -use OpenApi\Attributes as OAT; +use mysql_xdevapi\Schema; +use OpenApi\Attributes as OA; -#[OAT\Info(title: "My API", version: "1.0")] +#[OA\Info(title: "My API", version: "1.0")] class Controller { - #[OAT\Get(path: "/foo/{id}")] - #[OAT\Parameter(name:"id", in:"path", required:true, schema: new OAT\Schema(type:"string"))] - #[OAT\Response(response:"200", description:"Success")] - public function noPattern(): void + #[OA\Get(path: "/foo/{id}")] + #[OA\Response(response:"200", description:"Success")] + public function noPattern( + #[OA\Parameter]string $id + ): void { } - #[OAT\Get(path: "/baz/{id}")] - #[OAT\Parameter(name:"id", in:"path", required:true)] - #[OAT\Response(response:"200", description:"Success")] - public function noSchema(): void + #[OA\Get(path: "/baz/{id}")] + #[OA\Response(response:"200", description:"Success")] + public function noSchema( + #[OA\Parameter]string $id + ): void { } - #[OAT\Get(path: "/bar/{id}")] - #[OAT\Parameter( - name:"id", - in:"path", - required:true, - schema: new OAT\Schema(type:"string", pattern:"^[a-zA-Z0-9]+$") - )] - #[OAT\Response(response:"200", description:"Success")] - public function withPattern(): void + #[OA\Get(path: "/bar/{id}/{type}")] + #[OA\Response(response:"200", description:"Success")] + public function withPattern( + #[OA\PathParameter(required: true, schema: new OA\Schema(pattern:"^[a-zA-Z0-9]+$"))] + string $id, + #[OA\PathParameter(required: true, schema: new OA\Schema(enum: ["internal", "external"]))] + string $type + ): void { } } diff --git a/tests/Attributes/Fixtures/Priority/Controller.php b/tests/Attributes/Fixtures/Priority/Controller.php index 36f8f95..8e7d0b6 100644 --- a/tests/Attributes/Fixtures/Priority/Controller.php +++ b/tests/Attributes/Fixtures/Priority/Controller.php @@ -4,25 +4,25 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\Priority; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; -#[OAT\Info(title:"My API", version:"1.0")] +#[OA\Info(title:"My API", version:"1.0")] class Controller { - #[OAT\Get(path:"/foo")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/foo")] + #[OA\Response(response:"200", description:"Success")] public function foo(): void { } - #[OAT\Get(path:"/{catchall}", x: ["priority" => -100])] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/{catchall}", x: ["priority" => -100])] + #[OA\Response(response:"200", description:"Success")] public function catchall(): void { } - #[OAT\Get(path:"/bar", x: ["priority" => 10])] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/bar", x: ["priority" => 10])] + #[OA\Response(response:"200", description:"Success")] public function bar(): void { } diff --git a/tests/Attributes/Fixtures/SeveralClasses/BarController.php b/tests/Attributes/Fixtures/SeveralClasses/BarController.php index 81a6c98..b4f81bd 100644 --- a/tests/Attributes/Fixtures/SeveralClasses/BarController.php +++ b/tests/Attributes/Fixtures/SeveralClasses/BarController.php @@ -4,13 +4,13 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralClasses; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; -#[OAT\Info(title:"My API", version:"1.0")] +#[OA\Info(title:"My API", version:"1.0")] class BarController { - #[OAT\Get(path:"/bar")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/bar")] + #[OA\Response(response:"200", description:"Success")] public function __invoke(): void { } diff --git a/tests/Attributes/Fixtures/SeveralClasses/FooController.php b/tests/Attributes/Fixtures/SeveralClasses/FooController.php index 49ab08f..071433a 100644 --- a/tests/Attributes/Fixtures/SeveralClasses/FooController.php +++ b/tests/Attributes/Fixtures/SeveralClasses/FooController.php @@ -4,12 +4,12 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralClasses; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; class FooController { - #[OAT\Get(path:"/foo")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/foo")] + #[OA\Response(response:"200", description:"Success")] public function __invoke(): void { } diff --git a/tests/Attributes/Fixtures/SeveralClasses/SubNamespace/SubController.php b/tests/Attributes/Fixtures/SeveralClasses/SubNamespace/SubController.php index b03a4c8..2f59c32 100644 --- a/tests/Attributes/Fixtures/SeveralClasses/SubNamespace/SubController.php +++ b/tests/Attributes/Fixtures/SeveralClasses/SubNamespace/SubController.php @@ -4,12 +4,12 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralClasses\SubNamespace; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; class SubController { - #[OAT\Get(path:"/sub")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/sub")] + #[OA\Response(response:"200", description:"Success")] public function __invoke(): void { } diff --git a/tests/Attributes/Fixtures/SeveralHttpMethods/Controller.php b/tests/Attributes/Fixtures/SeveralHttpMethods/Controller.php index 8ba355d..4db068d 100644 --- a/tests/Attributes/Fixtures/SeveralHttpMethods/Controller.php +++ b/tests/Attributes/Fixtures/SeveralHttpMethods/Controller.php @@ -4,31 +4,31 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralHttpMethods; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; -#[OAT\Info(title:"My API", version:"1.0")] +#[OA\Info(title:"My API", version:"1.0")] class Controller { - #[OAT\Get(path:"/foobar")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Get(path:"/foobar")] + #[OA\Response(response:"200", description:"Success")] public function get(): void { } - #[OAT\Put(path:"/foobar")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Put(path:"/foobar")] + #[OA\Response(response:"200", description:"Success")] public function put(): void { } - #[OAT\Post(path:"/foobar")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Post(path:"/foobar")] + #[OA\Response(response:"200", description:"Success")] public function post(): void { } - #[OAT\Delete(path:"/foobar")] - #[OAT\Response(response:"200", description:"Success")] + #[OA\Delete(path:"/foobar")] + #[OA\Response(response:"200", description:"Success")] public function delete(): void { } diff --git a/tests/Attributes/Fixtures/SeveralRoutesOnOneAction/Controller.php b/tests/Attributes/Fixtures/SeveralRoutesOnOneAction/Controller.php index 776c983..e098dae 100644 --- a/tests/Attributes/Fixtures/SeveralRoutesOnOneAction/Controller.php +++ b/tests/Attributes/Fixtures/SeveralRoutesOnOneAction/Controller.php @@ -4,14 +4,14 @@ namespace Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralRoutesOnOneAction; -use OpenApi\Attributes as OAT; +use OpenApi\Attributes as OA; -#[OAT\Info(title:"My API", version:"1.0")] +#[OA\Info(title:"My API", version:"1.0")] class Controller { - #[OAT\Get(path:"/foobar", responses: [new OAT\Response(response:"200", description:"Success")])] - #[OAT\Post(path:"/foobar", responses: [new OAT\Response(response:"200", description:"Success")])] - #[OAT\Get(path:"/foo-bar", operationId: "my-name", responses: [new OAT\Response(response:"200", description:"Success")])] + #[OA\Get(path:"/foobar", responses: [new OA\Response(response:"200", description:"Success")])] + #[OA\Post(path:"/foobar", responses: [new OA\Response(response:"200", description:"Success")])] + #[OA\Get(path:"/foo-bar", operationId: "my-name", responses: [new OA\Response(response:"200", description:"Success")])] public function __invoke(): void { } diff --git a/tests/Attributes/OpenApiRouteLoaderAttributesTest.php b/tests/Attributes/OpenApiRouteLoaderAttributesTest.php index 9e55d1e..d79ab2e 100644 --- a/tests/Attributes/OpenApiRouteLoaderAttributesTest.php +++ b/tests/Attributes/OpenApiRouteLoaderAttributesTest.php @@ -20,22 +20,26 @@ use Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralHttpMethods\Controller as SeveralHttpMethodsController; use Tobion\OpenApiSymfonyRouting\Tests\Attributes\Fixtures\SeveralRoutesOnOneAction\Controller as SeveralRoutesOnOneActionController; -class OpenApiRouteLoaderAttributesTest extends TestCase +/** + * @requires PHP >= 8.1 + */ +final class OpenApiRouteLoaderAttributesTest extends TestCase { + private const FIXTURES_ROUTE_NAME_PREFIX = 'tobion_openapisymfonyrouting_tests_attributes_fixtures_'; + public function testBasic(): void { $routeLoader = OpenApiRouteLoader::fromDirectories(__DIR__.'/Fixtures/Basic'); $routes = $routeLoader->__invoke(); - foreach ($routes as $route) { - $this->assertEquals( - $route, - (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', BasicController::class.'::__invoke') - ); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'basic__invoke', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', BasicController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testFormatSuffix(): void @@ -44,24 +48,21 @@ public function testFormatSuffix(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/a.{_format}')) - ->setDefault('_format', null) - ->setMethods('GET') - ->setDefault('_controller', FormatSuffixController::class.'::inheritEnabledFormatSuffix'); - $expectedRoutes[] = (new Route('/b.{_format}')) - ->setDefault('_format', null) - ->setRequirement('_format', 'json|xml') - ->setMethods('GET') - ->setDefault('_controller', FormatSuffixController::class.'::defineFormatPattern'); - $expectedRoutes[] = (new Route('/c')) - ->setMethods('GET') - ->setDefault('_controller', FormatSuffixController::class.'::disableFormatSuffix'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'formatsuffix_inheritenabledformatsuffix', + (new Route('/a.{_format}'))->setDefault('_format', null)->setMethods('GET')->setDefault('_controller', FormatSuffixController::class.'::inheritEnabledFormatSuffix') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'formatsuffix_defineformatpattern', + (new Route('/b.{_format}'))->setDefault('_format', null)->setRequirement('_format', 'json|xml')->setMethods('GET')->setDefault('_controller', FormatSuffixController::class.'::defineFormatPattern') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'formatsuffix_disableformatsuffix', + (new Route('/c'))->setMethods('GET')->setDefault('_controller', FormatSuffixController::class.'::disableFormatSuffix') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testOperationId(): void @@ -85,22 +86,26 @@ public function testPathParameterPattern(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foo/{id}')) - ->setMethods('GET') - ->setDefault('_controller', PathParameterPatternController::class.'::noPattern'); - $expectedRoutes[] = (new Route('/baz/{id}')) - ->setMethods('GET') - ->setDefault('_controller', PathParameterPatternController::class.'::noSchema'); - $expectedRoutes[] = (new Route('/bar/{id}')) - ->setRequirement('id', '^[a-zA-Z0-9]+$') - ->setMethods('GET') - ->setDefault('_controller', PathParameterPatternController::class.'::withPattern'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'pathparameterpattern_nopattern', + (new Route('/foo/{id}'))->setMethods('GET')->setDefault('_controller', PathParameterPatternController::class.'::noPattern') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'pathparameterpattern_noschema', + (new Route('/baz/{id}'))->setMethods('GET')->setDefault('_controller', PathParameterPatternController::class.'::noSchema') + ); + + // OpenAPI needs the param pattern to be anchored (^$) to have the desired effect. Symfony automatically trims those to get a valid full path regex. + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'pathparameterpattern_withpattern', + (new Route('/bar/{id}/{type}')) + ->setRequirement('id', '^[a-zA-Z0-9]+$') + ->setRequirement('type', 'internal|external') + ->setMethods('GET')->setDefault('_controller', PathParameterPatternController::class.'::withPattern') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testPriority(): void @@ -109,21 +114,23 @@ public function testPriority(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/bar')) - ->setMethods('GET') - ->setDefault('_controller', PriorityController::class.'::bar'); - $expectedRoutes[] = (new Route('/foo')) - ->setMethods('GET') - ->setDefault('_controller', PriorityController::class.'::foo'); - $expectedRoutes[] = (new Route('/{catchall}')) - ->setMethods('GET') - ->setDefault('_controller', PriorityController::class.'::catchall'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'priority_foo', + (new Route('/foo'))->setMethods('GET')->setDefault('_controller', PriorityController::class.'::foo') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'priority_catchall', + (new Route('/{catchall}'))->setMethods('GET')->setDefault('_controller', PriorityController::class.'::catchall'), + -100 + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'priority_bar', + (new Route('/bar'))->setMethods('GET')->setDefault('_controller', PriorityController::class.'::bar'), + 10 + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralClasses(): void @@ -132,21 +139,21 @@ public function testSeveralClasses(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/bar')) - ->setMethods('GET') - ->setDefault('_controller', BarController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/foo')) - ->setMethods('GET') - ->setDefault('_controller', FooController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/sub')) - ->setMethods('GET') - ->setDefault('_controller', SubController::class.'::__invoke'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_bar__invoke', + (new Route('/bar'))->setMethods('GET')->setDefault('_controller', BarController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_foo__invoke', + (new Route('/foo'))->setMethods('GET')->setDefault('_controller', FooController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_subnamespace_sub__invoke', + (new Route('/sub'))->setMethods('GET')->setDefault('_controller', SubController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralHttpMethods(): void @@ -155,24 +162,25 @@ public function testSeveralHttpMethods(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::get'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('PUT') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::put'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('POST') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::post'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('DELETE') - ->setDefault('_controller', SeveralHttpMethodsController::class.'::delete'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_get', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', SeveralHttpMethodsController::class.'::get') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_put', + (new Route('/foobar'))->setMethods('PUT')->setDefault('_controller', SeveralHttpMethodsController::class.'::put') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_post', + (new Route('/foobar'))->setMethods('POST')->setDefault('_controller', SeveralHttpMethodsController::class.'::post') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalhttpmethods_delete', + (new Route('/foobar'))->setMethods('DELETE')->setDefault('_controller', SeveralHttpMethodsController::class.'::delete') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralRoutesOnOneAction(): void @@ -181,44 +189,40 @@ public function testSeveralRoutesOnOneAction(): void $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('POST') - ->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/foo-bar')) - ->setMethods('GET') - ->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalroutesononeaction__invoke', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalroutesononeaction__invoke_1', + (new Route('/foobar'))->setMethods('POST')->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke') + ); + $expectedRoutes->add( + 'my-name', + (new Route('/foo-bar'))->setMethods('GET')->setDefault('_controller', SeveralRoutesOnOneActionController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSeveralDirectories(): void { - $routeLoader = OpenApiRouteLoader::fromDirectories( - __DIR__.'/Fixtures/Basic', - __DIR__.'/Fixtures/SeveralClasses/SubNamespace' - ); + $routeLoader = OpenApiRouteLoader::fromDirectories(__DIR__.'/Fixtures/Basic', __DIR__.'/Fixtures/SeveralClasses/SubNamespace'); $routes = $routeLoader->__invoke(); - $expectedRoutes = []; - $expectedRoutes[] = (new Route('/foobar')) - ->setMethods('GET') - ->setDefault('_controller', BasicController::class.'::__invoke'); - $expectedRoutes[] = (new Route('/sub')) - ->setMethods('GET') - ->setDefault('_controller', SubController::class.'::__invoke'); - - $index = 0; - foreach ($routes as $route) { - $this->assertEquals($route, $expectedRoutes[$index++]); - } + $expectedRoutes = new RouteCollection(); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'basic__invoke', + (new Route('/foobar'))->setMethods('GET')->setDefault('_controller', BasicController::class.'::__invoke') + ); + $expectedRoutes->add( + self::FIXTURES_ROUTE_NAME_PREFIX.'severalclasses_subnamespace_sub__invoke', + (new Route('/sub'))->setMethods('GET')->setDefault('_controller', SubController::class.'::__invoke') + ); + + self::assertEquals($expectedRoutes, $routes); } public function testSrcDirectoryDoesNotExist(): void