OpenApiSchemaGenerator   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 176
c 1
b 0
f 1
dl 0
loc 395
rs 3.2
wmc 65

12 Methods

Rating   Name   Duplication   Size   Complexity  
A filterObjectAccess() 0 11 3
A translateType() 0 10 5
A convertTypesToSchema() 0 11 3
A defineSchemaForResource() 0 6 1
A getCacheKey() 0 3 1
A createSchema() 0 3 1
A __construct() 0 10 1
D createSchemaRecursive() 0 73 19
A runCallbacks() 0 24 6
C convertTypeToSchema() 0 36 13
A defineSchemaForPolymorphicObject() 0 34 4
B convertTypeArrayToSchema() 0 42 8

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
363
            $propertySchema->items = null;
364
         }
365
366
        return $propertySchema;
367
    }
368
369
    /**
370
     * Define an OpenAPI discriminator spec for an interface or base class that have a discriminator column.
371
     *
372
     * @param string $resourceInterface
373
     * @param string $discriminatorColumn
374
     * @param array $subclasses
375
     * @param string $operation
376
     * @param string[] $groups
377
     * @return Schema
378
     */
379
    public function defineSchemaForPolymorphicObject(
380
        string $resourceInterface,
381
        string $discriminatorColumn,
382
        array $subclasses,
383
        string $operation = 'get',
384
        array $groups = ['get', 'read']
385
    ): Schema {
386
        $cacheKey = $this->getCacheKey($resourceInterface, $operation, $groups);
387
        /** @var Schema[] $subschemas */
388
        $subschemas = [];
389
        $discriminatorMapping = [];
390
        foreach ($subclasses as $keyValue => $subclass) {
391
            $subschemas[$subclass] = $discriminatorMapping[$keyValue] = $this->createSchema($subclass, $operation, $groups);
392
            $properties = $subschemas[$subclass]->properties;
393
            if (isset($properties[$discriminatorColumn])) {
394
                $properties[$discriminatorColumn]->default = $keyValue;
395
                $properties[$discriminatorColumn]->example = $keyValue;
396
            } else {
397
                $properties[$discriminatorColumn] = SchemaFactory::createStringSchema(null, $keyValue);
398
            }
399
            $subschemas[$subclass]->properties = $properties;
400
        }
401
        $this->alreadyDefined[$cacheKey . ',0'] = new Schema([
402
            'type' => 'object',
403
            'properties' => [
404
                $discriminatorColumn => SchemaFactory::createStringSchema(),
405
            ],
406
            'oneOf' => array_values($subschemas),
407
            'discriminator' => new Discriminator($discriminatorColumn, $discriminatorMapping)
408
        ]);
409
        for ($i = 1; $i < self::MAX_RECURSION; $i++) {
410
            $this->alreadyDefined[$cacheKey . ',' . $i] = $this->alreadyDefined[$cacheKey . ',0'];
411
        }
412
        return $this->alreadyDefined[$cacheKey . ',0'];
413
    }
414
}
415