Skip to content
This repository was archived by the owner on Apr 7, 2025. It is now read-only.

Commit 49d2de9

Browse files
authored
Fix OpenAPI documentation (#75)
1 parent 7ffea8b commit 49d2de9

File tree

6 files changed

+195
-88
lines changed

6 files changed

+195
-88
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"jms/serializer": "^3.28",
1616
"jms/serializer-bundle": "^5.3",
1717
"knplabs/doctrine-behaviors": "^2.6",
18-
"nelmio/api-doc-bundle": "^4.12",
18+
"nelmio/api-doc-bundle": "^4.16",
1919
"nelmio/cors-bundle": "^2.3",
2020
"phpdocumentor/reflection-docblock": "^5.3",
2121
"phpstan/phpdoc-parser": "^1.24",

composer.lock

Lines changed: 35 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ services:
2727

2828
# add more service definitions when explicit configuration is needed
2929
# please note that last definitions always *replace* previous ones
30+
31+
App\OpenApi\SchemaQueryParameter:
32+
tags: ['nelmio_api_doc.swagger.processor']

src/Controller/Api/ThemesController.php

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -40,56 +40,16 @@ public function __construct(
4040
*/
4141
#[REST\Get('/', name: 'api_themes_get')]
4242
#[REST\View(serializerGroups: ['read'])]
43-
#[OA\Parameter(
44-
in: 'query',
45-
name: 'search',
46-
description: 'Search for themes.',
47-
example: 'Twenty',
48-
schema: new OA\Schema(
49-
type: 'string',
50-
minLength: 3,
51-
maxLength: 128,
52-
),
53-
)]
54-
#[OA\Parameter(
55-
in: 'query',
56-
name: 'page',
57-
description: 'The page number.',
58-
example: 1,
59-
schema: new OA\Schema(
60-
type: 'integer',
61-
minimum: 1,
62-
),
63-
)]
64-
#[OA\Parameter(
65-
in: 'query',
66-
name: 'per_page',
67-
description: 'The number of items per page.',
68-
example: 10,
69-
schema: new OA\Schema(
70-
type: 'integer',
71-
minimum: 1,
72-
maximum: 100,
73-
),
74-
)]
75-
#[OA\Parameter(
76-
in: 'query',
77-
name: 'order',
78-
description: 'The order of the items.',
79-
example: 'ASC',
80-
schema: new OA\Schema(
81-
type: 'string',
82-
enum: ['ASC', 'DESC'],
83-
),
84-
)]
85-
#[OA\Parameter(
86-
in: 'query',
87-
name: 'order_by',
88-
description: 'The field to order the items by.',
89-
example: 'name',
90-
schema: new OA\Schema(
91-
type: 'string',
92-
)
43+
#[OA\Get(
44+
x: [
45+
'query-args-explode' => new OA\Schema(
46+
type: 'string',
47+
ref: new Model(
48+
type: ThemeFilter::class,
49+
groups: ['read'],
50+
),
51+
),
52+
]
9353
)]
9454
#[OA\Response(
9555
response: 200,

src/ControllerFilter/Traits/OrderFilterTrait.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ trait OrderFilterTrait
1717
* @var string
1818
*/
1919
#[Serializer\Type('string')]
20+
#[Serializer\Groups(["read"])]
2021
#[OA\Property(type: 'string', description: 'The field to order by.', example: 'id')]
21-
private string $orderBy = 'id';
22+
private $orderBy = 'id';
2223

2324
/**
2425
* The direction to order by.
@@ -27,9 +28,10 @@ trait OrderFilterTrait
2728
* @var string
2829
*/
2930
#[Serializer\Type('string')]
31+
#[Serializer\Groups(["read"])]
3032
#[OA\Property(type: 'string', description: 'The direction to order by.', example: 'ASC')]
3133
#[Assert\Choice(choices: ['ASC', 'DESC'])]
32-
private string $order = 'ASC';
34+
private $order = 'ASC';
3335

