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

SchemaBuilder::buildSchema()   B

Complexity

Conditions 10
Paths 49

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 18
c 1
b 0
f 0
nc 49
nop 7
dl 0
loc 31
rs 7.6666

How to fix   Complexity   

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:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace ApiPlatform\Core\Swagger;
6
7
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
8
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
9
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
10
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
11
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
12
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
13
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
14
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
15
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
16
17
/**
18
 * Builds an OpenAPI Schema, an extended subset of the JSON Schema Specification.
19
 *
20
 * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object
21
 * @see https://tools.ietf.org/html/draft-wright-json-schema-01
22
 *
23
 * @author Kévin Dunglas <[email protected]>
24
 */
25
final class SchemaBuilder
26
{
27
    use TypeResolverTrait;
28
29
    private $resourceMetadataFactory;
30
    private $propertyNameCollectionFactory;
31
    private $propertyMetadataFactory;
32
    private $nameConverter;
33
34
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
35
    {
36
        $this->resourceMetadataFactory = $resourceMetadataFactory;
37
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
38
        $this->propertyMetadataFactory = $propertyMetadataFactory;
39
        $this->nameConverter = $nameConverter;
40
    }
41
42
    public function buildSchemaName(string $resourceClass, bool $output = true, ?string $operationType = null, ?string $operationName = null, ?string $format = null, ?array $serializerContext = null, string $version = DocumentationNormalizer::OPENAPI_VERSION): ?string
0 ignored issues
show
Unused Code introduced by
The parameter $version is not used and could be removed. ( Ignorable by Annotation )

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

42
    public function buildSchemaName(string $resourceClass, bool $output = true, ?string $operationType = null, ?string $operationName = null, ?string $format = null, ?array $serializerContext = null, /** @scrutinizer ignore-unused */ string $version = DocumentationNormalizer::OPENAPI_VERSION): ?string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $format is not used and could be removed. ( Ignorable by Annotation )

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

42
    public function buildSchemaName(string $resourceClass, bool $output = true, ?string $operationType = null, ?string $operationName = null, /** @scrutinizer ignore-unused */ ?string $format = null, ?array $serializerContext = null, string $version = DocumentationNormalizer::OPENAPI_VERSION): ?string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
43
    {
44
        if (null === $metadata = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext)) {
45
            return null;
46
        }
47
        [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;
48
49
        $prefix = $resourceMetadata->getShortName();
50
        if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) {
51
            $prefix .= ':'.md5($inputOrOutputClass);
52
        }
53
54
        /*if (null !== $format) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
55
            $prefix .= ':'.$format;
56
        }*/
57
58
        if (isset($serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME])) {
59
            $name = sprintf('%s-%s', $prefix, $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]);
60
        } else {
61
            $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []);
62
            $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...
63
        }
64
65
        return $name;
66
    }
67
68
    /**
69
     * @throws ResourceClassNotFoundException
70
     */
71
    public function buildSchema(string $resourceClass, bool $output = true, ?string $operationType = null, ?string $operationName = null, ?string $format = null, ?array $serializerContext = null, string $version = DocumentationNormalizer::OPENAPI_VERSION): ?\ArrayObject
72
    {
73
        if (null === $metadata = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext)) {
74
            return null;
75
        }
76
        [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;
77
78
        $schema = ['type' => 'object'];
79
        if (null !== $description = $resourceMetadata->getDescription()) {
80
            $schema['description'] = $description;
81
        }
82
        if (null !== $iri = $resourceMetadata->getIri()) {
83
            $schema['externalDocs'] = ['url' => $iri];
84
        }
85
86
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
87
        foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
88
            $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName);
89
            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...
90
                continue;
91
            }
92
93
            $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

93
            $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...
94
            if ($propertyMetadata->isRequired()) {
95
                $schema['required'][] = $normalizedPropertyName;
96
            }
97
98
            $schema['properties'][$normalizedPropertyName] = $this->buildPropertySchema($propertyMetadata, $serializerContext, $version);
99
        }
100
101
        return new \ArrayObject($schema);
102
    }
103
104
    private function getMetadata(string $resourceClass, bool $output, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
105
    {
106
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
107
        $attribute = $output ? 'output' : 'input';
108
        if (null === $operationType || null === $operationName) {
109
            $inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $resourceClass]);
110
        } else {
111
            $inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $resourceClass], true);
112
        }
113
114
        if (null === ($inputOrOutput['class'] ?? null)) {
115
            // input or output disabled
116
            return null;
117
        }
118
119
        return [
120
            $resourceMetadata,
121
            $serializerContext ?? $this->getSerializerContext($resourceMetadata, $output, $operationType, $operationName),
122
            $inputOrOutput['class']
123
        ];
124
    }
125
126
    private function buildPropertySchema(PropertyMetadata $propertyMetadata, array $serializerContext, string $version): \ArrayObject
127
    {
128
        $propertySchema = $propertyMetadata->getAttributes()[$version === DocumentationNormalizer::OPENAPI_VERSION ? 'openapi_context' : 'swagger_context'] ?? [];
129
130
        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...
131
            $propertySchema['readOnly'] = true;
132
        }
133
134
        if (null !== $description = $propertyMetadata->getDescription()) {
135
            $propertySchema['description'] = $description;
136
        }
137
138
        if (null !== $iri = $propertyMetadata->getIri()) {
139
            $schema['externalDocs'] = ['url' => $iri];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$schema was never initialized. Although not strictly required by PHP, it is generally a good practice to add $schema = array(); before regardless.
Loading history...
140
        }
141
142
        if (null === $type = $propertyMetadata->getType()) {
143
            return $propertySchema;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $propertySchema could return the type array which is incompatible with the type-hinted return ArrayObject. Consider adding an additional type-check to rule them out.
Loading history...
144
        }
145
146
        $isCollection = $type->isCollection();
147
        if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
148
            $builtinType = 'string';
149
            $className = null;
150
        } else {
151
            $builtinType = $valueType->getBuiltinType();
152
            $className = $valueType->getClassName();
153
        }
154
155
        $valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $serializerContext, $version);
156
157
        return new \ArrayObject($propertySchema + $valueSchema);
158
    }
159
160
    private function getSerializerContext(ResourceMetadata $resourceMetadata, bool $output, ?string $operationType, ?string $operationName): array
161
    {
162
        $attribute = $output ? 'normalization_context' : 'denormalization_context';
163
164
        if (null === $operationType || null === $operationName) {
165
            return $resourceMetadata->getAttribute($attribute, []);
166
        }
167
168
        return $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true);
169
    }
170
}
171