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

getPathOperation()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 28
rs 8.4444
c 0
b 0
f 0
cc 8
nc 32
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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