Passed
Pull Request — master (#2983)
by Kévin
04:30
created

SchemaFactory::buildSchema()   F

Complexity

Conditions 24
Paths 1650

Size

Total Lines 65
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 24
eloc 38
c 1
b 0
f 0
nc 1650
nop 8
dl 0
loc 65
rs 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\JsonSchema;
15
16
use ApiPlatform\Core\Api\OperationType;
17
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
20
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
21
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
22
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
23
use Symfony\Component\PropertyInfo\Type;
24
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
25
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
26
27
/**
28
 * {@inheritdoc}
29
 *
30
 * @experimental
31
 *
32
 * @author Kévin Dunglas <[email protected]>
33
 */
34
final class SchemaFactory implements SchemaFactoryInterface
35
{
36
    private $resourceMetadataFactory;
37
    private $propertyNameCollectionFactory;
38
    private $propertyMetadataFactory;
39
    private $typeFactory;
40
    private $nameConverter;
41
    private $distinctFormats = [];
42
43
    public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
44
    {
45
        $this->resourceMetadataFactory = $resourceMetadataFactory;
46
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
47
        $this->propertyMetadataFactory = $propertyMetadataFactory;
48
        $this->nameConverter = $nameConverter;
49
        $this->typeFactory = $typeFactory;
50
    }
51
52
    /**
53
     * When added to the list, the given format will lead to the creation of a new definition.
54
     *
55
     * @internal
56
     */
57
    public function addDistinctFormat(string $format): void
58
    {
59
        $this->distinctFormats[$format] = true;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function buildSchema(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
66
    {
67
        $schema = $schema ?? new Schema();
68
        if (null === $metadata = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext)) {
69
            return $schema;
70
        }
71
        [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;
72
73
        $version = $schema->getVersion();
74
        $definitionName = $this->buildDefinitionName($resourceClass, $format, $output, $operationType, $operationName, $serializerContext);
75
        if (!isset($schema['$ref']) && !isset($schema['type'])) {
76
            $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
77
78
            $method = null !== $operationType && null !== $operationName ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET') : 'GET';
79
            if ($forceCollection || (OperationType::COLLECTION === $operationType && 'POST' !== $method)) {
80
                $schema['type'] = 'array';
81
                $schema['items'] = ['$ref' => $ref];
82
            } else {
83
                $schema['$ref'] = $ref;
84
            }
85
        }
86
87
        $definitions = $schema->getDefinitions();
88
        if (isset($definitions[$definitionName])) {
89
            // Already computed
90
            return $schema;
91
        }
92
93
        $definition = new \ArrayObject(['type' => 'object']);
94
        $definitions[$definitionName] = $definition;
95
        if (null !== $description = $resourceMetadata->getDescription()) {
96
            $definition['description'] = $description;
97
        }
98
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
99
        if (
100
            Schema::VERSION_SWAGGER !== $version &&
101
            (
102
                (null !== $operationType && null !== $operationName && null !== $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) ||
103
                null !== $resourceMetadata->getAttribute('deprecation_reason', null)
104
            )
105
        ) {
106
            $definition['deprecated'] = true;
107
        }
108
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
109
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
110
        if (null !== $iri = $resourceMetadata->getIri()) {
111
            $definition['externalDocs'] = ['url' => $iri];
112
        }
113
114
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
115
        foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
116
            $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName);
117
            if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyMetadata->isReadable() 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...
Bug Best Practice introduced by
The expression $propertyMetadata->isWritable() 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...
118
                continue;
119
            }
120
121
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $inputOrOutputClass. ( Ignorable by Annotation )

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

121
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->/** @scrutinizer ignore-call */ normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
122
            if ($propertyMetadata->isRequired()) {
123
                $definition['required'][] = $normalizedPropertyName;
124
            }
125
126
            $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format);
127
        }
128
129
        return $schema;
130
    }
131
132
    private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, PropertyMetadata $propertyMetadata, array $serializerContext, string $format): void
