Passed
Push — master ( 9a5b6f...aff44c )
by Kévin
05:40 queued 02:53
created

getItemsPerPageParameters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ApiPlatform\Core\OpenApi...DocumentationNormalizer. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/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 before OtherDir/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:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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] ?? []));
0 ignored issues
show
Bug introduced by
It seems like $resourceMetadata->getShortName() can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\OpenApi...zer::getDefinitionKey() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
            $definitionKey = $this->getDefinitionKey(/** @scrutinizer ignore-type */ $resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
Loading history...
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()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyMetadata->isInitializable() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\OpenApi...r::updateGetOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
                return $this->updateGetOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
268
            case 'POST':
269
                return $this->updatePostOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\OpenApi...::updatePostOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

269
                return $this->updatePostOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\OpenApi...r::updatePutOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

274
                return $this->updatePutOperation($pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
275
            case 'DELETE':
276
                return $this->updateDeleteOperation($pathOperation, $resourceShortName);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\OpenApi...updateDeleteOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

276
                return $this->updateDeleteOperation($pathOperation, /** @scrutinizer ignore-type */ $resourceShortName);
Loading history...
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