Passed
Branch 3.3 (fc5b52)
by Pieter
02:25
created

OpenApiSchemaGenerator::createSchemaRecursive()   C

Complexity

Conditions 15
Paths 18

Size

Total Lines 65
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 65
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\ClassMetadataFactory;
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
    /**
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 ClassMetadataFactory
49
     */
50
    private $classMetadataFactory;
51
52
    /**
53
     * @param DynamicSchemaInterface $schemaGenerators
54
     */
55
    public function __construct(
56
        array $schemaGenerators,
57
        ObjectAccessInterface $objectAccess,
58
        ClassMetadataFactory $classMetadataFactory,
59
        PropertyInfoExtractor $propertyInfoExtractor,
60
        ClassResourceConverter $converter,
61
        NameConverterInterface $nameConverter
62
    ) {
63
        $this->schemaGenerators = $schemaGenerators;
64
        $this->objectAccess = $objectAccess;
65
        $this->nameConverter = $nameConverter;
66
        $this->classMetadataFactory = $classMetadataFactory;
67
        parent::__construct($classMetadataFactory, $propertyInfoExtractor, $converter, $nameConverter, $schemaGenerators);
68
    }
69
70
    /**
71
     * Define a resource class and Schema manually.
72
     * @param string $resourceClass
73
     * @param Schema $schema
74
     * @return SchemaGenerator
75
     */
76
    public function defineSchemaForResource(string $resourceClass, Schema $schema): SchemaGenerator
77
    {
78
        $this->predefined[$resourceClass] = $schema;
79
        $this->alreadyDefined = [];
80
81
        return $this;
82
    }
83
84
    public function createSchema(string $resourceClass, string $operation, array $groups): Schema
85
    {
86
        return $this->createSchemaRecursive($resourceClass, $operation, $groups);
87
    }
88
89
    /**
90
     * Creates a unique cache key to be used for already defined schemas for performance reasons.
91
     *
92
     * @param string $resourceClass
93
     * @param string $operation
94
     * @param string[] $groups
95
     * @return string
96
     */
97
    private function getCacheKey(string $resourceClass, string $operation, array $groups)
98
    {
99
        return $resourceClass . ',' . $operation . ',' . implode(', ', $groups);
100
    }
101
102
    /**
103
     * Iterate over a list of callbacks to see if they provide a schema for this resource class.
104
     *
105
     * @param string $cacheKey
106
     * @param string $resourceClass
107
     * @param string $operation
108
     * @param array $groups
109
     * @param int $recursion
110
     *
111
     * @return Schema|null
112
     */
113
    private function runCallbacks(string $cacheKey, string $resourceClass, string $operation, array $groups, int $recursion): ?Schema
114
    {
115
        if (!empty($this->building[$cacheKey])) {
116
            return null;
117
        }
118
        $this->building[$cacheKey] = true;
119
        try {
120
            // specifically defined: just call it.
121
            if (isset($this->schemaGenerators[$resourceClass])) {
122
                return $this->schemaGenerators[$resourceClass]($resourceClass, $operation, $groups, $recursion, $this);
123
            }
124
            foreach ($this->schemaGenerators as $classDeclaration => $callable) {
125
                if (is_a($resourceClass, $classDeclaration, true)) {
126
                    $res = $callable($resourceClass, $operation, $groups, $recursion, $this);
127
                    if ($res instanceof Schema) {
128
                        return $res;
129
                    }
130
                }
131
            }
132
            return null;
133
        } finally {
134
            unset($this->building[$cacheKey]);
135
        }
136
    }
137
138
    private function createSchemaRecursive(string $resourceClass, string $operation, array $groups, int $recursion = 0): Schema
139
    {
140
        $cacheKey = $this->getCacheKey($resourceClass, $operation, $groups) . ',' . $recursion;
141
        if (isset($this->alreadyDefined[$cacheKey])) {
142
            return $this->alreadyDefined[$cacheKey];
143
        }
144
145
        foreach ($this->predefined as $className => $schema) {
146
            if (is_a($resourceClass, $className, true)) {
147
                $this->alreadyDefined[$cacheKey] = $schema;
148
149
                return $this->alreadyDefined[$cacheKey];
150
            }
151
        }
152
153
        if ($predefinedSchema = $this->runCallbacks($cacheKey, $resourceClass, $operation, $groups, $recursion)) {
154
            return $this->alreadyDefined[$cacheKey] = $predefinedSchema;
155
        }
156
        $refl = new ReflectionClass($resourceClass);
157
        $schema = new Schema([
158
            'type' => 'object',
159
            'properties' => [],
160
            'title' => $refl->getShortName(),
161
            'description' => $refl->getShortName() . ' ' . $operation . ' for groups ' . implode(', ', $groups),
162
        ]);
163
        if ($recursion > 3) {
164
            return $this->alreadyDefined[$cacheKey] = $schema;
165
        }
166
        $objectAccess = $this->filterObjectAccess($this->objectAccess, $resourceClass, $groups);
167
        switch ($operation) {
168
            case 'post':
169
                $constructorArgs = $objectAccess->getConstructorArguments($refl);
170
                foreach ($constructorArgs as $key => $type) {
171
                    $fieldName = $this->nameConverter->normalize($key);
172
                    $schema->properties[$fieldName] = $this->convertTypeToSchema($type, $operation, $groups, $recursion + 1);
173
                    $description = $objectAccess->getDescription($refl, $fieldName, false);
174
                    if ($description) {
175
                        $schema->properties[$fieldName]->description = $description;
176
                    }
177
                }
178
                // FALLTHROUGH
179
            case 'put':
180
                $setterFields = $objectAccess->getSetterFields($refl);
181
                foreach ($setterFields as $setterField) {
182
                    $fieldName = $this->nameConverter->normalize($setterField);
183
                    $schema->properties[$fieldName] = $this->convertTypesToSchema($objectAccess->getSetterTypes($refl, $setterField), $operation, $groups, $recursion);
184
                    $description = $objectAccess->getDescription($refl, $fieldName, false);
185
                    if ($description) {
186
                        $schema->properties[$fieldName]->description = $description;
187
                    }
188
                }
189
                break;
190
            case 'get':
191
                $getterFields = $objectAccess->getGetterFields($refl);
192
                foreach ($getterFields as $getterField) {
193
                    $fieldName = $this->nameConverter->normalize($getterField);
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
211
            if (array_intersect($attributeMetadata->getGroups(), $groups)) {
212
                $allowedAttributes[] = $name;
213
            }
214
        }
215
216
        return new FilteredObjectAccess($objectAccess, $allowedAttributes);
217
    }
218
219
    private function convertTypesToSchema(array $types, string $operation, array $groups, int $recursion = 0): Schema
220
    {
221
        if (empty($types)) {
222
            return new Schema(['type' => 'object', 'additionalProperties' => true]);
223
        }
224
        $type = reset($types);
225
        return $this->convertTypeToSchema($type, $operation, $groups, $recursion + 1);
226
    }
227
228
    protected function convertTypeToSchema(?Type $type, string $operation, array $groups, int $recursion): Schema
229
    {
230
        if ($type && $type->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT && $type->getClassName()) {
231
            return $this->createSchemaRecursive($type->getClassName(), $operation, $groups, $recursion);
232
        }
233
        return parent::convertTypeToSchema($type, $operation, $groups, $recursion);
234
    }
235
}
236