1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the API Platform project. |
5
|
|
|
* |
6
|
|
|
* (c) Kévin Dunglas <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
declare(strict_types=1); |
13
|
|
|
|
14
|
|
|
namespace ApiPlatform\Core\Swagger\Serializer; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\FilterCollection; |
17
|
|
|
use ApiPlatform\Core\Api\FilterLocatorTrait; |
18
|
|
|
use ApiPlatform\Core\Api\OperationMethodResolverInterface; |
19
|
|
|
use ApiPlatform\Core\Api\OperationType; |
20
|
|
|
use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
21
|
|
|
use ApiPlatform\Core\Api\UrlGeneratorInterface; |
22
|
|
|
use ApiPlatform\Core\Documentation\Documentation; |
23
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
24
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
25
|
|
|
use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
26
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
27
|
|
|
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
28
|
|
|
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; |
29
|
|
|
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; |
30
|
|
|
use Psr\Container\ContainerInterface; |
31
|
|
|
use Symfony\Component\PropertyInfo\Type; |
32
|
|
|
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
33
|
|
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; |
34
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Creates a machine readable Swagger API documentation. |
38
|
|
|
* |
39
|
|
|
* @author Amrouche Hamza <[email protected]> |
40
|
|
|
* @author Teoh Han Hui <[email protected]> |
41
|
|
|
* @author Kévin Dunglas <[email protected]> |
42
|
|
|
*/ |
43
|
|
|
final class DocumentationNormalizer implements NormalizerInterface |
44
|
|
|
{ |
45
|
|
|
use FilterLocatorTrait; |
46
|
|
|
|
47
|
|
|
const SWAGGER_VERSION = '2.0'; |
48
|
|
|
const FORMAT = 'json'; |
49
|
|
|
const SWAGGER_DEFINITION_NAME = 'swagger_definition_name'; |
50
|
|
|
|
51
|
|
|
private $resourceMetadataFactory; |
52
|
|
|
private $propertyNameCollectionFactory; |
53
|
|
|
private $propertyMetadataFactory; |
54
|
|
|
private $resourceClassResolver; |
55
|
|
|
private $operationMethodResolver; |
56
|
|
|
private $operationPathResolver; |
57
|
|
|
private $nameConverter; |
58
|
|
|
private $oauthEnabled; |
59
|
|
|
private $oauthType; |
60
|
|
|
private $oauthFlow; |
61
|
|
|
private $oauthTokenUrl; |
62
|
|
|
private $oauthAuthorizationUrl; |
63
|
|
|
private $oauthScopes; |
64
|
|
|
private $apiKeys; |
65
|
|
|
private $subresourceOperationFactory; |
66
|
|
|
private $paginationEnabled; |
67
|
|
|
private $paginationPageParameterName; |
68
|
|
|
private $clientItemsPerPage; |
69
|
|
|
private $itemsPerPageParameterName; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection |
73
|
|
|
*/ |
74
|
|
|
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage') |
75
|
|
|
{ |
76
|
|
|
if ($urlGenerator) { |
77
|
|
|
@trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.1 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
$this->setFilterLocator($filterLocator, true); |
81
|
|
|
|
82
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
83
|
|
|
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
84
|
|
|
$this->propertyMetadataFactory = $propertyMetadataFactory; |
85
|
|
|
$this->resourceClassResolver = $resourceClassResolver; |
86
|
|
|
$this->operationMethodResolver = $operationMethodResolver; |
87
|
|
|
$this->operationPathResolver = $operationPathResolver; |
88
|
|
|
$this->nameConverter = $nameConverter; |
89
|
|
|
$this->oauthEnabled = $oauthEnabled; |
90
|
|
|
$this->oauthType = $oauthType; |
91
|
|
|
$this->oauthFlow = $oauthFlow; |
92
|
|
|
$this->oauthTokenUrl = $oauthTokenUrl; |
93
|
|
|
$this->oauthAuthorizationUrl = $oauthAuthorizationUrl; |
94
|
|
|
$this->oauthScopes = $oauthScopes; |
95
|
|
|
$this->subresourceOperationFactory = $subresourceOperationFactory; |
96
|
|
|
$this->paginationEnabled = $paginationEnabled; |
97
|
|
|
$this->paginationPageParameterName = $paginationPageParameterName; |
98
|
|
|
$this->apiKeys = $apiKeys; |
99
|
|
|
$this->subresourceOperationFactory = $subresourceOperationFactory; |
100
|
|
|
$this->clientItemsPerPage = $clientItemsPerPage; |
101
|
|
|
$this->itemsPerPageParameterName = $itemsPerPageParameterName; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* {@inheritdoc} |
106
|
|
|
*/ |
107
|
|
|
public function normalize($object, $format = null, array $context = []) |
108
|
|
|
{ |
109
|
|
|
$mimeTypes = $object->getMimeTypes(); |
110
|
|
|
$definitions = new \ArrayObject(); |
111
|
|
|
$paths = new \ArrayObject(); |
112
|
|
|
|
113
|
|
|
foreach ($object->getResourceNameCollection() as $resourceClass) { |
114
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
115
|
|
|
$resourceShortName = $resourceMetadata->getShortName(); |
116
|
|
|
|
117
|
|
|
$this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION); |
118
|
|
|
$this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM); |
119
|
|
|
|
120
|
|
|
if (null === $this->subresourceOperationFactory) { |
121
|
|
|
continue; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) { |
125
|
|
|
$operationName = 'get'; |
126
|
|
|
$subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']); |
127
|
|
|
$serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName); |
128
|
|
|
$responseDefinitionKey = $this->getDefinition($definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $serializerContext); |
129
|
|
|
|
130
|
|
|
$pathOperation = new \ArrayObject([]); |
131
|
|
|
$pathOperation['tags'] = $subresourceOperation['shortNames']; |
132
|
|
|
$pathOperation['operationId'] = $operationId; |
133
|
|
|
$pathOperation['produces'] = $mimeTypes; |
134
|
|
|
$pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : ''); |
135
|
|
|
$pathOperation['responses'] = [ |
136
|
|
|
'200' => $subresourceOperation['collection'] ? [ |
137
|
|
|
'description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0]), |
138
|
|
|
'schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]], |
139
|
|
|
] : [ |
140
|
|
|
'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]), |
141
|
|
|
'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)], |
142
|
|
|
], |
143
|
|
|
'404' => ['description' => 'Resource not found'], |
144
|
|
|
]; |
145
|
|
|
|
146
|
|
|
// Avoid duplicates parameters when there is a filter on a subresource identifier |
147
|
|
|
$parametersMemory = []; |
148
|
|
|
$pathOperation['parameters'] = []; |
149
|
|
|
|
150
|
|
|
foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) { |
151
|
|
|
if (true === $hasIdentifier) { |
152
|
|
|
$pathOperation['parameters'][] = ['name' => $identifier, 'in' => 'path', 'required' => true, 'type' => 'string']; |
153
|
|
|
$parametersMemory[] = $identifier; |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
if ($parameters = $this->getFiltersParameters($subresourceOperation['resource_class'], $operationName, $subResourceMetadata, $definitions, $serializerContext)) { |
158
|
|
|
foreach ($parameters as $parameter) { |
159
|
|
|
if (!\in_array($parameter['name'], $parametersMemory, true)) { |
160
|
|
|
$pathOperation['parameters'][] = $parameter; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
$paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]); |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$definitions->ksort(); |
170
|
|
|
$paths->ksort(); |
171
|
|
|
|
172
|
|
|
return $this->computeDoc($object, $definitions, $paths, $context); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Updates the list of entries in the paths collection. |
177
|
|
|
* |
178
|
|
|
* @param \ArrayObject $paths |
179
|
|
|
* @param \ArrayObject $definitions |
180
|
|
|
* @param string $resourceClass |
181
|
|
|
* @param string $resourceShortName |
182
|
|
|
* @param ResourceMetadata $resourceMetadata |
183
|
|
|
* @param array $mimeTypes |
184
|
|
|
* @param string $operationType |
185
|
|
|
*/ |
186
|
|
|
private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType) |
187
|
|
|
{ |
188
|
|
|
if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) { |
189
|
|
|
return; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
foreach ($operations as $operationName => $operation) { |
193
|
|
|
$path = $this->getPath($resourceShortName, $operationName, $operation, $operationType); |
194
|
|
|
$method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); |
195
|
|
|
|
196
|
|
|
$paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions); |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Gets the path for an operation. |
202
|
|
|
* |
203
|
|
|
* If the path ends with the optional _format parameter, it is removed |
204
|
|
|
* as optional path parameters are not yet supported. |
205
|
|
|
* |
206
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/issues/93 |
207
|
|
|
* |
208
|
|
|
* @param string $resourceShortName |
209
|
|
|
* @param string $operationName |
210
|
|
|
* @param array $operation |
211
|
|
|
* @param string $operationType |
212
|
|
|
* |
213
|
|
|
* @return string |
214
|
|
|
*/ |
215
|
|
|
private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string |
216
|
|
|
{ |
217
|
|
|
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName); |
218
|
|
|
if ('.{_format}' === substr($path, -10)) { |
219
|
|
|
$path = substr($path, 0, -10); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return $path; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Gets a path Operation Object. |
227
|
|
|
* |
228
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object |
229
|
|
|
* |
230
|
|
|
* @param string $operationName |
231
|
|
|
* @param array $operation |
232
|
|
|
* @param string $method |
233
|
|
|
* @param string $operationType |
234
|
|
|
* @param string $resourceClass |
235
|
|
|
* @param ResourceMetadata $resourceMetadata |
236
|
|
|
* @param string[] $mimeTypes |
237
|
|
|
* @param \ArrayObject $definitions |
238
|
|
|
* |
239
|
|
|
* @return \ArrayObject |
240
|
|
|
*/ |
241
|
|
|
private function getPathOperation(string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject |
242
|
|
|
{ |
243
|
|
|
$pathOperation = new \ArrayObject($operation['swagger_context'] ?? []); |
244
|
|
|
$resourceShortName = $resourceMetadata->getShortName(); |
245
|
|
|
$pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName]; |
246
|
|
|
$pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType); |
247
|
|
|
if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) { |
248
|
|
|
$pathOperation['deprecated'] = true; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
switch ($method) { |
252
|
|
|
case 'GET': |
253
|
|
|
return $this->updateGetOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions); |
254
|
|
|
case 'POST': |
255
|
|
|
return $this->updatePostOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions); |
256
|
|
|
case 'PATCH': |
257
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName); |
258
|
|
|
// no break |
259
|
|
|
case 'PUT': |
260
|
|
|
return $this->updatePutOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions); |
261
|
|
|
case 'DELETE': |
262
|
|
|
return $this->updateDeleteOperation($pathOperation, $resourceShortName); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
return $pathOperation; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* @param \ArrayObject $pathOperation |
270
|
|
|
* @param array $mimeTypes |
271
|
|
|
* @param string $operationType |
272
|
|
|
* @param ResourceMetadata $resourceMetadata |
273
|
|
|
* @param string $resourceClass |
274
|
|
|
* @param string $resourceShortName |
275
|
|
|
* @param string $operationName |
276
|
|
|
* @param \ArrayObject $definitions |
277
|
|
|
* |
278
|
|
|
* @return \ArrayObject |
279
|
|
|
*/ |
280
|
|
|
private function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions) |
281
|
|
|
{ |
282
|
|
|
$serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName); |
283
|
|
|
$responseDefinitionKey = $this->getDefinition($definitions, $resourceMetadata, $resourceClass, $serializerContext); |
284
|
|
|
|
285
|
|
|
$pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes; |
286
|
|
|
|
287
|
|
|
if (OperationType::COLLECTION === $operationType) { |
288
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName); |
289
|
|
|
$pathOperation['responses'] ?? $pathOperation['responses'] = [ |
290
|
|
|
'200' => [ |
291
|
|
|
'description' => sprintf('%s collection response', $resourceShortName), |
292
|
|
|
'schema' => [ |
293
|
|
|
'type' => 'array', |
294
|
|
|
'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)], |
295
|
|
|
], |
296
|
|
|
], |
297
|
|
|
]; |
298
|
|
|
|
299
|
|
|
if (!isset($pathOperation['parameters']) && $parameters = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext)) { |
300
|
|
|
$pathOperation['parameters'] = $parameters; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) { |
304
|
|
|
$pathOperation['parameters'][] = $this->getPaginationParameters(); |
305
|
|
|
|
306
|
|
|
if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) { |
307
|
|
|
$pathOperation['parameters'][] = $this->getItemsPerPageParameters(); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
return $pathOperation; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName); |
315
|
|
|
$pathOperation['parameters'] ?? $pathOperation['parameters'] = [[ |
316
|
|
|
'name' => 'id', |
317
|
|
|
'in' => 'path', |
318
|
|
|
'required' => true, |
319
|
|
|
'type' => 'string', |
320
|
|
|
]]; |
321
|
|
|
$pathOperation['responses'] ?? $pathOperation['responses'] = [ |
322
|
|
|
'200' => [ |
323
|
|
|
'description' => sprintf('%s resource response', $resourceShortName), |
324
|
|
|
'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)], |
325
|
|
|
], |
326
|
|
|
'404' => ['description' => 'Resource not found'], |
327
|
|
|
]; |
328
|
|
|
|
329
|
|
|
return $pathOperation; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @param \ArrayObject $pathOperation |
334
|
|
|
* @param array $mimeTypes |
335
|
|
|
* @param string $operationType |
336
|
|
|
* @param ResourceMetadata $resourceMetadata |
337
|
|
|
* @param string $resourceClass |
338
|
|
|
* @param string $resourceShortName |
339
|
|
|
* @param string $operationName |
340
|
|
|
* @param \ArrayObject $definitions |
341
|
|
|
* |
342
|
|
|
* @return \ArrayObject |
343
|
|
|
*/ |
344
|
|
|
private function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions) |
345
|
|
|
{ |
346
|
|
|
$pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes; |
347
|
|
|
$pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes; |
348
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName); |
349
|
|
|
$pathOperation['parameters'] ?? $pathOperation['parameters'] = [[ |
350
|
|
|
'name' => lcfirst($resourceShortName), |
351
|
|
|
'in' => 'body', |
352
|
|
|
'description' => sprintf('The new %s resource', $resourceShortName), |
353
|
|
|
'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass, |
354
|
|
|
$this->getSerializerContext($operationType, true, $resourceMetadata, $operationName) |
355
|
|
|
))], |
356
|
|
|
]]; |
357
|
|
|
$pathOperation['responses'] ?? $pathOperation['responses'] = [ |
358
|
|
|
'201' => [ |
359
|
|
|
'description' => sprintf('%s resource created', $resourceShortName), |
360
|
|
|
'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass, |
361
|
|
|
$this->getSerializerContext($operationType, false, $resourceMetadata, $operationName) |
362
|
|
|
))], |
363
|
|
|
], |
364
|
|
|
'400' => ['description' => 'Invalid input'], |
365
|
|
|
'404' => ['description' => 'Resource not found'], |
366
|
|
|
]; |
367
|
|
|
|
368
|
|
|
return $pathOperation; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* @param \ArrayObject $pathOperation |
373
|
|
|
* @param array $mimeTypes |
374
|
|
|
* @param string $operationType |
375
|
|
|
* @param ResourceMetadata $resourceMetadata |
376
|
|
|
* @param string $resourceClass |
377
|
|
|
* @param string $resourceShortName |
378
|
|
|
* @param string $operationName |
379
|
|
|
* @param \ArrayObject $definitions |
380
|
|
|
* |
381
|
|
|
* @return \ArrayObject |
382
|
|
|
*/ |
383
|
|
|
private function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions) |
384
|
|
|
{ |
385
|
|
|
$pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes; |
386
|
|
|
$pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes; |
387
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName); |
388
|
|
|
$pathOperation['parameters'] ?? $pathOperation['parameters'] = [ |
389
|
|
|
[ |
390
|
|
|
'name' => 'id', |
391
|
|
|
'in' => 'path', |
392
|
|
|
'type' => 'string', |
393
|
|
|
'required' => true, |
394
|
|
|
], |
395
|
|
|
[ |
396
|
|
|
'name' => lcfirst($resourceShortName), |
397
|
|
|
'in' => 'body', |
398
|
|
|
'description' => sprintf('The updated %s resource', $resourceShortName), |
399
|
|
|
'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass, |
400
|
|
|
$this->getSerializerContext($operationType, true, $resourceMetadata, $operationName) |
401
|
|
|
))], |
402
|
|
|
], |
403
|
|
|
]; |
404
|
|
|
$pathOperation['responses'] ?? $pathOperation['responses'] = [ |
405
|
|
|
'200' => [ |
406
|
|
|
'description' => sprintf('%s resource updated', $resourceShortName), |
407
|
|
|
'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass, |
408
|
|
|
$this->getSerializerContext($operationType, false, $resourceMetadata, $operationName) |
409
|
|
|
))], |
410
|
|
|
], |
411
|
|
|
'400' => ['description' => 'Invalid input'], |
412
|
|
|
'404' => ['description' => 'Resource not found'], |
413
|
|
|
]; |
414
|
|
|
|
415
|
|
|
return $pathOperation; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* @param \ArrayObject $pathOperation |
420
|
|
|
* @param string $resourceShortName |
421
|
|
|
* |
422
|
|
|
* @return \ArrayObject |
423
|
|
|
*/ |
424
|
|
|
private function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName): \ArrayObject |
425
|
|
|
{ |
426
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName); |
427
|
|
|
$pathOperation['responses'] ?? $pathOperation['responses'] = [ |
428
|
|
|
'204' => ['description' => sprintf('%s resource deleted', $resourceShortName)], |
429
|
|
|
'404' => ['description' => 'Resource not found'], |
430
|
|
|
]; |
431
|
|
|
|
432
|
|
|
$pathOperation['parameters'] ?? $pathOperation['parameters'] = [[ |
433
|
|
|
'name' => 'id', |
434
|
|
|
'in' => 'path', |
435
|
|
|
'type' => 'string', |
436
|
|
|
'required' => true, |
437
|
|
|
]]; |
438
|
|
|
|
439
|
|
|
return $pathOperation; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
/** |
443
|
|
|
* @param \ArrayObject $definitions |
444
|
|
|
* @param ResourceMetadata $resourceMetadata |
445
|
|
|
* @param string $resourceClass |
446
|
|
|
* @param array|null $serializerContext |
447
|
|
|
* |
448
|
|
|
* @return string |
449
|
|
|
*/ |
450
|
|
|
private function getDefinition(\ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string |
451
|
|
|
{ |
452
|
|
|
if (isset($serializerContext[self::SWAGGER_DEFINITION_NAME])) { |
453
|
|
|
$definitionKey = sprintf('%s-%s', $resourceMetadata->getShortName(), $serializerContext[self::SWAGGER_DEFINITION_NAME]); |
454
|
|
|
} else { |
455
|
|
|
$definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? [])); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
if (!isset($definitions[$definitionKey])) { |
459
|
|
|
$definitions[$definitionKey] = []; // Initialize first to prevent infinite loop |
460
|
|
|
$definitions[$definitionKey] = $this->getDefinitionSchema($resourceClass, $resourceMetadata, $definitions, $serializerContext); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
return $definitionKey; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
private function getDefinitionKey(string $resourceShortName, array $groups): string |
467
|
|
|
{ |
468
|
|
|
return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* Gets a definition Schema Object. |
473
|
|
|
* |
474
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject |
475
|
|
|
* |
476
|
|
|
* @param string $resourceClass |
477
|
|
|
* @param ResourceMetadata $resourceMetadata |
478
|
|
|
* @param \ArrayObject $definitions |
479
|
|
|
* @param array|null $serializerContext |
480
|
|
|
* |
481
|
|
|
* @return \ArrayObject |
482
|
|
|
*/ |
483
|
|
|
private function getDefinitionSchema(string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject |
484
|
|
|
{ |
485
|
|
|
$definitionSchema = new \ArrayObject(['type' => 'object']); |
486
|
|
|
|
487
|
|
|
if (null !== $description = $resourceMetadata->getDescription()) { |
488
|
|
|
$definitionSchema['description'] = $description; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
if (null !== $iri = $resourceMetadata->getIri()) { |
492
|
|
|
$definitionSchema['externalDocs'] = ['url' => $iri]; |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
$options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : []; |
496
|
|
|
foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) { |
497
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); |
498
|
|
|
$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName; |
499
|
|
|
|
500
|
|
|
if ($propertyMetadata->isRequired()) { |
501
|
|
|
$definitionSchema['required'][] = $normalizedPropertyName; |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
$definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata, $definitions, $serializerContext); |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
return $definitionSchema; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
/** |
511
|
|
|
* Gets a property Schema Object. |
512
|
|
|
* |
513
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject |
514
|
|
|
* |
515
|
|
|
* @param PropertyMetadata $propertyMetadata |
516
|
|
|
* @param \ArrayObject $definitions |
517
|
|
|
* @param array|null $serializerContext |
518
|
|
|
* |
519
|
|
|
* @return \ArrayObject |
520
|
|
|
*/ |
521
|
|
|
private function getPropertySchema(PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject |
522
|
|
|
{ |
523
|
|
|
$propertySchema = new \ArrayObject($propertyMetadata->getAttributes()['swagger_context'] ?? []); |
524
|
|
|
|
525
|
|
|
if (false === $propertyMetadata->isWritable()) { |
526
|
|
|
$propertySchema['readOnly'] = true; |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
if (null !== $description = $propertyMetadata->getDescription()) { |
530
|
|
|
$propertySchema['description'] = $description; |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
if (null === $type = $propertyMetadata->getType()) { |
534
|
|
|
return $propertySchema; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
$isCollection = $type->isCollection(); |
538
|
|
|
if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) { |
539
|
|
|
$builtinType = 'string'; |
540
|
|
|
$className = null; |
541
|
|
|
} else { |
542
|
|
|
$builtinType = $valueType->getBuiltinType(); |
543
|
|
|
$className = $valueType->getClassName(); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
$valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext); |
547
|
|
|
|
548
|
|
|
return new \ArrayObject((array) $propertySchema + $valueSchema); |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
/** |
552
|
|
|
* Gets the Swagger's type corresponding to the given PHP's type. |
553
|
|
|
* |
554
|
|
|
* @param string $type |
555
|
|
|
* @param bool $isCollection |
556
|
|
|
* @param string $className |
557
|
|
|
* @param bool $readableLink |
558
|
|
|
* @param \ArrayObject $definitions |
559
|
|
|
* @param array|null $serializerContext |
560
|
|
|
* |
561
|
|
|
* @return array |
562
|
|
|
*/ |
563
|
|
|
private function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array |
564
|
|
|
{ |
565
|
|
|
if ($isCollection) { |
566
|
|
|
return ['type' => 'array', 'items' => $this->getType($type, false, $className, $readableLink, $definitions, $serializerContext)]; |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
if (Type::BUILTIN_TYPE_STRING === $type) { |
570
|
|
|
return ['type' => 'string']; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
if (Type::BUILTIN_TYPE_INT === $type) { |
574
|
|
|
return ['type' => 'integer']; |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
if (Type::BUILTIN_TYPE_FLOAT === $type) { |
578
|
|
|
return ['type' => 'number']; |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
if (Type::BUILTIN_TYPE_BOOL === $type) { |
582
|
|
|
return ['type' => 'boolean']; |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
if (Type::BUILTIN_TYPE_OBJECT === $type) { |
586
|
|
|
if (null === $className) { |
587
|
|
|
return ['type' => 'string']; |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
if (is_subclass_of($className, \DateTimeInterface::class)) { |
591
|
|
|
return ['type' => 'string', 'format' => 'date-time']; |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
if (!$this->resourceClassResolver->isResourceClass($className)) { |
595
|
|
|
return ['type' => 'string']; |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
if (true === $readableLink) { |
599
|
|
|
return ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, |
600
|
|
|
$this->resourceMetadataFactory->create($className), |
601
|
|
|
$className, $serializerContext) |
602
|
|
|
)]; |
603
|
|
|
} |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
return ['type' => 'string']; |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Computes the Swagger documentation. |
611
|
|
|
* |
612
|
|
|
* @param Documentation $documentation |
613
|
|
|
* @param \ArrayObject $definitions |
614
|
|
|
* @param \ArrayObject $paths |
615
|
|
|
* @param array $context |
616
|
|
|
* |
617
|
|
|
* @return array |
618
|
|
|
*/ |
619
|
|
|
private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array |
620
|
|
|
{ |
621
|
|
|
$doc = [ |
622
|
|
|
'swagger' => self::SWAGGER_VERSION, |
623
|
|
|
'basePath' => $context['base_url'] ?? '/', |
624
|
|
|
'info' => [ |
625
|
|
|
'title' => $documentation->getTitle(), |
626
|
|
|
'version' => $documentation->getVersion(), |
627
|
|
|
], |
628
|
|
|
'paths' => $paths, |
629
|
|
|
]; |
630
|
|
|
|
631
|
|
|
$securityDefinitions = []; |
632
|
|
|
$security = []; |
633
|
|
|
|
634
|
|
|
if ($this->oauthEnabled) { |
635
|
|
|
$securityDefinitions['oauth'] = [ |
636
|
|
|
'type' => $this->oauthType, |
637
|
|
|
'description' => 'OAuth client_credentials Grant', |
638
|
|
|
'flow' => $this->oauthFlow, |
639
|
|
|
'tokenUrl' => $this->oauthTokenUrl, |
640
|
|
|
'authorizationUrl' => $this->oauthAuthorizationUrl, |
641
|
|
|
'scopes' => $this->oauthScopes, |
642
|
|
|
]; |
643
|
|
|
|
644
|
|
|
$security[] = ['oauth' => []]; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
if ($this->apiKeys) { |
|
|
|
|
648
|
|
|
foreach ($this->apiKeys as $key => $apiKey) { |
649
|
|
|
$name = $apiKey['name']; |
650
|
|
|
$type = $apiKey['type']; |
651
|
|
|
|
652
|
|
|
$securityDefinitions[$key] = [ |
653
|
|
|
'type' => 'apiKey', |
654
|
|
|
'in' => $type, |
655
|
|
|
'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type), |
656
|
|
|
'name' => $name, |
657
|
|
|
]; |
658
|
|
|
|
659
|
|
|
$security[] = [$key => []]; |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
if ($securityDefinitions && $security) { |
664
|
|
|
$doc['securityDefinitions'] = $securityDefinitions; |
665
|
|
|
$doc['security'] = $security; |
666
|
|
|
} |
667
|
|
|
|
668
|
|
|
if ('' !== $description = $documentation->getDescription()) { |
669
|
|
|
$doc['info']['description'] = $description; |
670
|
|
|
} |
671
|
|
|
|
672
|
|
|
if (\count($definitions) > 0) { |
673
|
|
|
$doc['definitions'] = $definitions; |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
return $doc; |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
/** |
680
|
|
|
* Gets Swagger parameters corresponding to enabled filters. |
681
|
|
|
* |
682
|
|
|
* @param string $resourceClass |
683
|
|
|
* @param string $operationName |
684
|
|
|
* @param ResourceMetadata $resourceMetadata |
685
|
|
|
* @param \ArrayObject $definitions |
686
|
|
|
* @param array|null $serializerContext |
687
|
|
|
* |
688
|
|
|
* @return array |
689
|
|
|
*/ |
690
|
|
|
private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array |
691
|
|
|
{ |
692
|
|
|
if (null === $this->filterLocator) { |
693
|
|
|
return []; |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
$parameters = []; |
697
|
|
|
$resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true); |
698
|
|
|
foreach ($resourceFilters as $filterId) { |
699
|
|
|
if (!$filter = $this->getFilter($filterId)) { |
700
|
|
|
continue; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
foreach ($filter->getDescription($resourceClass) as $name => $data) { |
704
|
|
|
$parameter = [ |
705
|
|
|
'name' => $name, |
706
|
|
|
'in' => 'query', |
707
|
|
|
'required' => $data['required'], |
708
|
|
|
]; |
709
|
|
|
$parameter += $this->getType($data['type'], false, null, null, $definitions, $serializerContext); |
710
|
|
|
|
711
|
|
|
if (isset($data['swagger'])) { |
712
|
|
|
$parameter = $data['swagger'] + $parameter; |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
$parameters[] = $parameter; |
716
|
|
|
} |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
return $parameters; |
720
|
|
|
} |
721
|
|
|
|
722
|
|
|
/** |
723
|
|
|
* Returns pagination parameters for the "get" collection operation. |
724
|
|
|
* |
725
|
|
|
* @return array |
726
|
|
|
*/ |
727
|
|
|
private function getPaginationParameters(): array |
728
|
|
|
{ |
729
|
|
|
return [ |
730
|
|
|
'name' => $this->paginationPageParameterName, |
731
|
|
|
'in' => 'query', |
732
|
|
|
'required' => false, |
733
|
|
|
'type' => 'integer', |
734
|
|
|
'description' => 'The collection page number', |
735
|
|
|
]; |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* Returns items per page parameters for the "get" collection operation. |
740
|
|
|
* |
741
|
|
|
* @return array |
742
|
|
|
*/ |
743
|
|
|
private function getItemsPerPageParameters(): array |
744
|
|
|
{ |
745
|
|
|
return [ |
746
|
|
|
'name' => $this->itemsPerPageParameterName, |
747
|
|
|
'in' => 'query', |
748
|
|
|
'required' => false, |
749
|
|
|
'type' => 'integer', |
750
|
|
|
'description' => 'The number of items per page', |
751
|
|
|
]; |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
/** |
755
|
|
|
* {@inheritdoc} |
756
|
|
|
*/ |
757
|
|
|
public function supportsNormalization($data, $format = null) |
758
|
|
|
{ |
759
|
|
|
return self::FORMAT === $format && $data instanceof Documentation; |
760
|
|
|
} |
761
|
|
|
|
762
|
|
|
/** |
763
|
|
|
* @param string $operationType |
764
|
|
|
* @param bool $denormalization |
765
|
|
|
* @param ResourceMetadata $resourceMetadata |
766
|
|
|
* @param string $operationType |
767
|
|
|
* |
768
|
|
|
* @return array|null |
769
|
|
|
*/ |
770
|
|
|
private function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName) |
771
|
|
|
{ |
772
|
|
|
$contextKey = $denormalization ? 'denormalization_context' : 'normalization_context'; |
773
|
|
|
|
774
|
|
|
if (OperationType::COLLECTION === $operationType) { |
775
|
|
|
return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true); |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true); |
779
|
|
|
} |
780
|
|
|
} |
781
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.