Completed
Push — master ( c5da41...e1d486 )
by Pieter
22s queued 14s
created

OpenApiSchemaGenerator::createSchemaRecursive()   D

Complexity

Conditions 19
Paths 56

Size

Total Lines 77
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 19
eloc 52
c 1
b 0
f 1
nc 56
nop 4
dl 0
loc 77
rs 4.5166

How to fix   Long Method    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
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
    /**
87
     * Creates a Schema for  specific resource class.
88
     *
89
     * @param string $resourceClass
90
     * @param string $operation
91
     * @param array $groups
92
     * @return Schema
93
     */
94
    public function createSchema(string $resourceClass, string $operation, array $groups): Schema
95
    {
96
        return unserialize(serialize($this->createSchemaRecursive($resourceClass, $operation, $groups, $this->oldRecursion + 1)));
97
    }
98
99
    /**
100
     * Creates a unique cache key to be used for already defined schemas for performance reasons.
101
     *
102
     * @param string $resourceClass
103
     * @param string $operation
104
     * @param string[] $groups
105
     * @return string
106
     */
107
    private function getCacheKey(string $resourceClass, string $operation, array $groups)
108
    {
109
        return $resourceClass . ',' . $operation . ',' . implode(', ', $groups);
110
    }
111
112
    /**
113
     * Iterate over a list of callbacks to see if they provide a schema for this resource class.
114
     *
115
     * @param string $cacheKey
116
     * @param string $resourceClass
117
     * @param string $operation
118
     * @param array $groups
119
     * @param int $recursion
120
     *
121
     * @return Schema|null
122
     */
123
    private function runCallbacks(string $cacheKey, string $resourceClass, string $operation, array $groups, int $recursion): ?Schema
124
    {
125
        if (!empty($this->building[$cacheKey])) {
126
            return null;
127
        }
128
        $this->building[$cacheKey] = true;
129
        $oldValue = $this->oldRecursion;
130
        try {
131
            // specifically defined: just call it.
132
            if (isset($this->schemaGenerators[$resourceClass])) {
133
                return $this->schemaGenerators[$resourceClass]($resourceClass, $operation, $groups, $recursion, $this);
134
            }
135
            foreach ($this->schemaGenerators as $classDeclaration => $callable) {
136
                if (is_a($resourceClass, $classDeclaration, true)) {
137
                    $res = $callable($resourceClass, $operation, $groups, $recursion, $this);
138
                    if ($res instanceof Schema) {
139
                        return $res;
140
                    }
141
                }
142
            }
143
            return null;
144
        } finally {
145
            $this->oldRecursion = $oldValue;
146
            unset($this->building[$cacheKey]);
147
        }
148
    }
149
150
    private function createSchemaRecursive(string $resourceClass, string $operation, array $groups, int $recursion = 0): Schema
151
    {
152
        $cacheKey = $this->getCacheKey($resourceClass, $operation, $groups) . ',' . $recursion;
153
        if (isset($this->alreadyDefined[$cacheKey])) {
154
            return $this->alreadyDefined[$cacheKey];
155
        }
156
157
        foreach ($this->predefined as $className => $schema) {
158
            if (is_a($resourceClass, $className, true)) {
159
                $this->alreadyDefined[$cacheKey] = $schema;
160
161
                return $this->alreadyDefined[$cacheKey];
162
            }
163
        }
164
165
        if ($predefinedSchema = $this->runCallbacks($cacheKey, $resourceClass, $operation, $groups, $recursion)) {
166
            return $this->alreadyDefined[$cacheKey] = $predefinedSchema;
167
        }
168
        $refl = new ReflectionClass($resourceClass);
169
        $schema = new Schema([
170
            'type' => 'object',
171
            'properties' => [],
172
            'title' => $refl->getShortName(),
173
            'description' => $refl->getShortName() . ' ' . $operation . ' for groups ' . implode(', ', $groups),
174
        ]);
175
        // if definition is an interface or abstract base class it is possible that it has additional properties.
176
        if ($refl->isAbstract() || $refl->isInterface()) {
177
            $schema->additionalProperties = true;
178
        }
179
        if ($recursion > self::MAX_RECURSION) {
180
            $schema->properties = null;
181
            $schema->additionalProperties = true;
182
            return $this->alreadyDefined[$cacheKey] = $schema;
183
        }
184
        $objectAccess = $this->filterObjectAccess($this->objectAccess, $resourceClass, $groups);
185
        switch ($operation) {
186
            case 'post':
187
                $constructorArgs = $objectAccess->getConstructorArguments($refl);
188
                foreach ($constructorArgs as $key => $type) {
189
                    /** @scrutinizer ignore-call */
190
                    $fieldName = $this->nameConverter->normalize($key, $resourceClass);
191
                    $schema->properties[$fieldName] = $this->convertTypeToSchema($type, $operation, $groups, $recursion);
192
                    $description = $objectAccess->getDescription($refl, $key, false);
193
                    if ($description) {
194
                        $schema->properties[$fieldName]->description = $description;
195
                    }
196
                }
197
                // FALLTHROUGH
198
            case 'put':
199
                $setterFields = $objectAccess->getSetterFields($refl);
200
                foreach ($setterFields as $setterField) {
201
                    /** @scrutinizer ignore-call */
202
                    $fieldName = $this->nameConverter->normalize($setterField, $resourceClass);
203
                    $schema->properties[$fieldName] = $this->convertTypesToSchema($objectAccess->getSetterTypes($refl, $setterField), $operation, $groups, $recursion);
204
                    $description = $objectAccess->getDescription($refl, $setterField, false);
205
                    if ($description) {
206
                        $schema->properties[$fieldName]->description = $description;
207
                    }
208
                }
209
                break;
210
            case 'get':
211
                $getterFields = $objectAccess->getGetterFields($refl);
212
                foreach ($getterFields as $getterField) {
213
                    /** @scrutinizer ignore-call */
214
                    $fieldName = $this->nameConverter->normalize($getterField, $resourceClass);
215
                    $schema->properties[$fieldName] = $this->convertTypesToSchema($objectAccess->getGetterTypes($refl, $getterField), $operation, $groups, $recursion);
216
                    $description = $objectAccess->getDescription($refl, $getterField, true);
217
                    if ($description) {
218
                        $schema->properties[$fieldName]->description = $description;
219
                    }
220
                }
221
                break;
222
        }
223
        if (is_array($schema->properties) && empty($schema->properties)) {
224
            $schema->properties = null;
225
        }
226
        return $this->alreadyDefined[$cacheKey] = $schema;
227
    }
