Passed
Branch 3.3 (6947f9)
by Pieter
03:45
created

OpenApiSchemaGenerator   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 143
c 1
b 0
f 1
dl 0
loc 302
rs 6
wmc 55

10 Methods

Rating   Name   Duplication   Size   Complexity  
A filterObjectAccess() 0 11 3
A translateType() 0 10 5
A convertTypesToSchema() 0 7 2
C convertTypeToSchema() 0 57 16
A defineSchemaForResource() 0 6 1
A getCacheKey() 0 3 1
A createSchema() 0 3 1
A __construct() 0 13 1
D createSchemaRecursive() 0 77 19
A runCallbacks() 0 24 6

How to fix   Complexity   

Complex Class

Complex classes like OpenApiSchemaGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OpenApiSchemaGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace W2w\Lib\Apie\OpenApiSchema;
4
5
use erasys\OpenApi\Spec\v3\Schema;
6
use ReflectionClass;
7
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
8
use Symfony\Component\PropertyInfo\Type;
9
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
10
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
11
use W2w\Lib\Apie\Core\ClassResourceConverter;
12
use W2w\Lib\Apie\PluginInterfaces\DynamicSchemaInterface;
13
use W2w\Lib\ApieObjectAccessNormalizer\ObjectAccess\FilteredObjectAccess;
14
use W2w\Lib\ApieObjectAccessNormalizer\ObjectAccess\ObjectAccessInterface;
15
16
class OpenApiSchemaGenerator extends SchemaGenerator
0 ignored issues
show
Deprecated Code introduced by
The class W2w\Lib\Apie\OpenApiSchema\SchemaGenerator has been deprecated: use OpenApiSchemaGenerator ( Ignorable by Annotation )

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

