Passed
Branch 4.0 (6a2b4f)
by Pieter
02:25
created

OpenApiSchemaGenerator   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 278
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 127
c 1
b 0
f 1
dl 0
loc 278
rs 8.4
wmc 50

10 Methods

Rating   Name   Duplication   Size   Complexity  
A defineSchemaForResource() 0 6 1
A getCacheKey() 0 3 1
A createSchema() 0 3 1
A __construct() 0 10 1
A runCallbacks() 0 22 6
A filterObjectAccess() 0 11 3
A translateType() 0 10 5
A convertTypesToSchema() 0 7 2
C convertTypeToSchema() 0 47 15
C createSchemaRecursive() 0 68 15

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