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\OpenApi\Serializer; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\OperationType; |
17
|
|
|
use ApiPlatform\Core\Documentation\Documentation; |
18
|
|
|
use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
19
|
|
|
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
20
|
|
|
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; |
|
|
|
|
21
|
|
|
use Symfony\Component\PropertyInfo\Type; |
22
|
|
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; |
23
|
|
|
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; |
24
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Common features regarding Documentation normalization. |
28
|
|
|
* |
29
|
|
|
* @author Amrouche Hamza <[email protected]> |
30
|
|
|
* @author Teoh Han Hui <[email protected]> |
31
|
|
|
* @author Kévin Dunglas <[email protected]> |
32
|
|
|
* @author Anthony GRASSIOT <[email protected]> |
33
|
|
|
* |
34
|
|
|
* @internal |
35
|
|
|
*/ |
36
|
|
|
abstract class AbstractDocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface |
37
|
|
|
{ |
38
|
|
|
const FORMAT = 'json'; |
39
|
|
|
const ATTRIBUTE_NAME = 'openapi_context'; |
40
|
|
|
const BASE_URL = 'base_url'; |
41
|
|
|
|
42
|
|
|
protected $resourceMetadataFactory; |
43
|
|
|
protected $propertyNameCollectionFactory; |
44
|
|
|
protected $propertyMetadataFactory; |
45
|
|
|
protected $resourceClassResolver; |
46
|
|
|
protected $operationMethodResolver; |
47
|
|
|
protected $operationPathResolver; |
48
|
|
|
protected $nameConverter; |
49
|
|
|
protected $oauthEnabled; |
50
|
|
|
protected $oauthType; |
51
|
|
|
protected $oauthFlow; |
52
|
|
|
protected $oauthTokenUrl; |
53
|
|
|
protected $oauthAuthorizationUrl; |
54
|
|
|
protected $oauthScopes; |
55
|
|
|
protected $apiKeys; |
56
|
|
|
protected $subresourceOperationFactory; |
57
|
|
|
protected $paginationEnabled; |
58
|
|
|
protected $paginationPageParameterName; |
59
|
|
|
protected $clientItemsPerPage; |
60
|
|
|
protected $itemsPerPageParameterName; |
61
|
|
|
protected $paginationClientEnabled; |
62
|
|
|
protected $paginationClientEnabledParameterName; |
63
|
|
|
protected $formatsProvider; |
64
|
|
|
protected $defaultContext = [self::BASE_URL => '/']; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* {@inheritdoc} |
68
|
|
|
*/ |
69
|
|
|
public function supportsNormalization($data, $format = null) |
70
|
|
|
{ |
71
|
|
|
return self::FORMAT === $format && $data instanceof Documentation; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Gets the path for an operation. |
76
|
|
|
* |
77
|
|
|
* If the path ends with the optional _format parameter, it is removed |
78
|
|
|
* as optional path parameters are not yet supported. |
79
|
|
|
* |
80
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/issues/93 |
81
|
|
|
*/ |
82
|
|
|
protected function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string |
83
|
|
|
{ |
84
|
|
|
$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName); |
85
|
|
|
if ('.{_format}' === substr($path, -10)) { |
86
|
|
|
$path = substr($path, 0, -10); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
return $path; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Gets the Swagger's type corresponding to the given PHP's type. |
94
|
|
|
* |
95
|
|
|
* @param string $className |
96
|
|
|
* @param bool $readableLink |
97
|
|
|
*/ |
98
|
|
|
protected function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array |
99
|
|
|
{ |
100
|
|
|
if ($isCollection) { |
101
|
|
|
return ['type' => 'array', 'items' => $this->getType($type, false, $className, $readableLink, $definitions, $serializerContext)]; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
if (Type::BUILTIN_TYPE_STRING === $type) { |
105
|
|
|
return ['type' => 'string']; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
if (Type::BUILTIN_TYPE_INT === $type) { |
109
|
|
|
return ['type' => 'integer']; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
if (Type::BUILTIN_TYPE_FLOAT === $type) { |
113
|
|
|
return ['type' => 'number']; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if (Type::BUILTIN_TYPE_BOOL === $type) { |
117
|
|
|
return ['type' => 'boolean']; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
if (Type::BUILTIN_TYPE_OBJECT === $type) { |
121
|
|
|
if (null === $className) { |
122
|
|
|
return ['type' => 'string']; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
if (is_subclass_of($className, \DateTimeInterface::class)) { |
126
|
|
|
return ['type' => 'string', 'format' => 'date-time']; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
if (!$this->resourceClassResolver->isResourceClass($className)) { |
130
|
|
|
return ['type' => 'string']; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
if (true === $readableLink) { |
134
|
|
|
return ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, |
135
|
|
|
$this->resourceMetadataFactory->create($className), |
136
|
|
|
$className, $serializerContext) |
137
|
|
|
)]; |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return ['type' => 'string']; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @return array|null |
146
|
|
|
*/ |
147
|
|
|
protected function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName) |
148
|
|
|
{ |
149
|
|
|
$contextKey = $denormalization ? 'denormalization_context' : 'normalization_context'; |
150
|
|
|
|
151
|
|
|
if (OperationType::COLLECTION === $operationType) { |
152
|
|
|
return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
protected function getDefinition(\ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string |
159
|
|
|
{ |
160
|
|
|
if (isset($serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME])) { |
161
|
|
|
$definitionKey = sprintf('%s-%s', $resourceMetadata->getShortName(), $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]); |
162
|
|
|
} else { |
163
|
|
|
$definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? [])); |
|
|
|
|
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
if (!isset($definitions[$definitionKey])) { |
167
|
|
|
$definitions[$definitionKey] = []; // Initialize first to prevent infinite loop |
168
|
|
|
$definitions[$definitionKey] = $this->getDefinitionSchema($resourceClass, $resourceMetadata, $definitions, $serializerContext); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return $definitionKey; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
protected function getDefinitionKey(string $resourceShortName, array $groups): string |
175
|
|
|
{ |
176
|
|
|
return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Gets a definition Schema Object. |
181
|
|
|
* |
182
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject |
183
|
|
|
*/ |
184
|
|
|
protected function getDefinitionSchema(string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject |
185
|
|
|
{ |
186
|
|
|
$definitionSchema = new \ArrayObject(['type' => 'object']); |
187
|
|
|
|
188
|
|
|
if (null !== $description = $resourceMetadata->getDescription()) { |
189
|
|
|
$definitionSchema['description'] = $description; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
if (null !== $iri = $resourceMetadata->getIri()) { |
193
|
|
|
$definitionSchema['externalDocs'] = ['url' => $iri]; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
$options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : []; |
197
|
|
|
foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) { |
198
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); |
199
|
|
|
$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass, self::FORMAT, $serializerContext ?? []) : $propertyName; |
200
|
|
|
if ($propertyMetadata->isRequired()) { |
201
|
|
|
$definitionSchema['required'][] = $normalizedPropertyName; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata, $definitions, $serializerContext); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return $definitionSchema; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Gets a property Schema Object. |
212
|
|
|
* |
213
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject |
214
|
|
|
*/ |
215
|
|
|
protected function getPropertySchema(PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject |
216
|
|
|
{ |
217
|
|
|
$propertySchema = new \ArrayObject($propertyMetadata->getAttributes()[static::ATTRIBUTE_NAME] ?? []); |
218
|
|
|
|
219
|
|
|
if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { |
|
|
|
|
220
|
|
|
$propertySchema['readOnly'] = true; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if (null !== $description = $propertyMetadata->getDescription()) { |
224
|
|
|
$propertySchema['description'] = $description; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if (null === $type = $propertyMetadata->getType()) { |
228
|
|
|
return $propertySchema; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$isCollection = $type->isCollection(); |
232
|
|
|
if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) { |
233
|
|
|
$builtinType = 'string'; |
234
|
|
|
$className = null; |
235
|
|
|
} else { |
236
|
|
|
$builtinType = $valueType->getBuiltinType(); |
237
|
|
|
$className = $valueType->getClassName(); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
$valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext); |
241
|
|
|
|
242
|
|
|
return new \ArrayObject((array) $propertySchema + $valueSchema); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Gets a path Operation Object. |
247
|
|
|
* |
248
|
|
|
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object |
249
|
|
|
* |
250
|
|
|
* @param string[] $mimeTypes |
251
|
|
|
*/ |
252
|
|
|
protected function getPathOperation(string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject |
253
|
|
|
{ |
254
|
|
|
$pathOperation = new \ArrayObject($operation[static::ATTRIBUTE_NAME] ?? []); |
255
|
|
|
$resourceShortName = $resourceMetadata->getShortName(); |
256
|
|
|
$pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName]; |
257
|
|
|
$pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType); |
258
|
|
|
if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) { |
259
|
|
|
$pathOperation['deprecated'] = true; |
260
|
|
|
} |
261
|
|
|
if (null !== $this->formatsProvider) { |
262
|
|
|
$responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType); |
263
|
|
|
$responseMimeTypes = $this->extractMimeTypes($responseFormats); |
264
|
|
|
} |
265
|
|
|
switch ($method) { |
266
|
|
|
case 'GET': |
267
|
|
|
return $this->updateGetOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions); |
|
|
|
|
268
|
|
|
case 'POST': |
269
|
|
|
return $this->updatePostOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions); |
|
|
|
|
270
|
|
|
case 'PATCH': |
271
|
|
|
$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName); |
272
|
|
|
// no break |
273
|
|
|
case 'PUT': |
274
|
|
|
return $this->updatePutOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions); |
|
|
|
|
275
|
|
|
case 'DELETE': |
276
|
|
|
return $this->updateDeleteOperation($pathOperation, $resourceShortName); |
|
|
|
|
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
return $pathOperation; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
abstract protected function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions); |
283
|
|
|
|
284
|
|
|
abstract protected function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions); |
285
|
|
|
|
286
|
|
|
abstract protected function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions); |
287
|
|
|
|
288
|
|
|
abstract protected function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName); |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Updates the list of entries in the paths collection. |
292
|
|
|
*/ |
293
|
|
|
protected function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType) |
294
|
|
|
{ |
295
|
|
|
if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) { |
296
|
|
|
return; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
foreach ($operations as $operationName => $operation) { |
300
|
|
|
$path = $this->getPath($resourceShortName, $operationName, $operation, $operationType); |
301
|
|
|
$method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); |
302
|
|
|
|
303
|
|
|
$paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions); |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Returns pagination parameters for the "get" collection operation. |
309
|
|
|
*/ |
310
|
|
|
protected function getPaginationParameters(): array |
311
|
|
|
{ |
312
|
|
|
return [ |
313
|
|
|
'name' => $this->paginationPageParameterName, |
314
|
|
|
'in' => 'query', |
315
|
|
|
'required' => false, |
316
|
|
|
'type' => 'integer', |
317
|
|
|
'description' => 'The collection page number', |
318
|
|
|
]; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Returns enable pagination parameter for the "get" collection operation. |
323
|
|
|
*/ |
324
|
|
|
protected function getPaginationClientEnabledParameters(): array |
325
|
|
|
{ |
326
|
|
|
return [ |
327
|
|
|
'name' => $this->paginationClientEnabledParameterName, |
328
|
|
|
'in' => 'query', |
329
|
|
|
'required' => false, |
330
|
|
|
'type' => 'boolean', |
331
|
|
|
'description' => 'Enable or disable pagination', |
332
|
|
|
]; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Returns items per page parameters for the "get" collection operation. |
337
|
|
|
*/ |
338
|
|
|
protected function getItemsPerPageParameters(): array |
339
|
|
|
{ |
340
|
|
|
return [ |
341
|
|
|
'name' => $this->itemsPerPageParameterName, |
342
|
|
|
'in' => 'query', |
343
|
|
|
'required' => false, |
344
|
|
|
'type' => 'integer', |
345
|
|
|
'description' => 'The number of items per page', |
346
|
|
|
]; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* {@inheritdoc} |
351
|
|
|
*/ |
352
|
|
|
public function hasCacheableSupportsMethod(): bool |
353
|
|
|
{ |
354
|
|
|
return true; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
protected function extractMimeTypes(array $responseFormats): array |
358
|
|
|
{ |
359
|
|
|
$responseMimeTypes = []; |
360
|
|
|
foreach ($responseFormats as $mimeTypes) { |
361
|
|
|
foreach ($mimeTypes as $mimeType) { |
362
|
|
|
$responseMimeTypes[] = $mimeType; |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
return $responseMimeTypes; |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: