diff --git a/composer.json b/composer.json index 7e15a08..a38b775 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "jms/serializer": "^3.28", "jms/serializer-bundle": "^5.3", "knplabs/doctrine-behaviors": "^2.6", - "nelmio/api-doc-bundle": "^4.12", + "nelmio/api-doc-bundle": "^4.16", "nelmio/cors-bundle": "^2.3", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.24", diff --git a/composer.lock b/composer.lock index 1c3c117..a08e9b6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6c528f9a772818eea2287af006056c05", + "content-hash": "d1c8d1bae41718751b0184995e6e5ce0", "packages": [ { "name": "brick/math", @@ -2114,64 +2114,64 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v4.12.0", + "version": "v4.16.2", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "b9fc542143a4c38dc1a302b798a1caeb7d33484f" + "reference": "31da761b6c9d275fb3bbee87c4c6888b17aec4ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/b9fc542143a4c38dc1a302b798a1caeb7d33484f", - "reference": "b9fc542143a4c38dc1a302b798a1caeb7d33484f", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/31da761b6c9d275fb3bbee87c4c6888b17aec4ad", + "reference": "31da761b6c9d275fb3bbee87c4c6888b17aec4ad", "shasum": "" }, "require": { - "doctrine/annotations": "^2.0", "ext-json": "*", "php": ">=7.2", "phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0", "psr/cache": "^1.0|^2.0|^3.0", "psr/container": "^1.0|^2.0", "psr/log": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/framework-bundle": "^5.4.24|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/options-resolver": "^5.4|^6.0", - "symfony/property-info": "^5.4|^6.0", - "symfony/routing": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4.24|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", "zircote/swagger-php": "^4.2.15" }, - "conflict": { - "symfony/framework-bundle": "4.2.7" - }, "require-dev": { - "api-platform/core": "^2.7.0|^3@dev", + "api-platform/core": "^2.7.0|^3", "composer/package-versions-deprecated": "1.11.99.1", + "doctrine/annotations": "^2.0", "friendsofsymfony/rest-bundle": "^2.8|^3.0", "jms/serializer": "^1.14|^3.0", - "jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0@beta", + "jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0", + "phpunit/phpunit": "^8.5|^9.6", "sensio/framework-extra-bundle": "^5.4|^6.0", - "symfony/asset": "^5.4|^6.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/cache": "^5.4|^6.0", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/cache": "^5.4|^6.0|^7.0", "symfony/deprecation-contracts": "^2.1|^3", - "symfony/dom-crawler": "^5.4|^6.0", - "symfony/form": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.4.23", - "symfony/property-access": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/templating": "^5.4|^6.0", - "symfony/twig-bundle": "^5.4|^6.0", - "symfony/validator": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^6.4", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/templating": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/validator": "^5.4|^6.0|^7.0", "willdurand/hateoas-bundle": "^1.0|^2.0" }, "suggest": { "api-platform/core": "For using an API oriented framework.", + "doctrine/annotations": "For using doctrine annotations", "friendsofsymfony/rest-bundle": "For using the parameters annotations.", "jms/serializer-bundle": "For describing your models.", "symfony/asset": "For using the Swagger UI.", @@ -2220,9 +2220,9 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.12.0" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.16.2" }, - "time": "2023-06-16T10:39:21+00:00" + "time": "2024-01-06T21:33:48+00:00" }, { "name": "nelmio/cors-bundle", @@ -10910,5 +10910,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/config/services.yaml b/config/services.yaml index e9ba963..80eb059 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -27,3 +27,6 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + App\OpenApi\SchemaQueryParameter: + tags: ['nelmio_api_doc.swagger.processor'] diff --git a/src/Controller/Api/ThemesController.php b/src/Controller/Api/ThemesController.php index 25b9d3c..4ce34f9 100644 --- a/src/Controller/Api/ThemesController.php +++ b/src/Controller/Api/ThemesController.php @@ -40,56 +40,16 @@ public function __construct( */ #[REST\Get('/', name: 'api_themes_get')] #[REST\View(serializerGroups: ['read'])] - #[OA\Parameter( - in: 'query', - name: 'search', - description: 'Search for themes.', - example: 'Twenty', - schema: new OA\Schema( - type: 'string', - minLength: 3, - maxLength: 128, - ), - )] - #[OA\Parameter( - in: 'query', - name: 'page', - description: 'The page number.', - example: 1, - schema: new OA\Schema( - type: 'integer', - minimum: 1, - ), - )] - #[OA\Parameter( - in: 'query', - name: 'per_page', - description: 'The number of items per page.', - example: 10, - schema: new OA\Schema( - type: 'integer', - minimum: 1, - maximum: 100, - ), - )] - #[OA\Parameter( - in: 'query', - name: 'order', - description: 'The order of the items.', - example: 'ASC', - schema: new OA\Schema( - type: 'string', - enum: ['ASC', 'DESC'], - ), - )] - #[OA\Parameter( - in: 'query', - name: 'order_by', - description: 'The field to order the items by.', - example: 'name', - schema: new OA\Schema( - type: 'string', - ) + #[OA\Get( + x: [ + 'query-args-explode' => new OA\Schema( + type: 'string', + ref: new Model( + type: ThemeFilter::class, + groups: ['read'], + ), + ), + ] )] #[OA\Response( response: 200, diff --git a/src/ControllerFilter/Traits/OrderFilterTrait.php b/src/ControllerFilter/Traits/OrderFilterTrait.php index a39d2aa..8ae32af 100644 --- a/src/ControllerFilter/Traits/OrderFilterTrait.php +++ b/src/ControllerFilter/Traits/OrderFilterTrait.php @@ -17,8 +17,9 @@ trait OrderFilterTrait * @var string */ #[Serializer\Type('string')] + #[Serializer\Groups(["read"])] #[OA\Property(type: 'string', description: 'The field to order by.', example: 'id')] - private string $orderBy = 'id'; + private $orderBy = 'id'; /** * The direction to order by. @@ -27,9 +28,10 @@ trait OrderFilterTrait * @var string */ #[Serializer\Type('string')] + #[Serializer\Groups(["read"])] #[OA\Property(type: 'string', description: 'The direction to order by.', example: 'ASC')] #[Assert\Choice(choices: ['ASC', 'DESC'])] - private string $order = 'ASC'; + private $order = 'ASC'; /** * Get the field to order by. diff --git a/src/OpenApi/SchemaQueryParameter.php b/src/OpenApi/SchemaQueryParameter.php new file mode 100644 index 0000000..7810f7b --- /dev/null +++ b/src/OpenApi/SchemaQueryParameter.php @@ -0,0 +1,142 @@ +analysis = $analysis; + + /** @var OA\Parameter[] $schemas */ + $parameters = $analysis->getAnnotationsOfType(OA\Parameter::class); + + /** @var OA\Operation[] $operations */ + $operations = $analysis->getAnnotationsOfType(OA\Operation::class); + + + foreach ($operations as $operation) { + if ($operation->x !== GENERATOR::UNDEFINED && array_key_exists(self::X_QUERY_AGS_REF, $operation->x)) { + // Check if the ref exists and is a string. + if (is_string($operation->x[self::X_QUERY_AGS_REF]->ref)) { + $schema = $this->schemaForRef($operation->x[self::X_QUERY_AGS_REF]->ref); + + // Check if the schema is of type 'OA\Schema' and if it has properties. + if ($schema && $schema instanceof OA\Schema && $schema->properties !== GENERATOR::UNDEFINED) { + $this->expandQueryArgs($operation, $schema); + $this->cleanUp($operation, $schema); + } + } + } + } + } + + /** + * Find schema for the given ref. + * + * @param Schema[] $schemas + * @param string $ref + */ + protected function schemaForRef(string $ref) + { + $name = str_replace(OA\Components::SCHEMA_REF, '', $ref); + $schema = Util::getSchema($this->analysis->openapi, $name); + + if ($schema) { + return $schema; + } + + return null; + } + + /** + * Expand the given operation by injecting parameters for all properties of the given schema. + */ + protected function expandQueryArgs(Operation $operation, OA\Schema $schema) + { + + $operation->parameters = $operation->parameters === GENERATOR::UNDEFINED ? [] : $operation->parameters; + + // Extract the properties from the schema. + $properties = $schema->properties; + + // Loop through the properties and create a parameter for each. + foreach ($properties as $property) { + if (!($property instanceof OA\Property)) { + continue; + } + + $parameterName = $property->property; + + // If the property is an array, we need to add the [] to the name. + if ($property->type === 'array') { + $parameterName .= '[]'; + } + + $parameter = new OA\Parameter([ + 'name' => $parameterName, + 'in' => 'query', + 'required' => $property->required, + 'description' => $property->description, + 'schema' => $property, + ]); + + $operation->parameters[] = $parameter; + } + } + + /** + * Clean up. + */ + protected function cleanUp(OA\Operation $operation, OA\Schema $schema) + { + + /** @var OA\OpenApi */ + $api = $this->analysis->openapi; + + // Find the key for the schema. + $key = null; + foreach ($api->components->schemas as $k => $v) { + if ($v === $schema) { + $key = $k; + break; + } + } + + // Remove the schema from the components. + if ($key !== null) { + unset($api->components->schemas[$key]); + } + + unset($operation->x[self::X_QUERY_AGS_REF]); + if (!$operation->x) { + $operation->x = GENERATOR::UNDEFINED; + } + } + + /** + * Helper function to check if a given values is "undefined" in the context of the OpenApiPhp library. + */ + protected function isUndefined($value) + { + return $value === GENERATOR::UNDEFINED; + } +}