3436
/**
3537
* Get the field to order by.

src/OpenApi/SchemaQueryParameter.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
namespace App\OpenApi;
4+
5+
use OpenApi\Analysis;
6+
use OpenApi\Annotations\Operation;
7+
use OpenApi\Annotations as OA;
8+
use OpenApi\Generator;
9+
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
10+
11+
/**
12+
* Custom processor to translate the vendor tag `` into query parameter annotations.
13+
*
14+
* Details for the parameters are taken from the referenced schema.
15+
*/
16+
class SchemaQueryParameter
17+
{
18+
const X_QUERY_AGS_REF = 'query-args-explode';
19+
20+
/**
21+
* @param Analysis $analysis
22+
*/
23+
protected $analysis;
24+
25+
public function __invoke(Analysis $analysis)
26+
{
27+
$this->analysis = $analysis;
28+
29+
/** @var OA\Parameter[] $schemas */
30+
$parameters = $analysis->getAnnotationsOfType(OA\Parameter::class);
31+
32+
/** @var OA\Operation[] $operations */
33+
$operations = $analysis->getAnnotationsOfType(OA\Operation::class);
34+
35+
36+
foreach ($operations as $operation) {
37+
if ($operation->x !== GENERATOR::UNDEFINED && array_key_exists(self::X_QUERY_AGS_REF, $operation->x)) {
38+
// Check if the ref exists and is a string.
39+
if (is_string($operation->x[self::X_QUERY_AGS_REF]->ref)) {
40+
$schema = $this->schemaForRef($operation->x[self::X_QUERY_AGS_REF]->ref);
41+
42+
// Check if the schema is of type 'OA\Schema' and if it has properties.
43+
if ($schema && $schema instanceof OA\Schema && $schema->properties !== GENERATOR::UNDEFINED) {
44+
$this->expandQueryArgs($operation, $schema);
45+
$this->cleanUp($operation, $schema);
46+
}
47+
}
48+
}
49+
}
50+
}
51+
52+
/**
53+
* Find schema for the given ref.
54+
*
55+
* @param Schema[] $schemas
56+
* @param string $ref
57+
*/
58+
protected function schemaForRef(string $ref)
59+
{
60+
$name = str_replace(OA\Components::SCHEMA_REF, '', $ref);
61+
$schema = Util::getSchema($this->analysis->openapi, $name);
62+
63+
if ($schema) {
64+
return $schema;
65+
}
66+
67+
return null;
68+
}
69+
70+
/**
71+
* Expand the given operation by injecting parameters for all properties of the given schema.
72+
*/
73+
protected function expandQueryArgs(Operation $operation, OA\Schema $schema)
74+
{
75+
76+
$operation->parameters = $operation->parameters === GENERATOR::UNDEFINED ? [] : $operation->parameters;
77+
78+
// Extract the properties from the schema.
79+
$properties = $schema->properties;
80+
81+
// Loop through the properties and create a parameter for each.
82+
foreach ($properties as $property) {
83+
if (!($property instanceof OA\Property)) {
84+
continue;
85+
}
86+
87+
$parameterName = $property->property;
88+
89+
// If the property is an array, we need to add the [] to the name.
90+
if ($property->type === 'array') {
91+
$parameterName .= '[]';
92+
}
93+
94+
$parameter = new OA\Parameter([
95+
'name' => $parameterName,
96+
'in' => 'query',
97+
'required' => $property->required,
98+
'description' => $property->description,
99+
'schema' => $property,
100+
]);
101+
102+
$operation->parameters[] = $parameter;
103+
}
104+
}
105+
106+
/**
107+
* Clean up.
108+
*/
109+
protected function cleanUp(OA\Operation $operation, OA\Schema $schema)
110+
{
111+
112+
/** @var OA\OpenApi */
113+
$api = $this->analysis->openapi;
114+
115+
// Find the key for the schema.
116+
$key = null;
117+
foreach ($api->components->schemas as $k => $v) {
118+
if ($v === $schema) {
119+
$key = $k;
120+
break;
121+
}
122+
}
123+
124+
// Remove the schema from the components.
125+
if ($key !== null) {
126+
unset($api->components->schemas[$key]);
127+
}
128+
129+
unset($operation->x[self::X_QUERY_AGS_REF]);
130+
if (!$operation->x) {
131+
$operation->x = GENERATOR::UNDEFINED;
132+
}
133+
}
134+
135+
/**
136+
* Helper function to check if a given values is "undefined" in the context of the OpenApiPhp library.
137+
*/
138+
protected function isUndefined($value)
139+
{
140+
return $value === GENERATOR::UNDEFINED;
141+
}
142+
}

0 commit comments

Comments
 (0)