16
class OpenApiSchemaGenerator extends /** @scrutinizer ignore-deprecated */ SchemaGenerator
Loading history...
17
{
18
    private const MAX_RECURSION = 2;
19
20
    /**
21
     * @var DynamicSchemaInterface[]
22
     */
23
    private $schemaGenerators;
24
25
    /**
26
     * @var Schema[]
27
     */
28
    private $predefined = [];
29
30
    /**
31
     * @var bool[]
32
     */
33
    private $building = [];
34
    /**
35
     * @var ObjectAccessInterface
36
     */
37
    private $objectAccess;
38
39
    /**
40
     * @var NameConverterInterface
41
     */
42
    private $nameConverter;
43
44
    /**
45
     * @var ClassMetadataFactoryInterface
46
     */
47
    private $classMetadataFactory;
48
49
    /**
50
     * @param DynamicSchemaInterface[] $schemaGenerators
51
     * @param ObjectAccessInterface $objectAccess
52
     * @param ClassMetadataFactoryInterface $classMetadataFactory
53
     * @param PropertyInfoExtractor $propertyInfoExtractor
54
     * @param ClassResourceConverter $converter
55
     * @param NameConverterInterface $nameConverter
56
     */
57
    public function __construct(
58
        array $schemaGenerators,
59
        ObjectAccessInterface $objectAccess,
60
        ClassMetadataFactoryInterface $classMetadataFactory,
61
        PropertyInfoExtractor $propertyInfoExtractor,
62
        ClassResourceConverter $converter,
63
        NameConverterInterface $nameConverter
64
    ) {
65
        $this->schemaGenerators = $schemaGenerators;
66
        $this->objectAccess = $objectAccess;
67
        $this->nameConverter = $nameConverter;
68
        $this->classMetadataFactory = $classMetadataFactory;
69
        parent::__construct($classMetadataFactory, $propertyInfoExtractor, $converter, $nameConverter, $schemaGenerators);
70
    }
71
72
    /**
73
     * Define a resource class and Schema manually.
74
     * @param string $resourceClass
75
     * @param Schema $schema
76
     * @return OpenApiSchemaGenerator
77
     */
78
    public function defineSchemaForResource(string $resourceClass, Schema $schema)
79
    {
80
        $this->predefined[$resourceClass] = $schema;
81
        $this->alreadyDefined = [];
82
83
        return $this;
84
    }
85
86
    public function createSchema(string $resourceClass, string $operation, array $groups): Schema
87
    {
88
        return $this->createSchemaRecursive($resourceClass, $operation, $groups, $this->oldRecursion + 1);
89
    }
90
91
    /**
92
     * Creates a unique cache key to be used for already defined schemas for performance reasons.
93
     *
94
     * @param string $resourceClass
95
     * @param string $operation
96
     * @param string[] $groups
97
     * @return string
98
     */
99
    private function getCacheKey(string $resourceClass, string $operation, array $groups)
100
    {
101
        return $resourceClass . ',' . $operation . ',' . implode(', ', $groups);
102
    }
103
104
    /**
105
     * Iterate over a list of callbacks to see if they provide a schema for this resource class.
106
     *
107
     * @param string $cacheKey
108
     * @param string $resourceClass
109
     * @param string $operation
110
     * @param array $groups
111
     * @param int $recursion
112
     *
113
     * @return Schema|null
114
     */
115
    private function runCallbacks(string $cacheKey, string $resourceClass, string $operation, array $groups, int $recursion): ?Schema
116
    {
117
        if (!empty($this->building[$cacheKey])) {
118
            return null;
119
        }
120
        $this->building[$cacheKey] = true;
121
        $oldValue = $this->oldRecursion;
122
        try {
123
            // specifically defined: just call it.
124
            if (isset($this->schemaGenerators[$resourceClass])) {
125
                return $this->schemaGenerators[$resourceClass]($resourceClass, $operation, $groups, $recursion, $this);
126
            }
127
            foreach ($this->schemaGenerators as $classDeclaration => $callable) {
128
                if (is_a($resourceClass, $classDeclaration, true)) {
129
                    $res = $callable($resourceClass, $operation, $groups, $recursion, $this);
130
                    if ($res instanceof Schema) {
131
                        return $res;
132
                    }
133
                }
134
            }
135
            return null;
136
        } finally {
137
            $this->oldRecursion = $oldValue;
138
            unset($this->building[$cacheKey]);
139
        }
140
    }
141
142
    private function createSchemaRecursive(string $resourceClass, string $operation, array $groups, int $recursion = 0): Schema
143
    {
144
        $cacheKey = $this->getCacheKey($resourceClass, $operation, $groups) . ',' . $recursion;
145
        if (isset($this->alreadyDefined[$cacheKey])) {
146
            return $this->alreadyDefined[$cacheKey];
147
        }
148
149
        foreach ($this->predefined as $className => $schema) {
150
            if (is_a($resourceClass, $className, true)) {
151
                $this->alreadyDefined[$cacheKey] = $schema;
152
153
                return $this->alreadyDefined[$cacheKey];
154
            }
155
        }
156
157
        if ($predefinedSchema = $this->runCallbacks($cacheKey, $resourceClass, $operation, $groups, $recursion)) {
158
            return $this->alreadyDefined[$cacheKey] = $predefinedSchema;
159
        }
160
        $refl = new ReflectionClass($resourceClass);
161
        $schema = new Schema([
162
            'type' => 'object',
163
            'properties' => [],
164
            'title' => $refl->getShortName(),
165
            'description' => $refl->getShortName() . ' ' . $operation . ' for groups ' . implode(', ', $groups),
166
        ]);
167
        // if definition is an interface or abstract base class it is possible that it has additional properties.
168
        if ($refl->isAbstract() || $refl->isInterface()) {
169
            $schema->additionalProperties = true;
170
        }
171
        if ($recursion > self::MAX_RECURSION) {
172
            $schema->properties = null;
173
            $schema->additionalProperties = true;
174
            return $this->alreadyDefined[$cacheKey] = $schema;
175
        }
176
        $objectAccess = $this->filterObjectAccess($this->objectAccess, $resourceClass, $groups);
177
        switch ($operation) {
178
            case 'post':
179
                $constructorArgs = $objectAccess->getConstructorArguments($refl);
180
                foreach ($constructorArgs as $key => $type) {
181
                    /** @scrutinizer ignore-call */
182
                    $fieldName = $this->nameConverter->normalize($key, $resourceClass);
183
                    $schema->properties[$fieldName] = $this->convertTypeToSchema($type, $operation, $groups, $recursion);
184
                    $description = $objectAccess->getDescription($refl, $key, false);
185
                    if ($description) {
186
                        $schema->properties[$fieldName]->description = $description;
187
                    }
188
                }
189
                // FALLTHROUGH
190
            case 'put':
191
                $setterFields = $objectAccess->getSetterFields($refl);
192
                foreach ($setterFields as $setterField) {
193
                    /** @scrutinizer ignore-call */
194
                    $fieldName = $this->nameConverter->normalize($setterField, $resourceClass);
195
                    $schema->properties[$fieldName] = $this->convertTypesToSchema($objectAccess->getSetterTypes($refl, $setterField), $operation, $groups, $recursion);
196
                    $description = $objectAccess->getDescription($refl, $setterField, false);
197
                    if ($description) {
198
                        $schema->properties[$fieldName]->description = $description;
199
                    }
200
                }
201
                break;
202
            case 'get':
203
                $getterFields = $objectAccess->getGetterFields($refl);
204
                foreach ($getterFields as $getterField) {
205
                    /** @scrutinizer ignore-call */
206
                    $fieldName = $this->nameConverter->normalize($getterField, $resourceClass);
207
                    $schema->properties[$fieldName] = $this->convertTypesToSchema($objectAccess->getGetterTypes($refl, $getterField), $operation, $groups, $recursion);
208
                    $description = $objectAccess->getDescription($refl, $getterField, true);
209
                    if ($description) {
210
                        $schema->properties[$fieldName]->description = $description;
211
                    }
212
                }
213
                break;
214
        }
215
        if (is_array($schema->properties) && empty($schema->properties)) {
216
            $schema->properties = null;
217
        }
218
        return $this->alreadyDefined[$cacheKey] = $schema;
219
    }
220
221
    private function filterObjectAccess(ObjectAccessInterface $objectAccess, string $className, array $groups): ObjectAccessInterface
222
    {
223
        $allowedAttributes = [];
224
        foreach ($this->classMetadataFactory->getMetadataFor($className)->getAttributesMetadata() as $attributeMetadata) {
225
            $name = $attributeMetadata->getName();
226
            if (array_intersect($attributeMetadata->getGroups(), $groups)) {
227
                $allowedAttributes[] = $name;
228
            }
229
        }
230
231
        return new FilteredObjectAccess($objectAccess, $allowedAttributes);
232
    }
233
234
    private function convertTypesToSchema(array $types, string $operation, array $groups, int $recursion = 0): Schema
235
    {
236
        if (empty($types)) {
237
            return new Schema([]);
238
        }
239
        $type = reset($types);
240
        return $this->convertTypeToSchema($type, $operation, $groups, $recursion);
241
    }
242
243
    /**
244
     * Returns OpenApi property type for scalars.
245
     *
246
     * @param string $type
247
     * @return string
248
     */
249
    private function translateType(string $type): string
250
    {
251
        switch ($type) {
252
            case 'int': return 'integer';
253
            case 'bool': return 'boolean';
254
            case 'float': return 'number';
255
            case 'double': return 'number';
256
        }
257
258
        return $type;
259
    }
260
261
    protected function convertTypeToSchema(?Type $type, string $operation, array $groups, int $recursion): Schema
262
    {
263
        if ($type === null) {
264
            return new Schema([]);
265
        }
266
        if ($type && $type->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT && $type->getClassName() && !$type->isCollection()) {
267
            $this->oldRecursion++;
268
            try {
269
                return $this->createSchemaRecursive($type->getClassName(), $operation, $groups, $recursion + 1);
270
            } finally {
271
                $this->oldRecursion--;
272
            }
273
274
        }
275
        $propertySchema = new Schema([
276
            'type'        => 'string',
277
            'nullable'    => true,
278
        ]);
279
        $propertySchema->type = $this->translateType($type->getBuiltinType());
280
        if (!$type->isNullable()) {
281
            $propertySchema->nullable = false;
282
        }
283
        if ($type->isCollection()) {
284
            $propertySchema->type = 'array';
285
            $propertySchema->items = new Schema([]);
286
            $arrayType = $type->getCollectionValueType();
287
            if ($arrayType) {
288
                if ($arrayType->getClassName()) {
289
                    $this->oldRecursion++;
290
                    try {
291
                        $propertySchema->items = $this->createSchemaRecursive(
292
                            $arrayType->getClassName(),
293
                            $operation,
294
                            $groups,
295
                            $recursion + 1
296
                        );
297
                    } finally {
298
                        $this->oldRecursion--;
299
                    }
300
                } elseif ($arrayType->getBuiltinType()) {
301
                    $schemaType = $this->translateType($arrayType->getBuiltinType());
302
                    $propertySchema->items = new Schema([
303
                        'type' => $schemaType,
304
                        'format' => ($schemaType === 'number') ? $arrayType->getBuiltinType() : null,
305
                    ]);
306
                }
307
            }
308
            return $propertySchema;
309
        }
310
        if ($propertySchema->type === 'number') {
311
            $propertySchema->format = $type->getBuiltinType();
312
        }
313
        $className = $type->getClassName();
314
        if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && $recursion < self::MAX_RECURSION && !is_null($className)) {
315
            return $this->createSchemaRecursive($className, $operation, $groups, $recursion + 1);
316
        }
317
        return $propertySchema;
318
    }
319
}
320