SchemaFactory::buildSchema()   F
last analyzed

Complexity

Conditions 29
Paths 4951

Size

Total Lines 76
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 29
eloc 44
c 2
b 0
f 0
nc 4951
nop 8
dl 0
loc 76
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', string $type = Schema::TYPE_OUTPUT, ?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, $type, $operationType, $operationName, $serializerContext)) {
69
            return $schema;
70
        }
71
        [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;
72
73
        $version = $schema->getVersion();
74
        $definitionName = $this->buildDefinitionName($resourceClass, $format, $type, $operationType, $operationName, $serializerContext);
75
76
        if (null === $operationType || null === $operationName) {
77
            $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
78
        } else {
79
            $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method');
80
        }
81
82
        if (Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) {
83
            return $schema;
84
        }
85
86
        if (!isset($schema['$ref']) && !isset($schema['type'])) {
87
            $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
88
89
            $method = null !== $operationType && null !== $operationName ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET') : 'GET';
90
            if ($forceCollection || (OperationType::COLLECTION === $operationType && 'POST' !== $method)) {
91
                $schema['type'] = 'array';
92
                $schema['items'] = ['$ref' => $ref];
93
            } else {
94
                $schema['$ref'] = $ref;
95
            }
96
        }
97
98
        $definitions = $schema->getDefinitions();
99
        if (isset($definitions[$definitionName])) {
100
            // Already computed
101
            return $schema;
102
        }
103
104
        $definition = new \ArrayObject(['type' => 'object']);
105
        $definitions[$definitionName] = $definition;
106
        if (null !== $description = $resourceMetadata->getDescription()) {
107
            $definition['description'] = $description;
108
        }
109
        // see https://github.com/json-schema-org/json-schema-spec/pull/737
110
        if (
111
            Schema::VERSION_SWAGGER !== $version &&
112
            (
113
                (null !== $operationType && null !== $operationName && null !== $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) ||
114
                null !== $resourceMetadata->getAttribute('deprecation_reason', null)
115
            )
116
        ) {
117
            $definition['deprecated'] = true;
118
        }
119
        // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
120
        // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
121
        if (null !== $iri = $resourceMetadata->getIri()) {
122
            $definition['externalDocs'] = ['url' => $iri];
123
        }
124
125
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => (array) $serializerContext[AbstractNormalizer::GROUPS]] : [];
126
        foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
127
            $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName);
128
            if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) {
0 ignored issues
show
Bug Best Practice introduced by Paweł Skotnicki
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...
Bug Best Practice introduced by Paweł Skotnicki
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...
129
                continue;
130
            }
131
132
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;
0 ignored issues
show
Unused Code introduced by Kévin Dunglas
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

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