228
229
    private function filterObjectAccess(ObjectAccessInterface $objectAccess, string $className, array $groups): ObjectAccessInterface
230
    {
231
        $allowedAttributes = [];
232
        foreach ($this->classMetadataFactory->getMetadataFor($className)->getAttributesMetadata() as $attributeMetadata) {
233
            $name = $attributeMetadata->getName();
234
            if (array_intersect($attributeMetadata->getGroups(), $groups)) {
235
                $allowedAttributes[] = $name;
236
            }
237
        }
238
239
        return new FilteredObjectAccess($objectAccess, $allowedAttributes);
240
    }
241
242
    private function convertTypesToSchema(array $types, string $operation, array $groups, int $recursion = 0): Schema
243
    {
244
        if (empty($types)) {
245
            return new Schema([]);
246
        }
247
        $type = reset($types);
248
        // this is only because this serializer does not do a deep populate.
249
        if ($operation === 'put') {
250
            $operation = 'post';
251
        }
252
        return $this->convertTypeToSchema($type, $operation, $groups, $recursion);
253
    }
254
255
    /**
256
     * Returns OpenApi property type for scalars.
257
     *
258
     * @param string $type
259
     * @return string
260
     */
261
    private function translateType(string $type): string
262
    {
263
        switch ($type) {
264
            case 'int': return 'integer';
265
            case 'bool': return 'boolean';
266
            case 'float': return 'number';
267
            case 'double': return 'number';
268
        }
269
270
        return $type;
271
    }
272
273
    protected function convertTypeToSchema(?Type $type, string $operation, array $groups, int $recursion): Schema
274
    {
275
        if ($type === null) {
276
            return new Schema([]);
277
        }
278
        if ($type && $type->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT && $type->getClassName() && !$type->isCollection()) {
279
            $this->oldRecursion++;
280
            try {
281
                return $this->createSchemaRecursive($type->getClassName(), $operation, $groups, $recursion + 1);
282
            } finally {
283
                $this->oldRecursion--;
284
            }
285
286
        }
287
        $propertySchema = new Schema([
288
            'type'        => 'string',
289
            'nullable'    => true,
290
        ]);
291
        $propertySchema->type = $this->translateType($type->getBuiltinType());
292
        if ($propertySchema->type === 'array') {
293
            $propertySchema->items = new Schema([]);
294
        }
295
        if (!$type->isNullable()) {
296
            $propertySchema->nullable = false;
297
        }
298
        if ($type->isCollection()) {
299
            $propertySchema->type = 'array';
300
            $propertySchema->items = new Schema([]);
301
            $arrayType = $type->getCollectionValueType();
302
            if ($arrayType) {
303
                if ($arrayType->getClassName()) {
304
                    $this->oldRecursion++;
305
                    try {
306
                        $propertySchema->items = $this->createSchemaRecursive(
307
                            $arrayType->getClassName(),
308
                            $operation,
309
                            $groups,
310
                            $recursion + 1
311
                        );
312
                    } finally {
313
                        $this->oldRecursion--;
314
                    }
315
                } elseif ($arrayType->getBuiltinType()) {
316
                    $schemaType = $this->translateType($arrayType->getBuiltinType());
317
                    $propertySchema->items = new Schema([
318
                        'type' => $schemaType,
319
                        'format' => ($schemaType === 'number') ? $arrayType->getBuiltinType() : null,
320
                    ]);
321
                    //array[] typehint...
322
                    if ($schemaType === 'array') {
323
                        $propertySchema->items->items = new Schema([]);
324
                    }
325
                }
326
            }
327
            return $propertySchema;
328
        }
329
        if ($propertySchema->type === 'number') {
330
            $propertySchema->format = $type->getBuiltinType();
331
        }
332
        $className = $type->getClassName();
333
        if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && $recursion < self::MAX_RECURSION && !is_null($className)) {
334
            return $this->createSchemaRecursive($className, $operation, $groups, $recursion + 1);
335
        }
336
        return $propertySchema;
337
    }
338
}
339