Passed
Branch 3.3 (0d0269)
by Pieter
02:33
created

OpenApiSchemaGenerator::createSchemaRecursive()   C

Complexity

Conditions 15
Paths 18

Size

Total Lines 68
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 15
eloc 46
c 1
b 0
f 1
nc 18
nop 4
dl 0
loc 68
rs 5.9166

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