133
    {
134
        $version = $schema->getVersion();
135
        $swagger = false;
136
        switch ($version) {
137
            case Schema::VERSION_SWAGGER:
138
                $swagger = true;
139
                $basePropertySchemaAttribute = 'swagger_context';
140
                break;
141
            case Schema::VERSION_OPENAPI:
142
                $basePropertySchemaAttribute = 'openapi_context';
143
                break;
144
            default:
145
                $basePropertySchemaAttribute = 'json_schema_context';
146
        }
147
148
        $propertySchema = $propertyMetadata->getAttributes()[$basePropertySchemaAttribute] ?? [];
149
        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...
150
            $propertySchema['readOnly'] = true;
151
        }
152
        if (!$swagger && false === $propertyMetadata->isReadable()) {
153
            $propertySchema['writeOnly'] = true;
154
        }
155
        if (null !== $description = $propertyMetadata->getDescription()) {
156
            $propertySchema['description'] = $description;
157
        }
158
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
159
        if (!$swagger && null !== $propertyMetadata->getAttribute('deprecation_reason')) {
160
            $propertySchema['deprecated'] = true;
161
        }
162
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
163
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
164
        if (null !== $iri = $propertyMetadata->getIri()) {
165
            $propertySchema['externalDocs'] = ['url' => $iri];
166
        }
167
168
        $valueSchema = [];
169
        if (null !== $type = $propertyMetadata->getType()) {
170
            $isCollection = $type->isCollection();
171
            if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
172
                $builtinType = 'string';
173
                $className = null;
174
            } else {
175
                $builtinType = $valueType->getBuiltinType();
176
                $className = $valueType->getClassName();
177
            }
178
179
            $valueSchema = $this->typeFactory->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection), $format, $propertyMetadata->isReadableLink(), $serializerContext, $schema);
180
        }
181
182
        $propertySchema = new \ArrayObject($propertySchema + $valueSchema);
183
        if (DocumentationNormalizer::OPENAPI_VERSION === $version) {
184
            $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
185
186
            return;
187
        }
188
189
        $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
190
    }
191
192
    private function buildDefinitionName(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
193
    {
194
        [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext);
195
196
        $prefix = $resourceMetadata->getShortName();
197
        if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) {
198
            $prefix .= ':'.md5($inputOrOutputClass);
199
        }
200
201
        if (isset($this->distinctFormats[$format])) {
202
            // JSON is the default, and so isn't included in the definition name
203
            $prefix .= ':'.$format;
204
        }
205
206
        if (isset($serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME])) {
207
            $name = sprintf('%s-%s', $prefix, $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]);
208
        } else {
209
            $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []);
210
            $name = $groups ? sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix;
0 ignored issues
show
introduced by
$groups is an empty array, thus is always false.
Loading history...
211
        }
212
213
        return $name;
214
    }
215
216
    private function getMetadata(string $resourceClass, bool $output, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
217
    {
218
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
219
        $attribute = $output ? 'output' : 'input';
220
        if (null === $operationType || null === $operationName) {
221
            $inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $resourceClass]);
222
        } else {
223
            $inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $resourceClass], true);
224
        }
225
226
        if (null === ($inputOrOutput['class'] ?? null)) {
227
            // input or output disabled
228
            return null;
229
        }
230
231
        return [
232
            $resourceMetadata,
233
            $serializerContext ?? $this->getSerializerContext($resourceMetadata, $output, $operationType, $operationName),
234
            $inputOrOutput['class'],
235
        ];
236
    }
237
238
    private function getSerializerContext(ResourceMetadata $resourceMetadata, bool $output, ?string $operationType, ?string $operationName): array
239
    {
240
        $attribute = $output ? 'normalization_context' : 'denormalization_context';
241
242
        if (null === $operationType || null === $operationName) {
243
            return $resourceMetadata->getAttribute($attribute, []);
244
        }
245
246
        return $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true);
247
    }
248
}
249