Passed
Push — master ( a7ca13...505408 )
by Pieter
03:55 queued 46s
created

defineSchemaForPolymorphicObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 5
dl 0
loc 17
rs 10
c 0
b 0
f 0
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 PhpValueObjects\AbstractStringValueObject;
0 ignored issues
show
Bug introduced by
The type PhpValueObjects\AbstractStringValueObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use ReflectionClass;
9
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
10
use Symfony\Component\PropertyInfo\Type;
11
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
12
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
13
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
14
use W2w\Lib\Apie\ClassResourceConverter;
15
use W2w\Lib\Apie\ValueObjects\ValueObjectInterface;
16
17
/**
18
 * Class that uses symfony/property-info and reflection to create a Schema instance of a class.
19
 */
20
class SchemaGenerator
21
{
22
    /**
23
     * @var ClassMetadataFactory
24
     */
25
    private $classMetadataFactory;
26
27
    /**
28
     * @var PropertyInfoExtractor
29
     */
30
    private $propertyInfoExtractor;
31
32
    /**
33
     * @var ClassResourceConverter
34
     */
35
    private $converter;
36
37
    /**
38
     * @var NameConverterInterface
39
     */
40
    private $nameConverter;
41
42
    /**
43
     * @var Schema[]
44
     */
45
    private $alreadyDefined = [];
46
47
    /**
48
     * @var Schema[]
49
     */
50
    private $predefined = [];
51
52
    /**
53
     * @param ClassMetadataFactory $classMetadataFactory
54
     * @param PropertyInfoExtractor $propertyInfoExtractor
55
     * @param ClassResourceConverter $converter
56
     * @param NameConverterInterface $nameConverter
57
     */
58
    public function __construct(
59
        ClassMetadataFactory $classMetadataFactory,
60
        PropertyInfoExtractor $propertyInfoExtractor,
61
        ClassResourceConverter $converter,
62
        NameConverterInterface $nameConverter
63
    ) {
64
        $this->classMetadataFactory = $classMetadataFactory;
65
        $this->propertyInfoExtractor = $propertyInfoExtractor;
66
        $this->converter = $converter;
67
        $this->nameConverter = $nameConverter;
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): self
77
    {
78
        $this->predefined[$resourceClass] = $schema;
79
        $this->alreadyDefined = [];
80
81
        return $this;
82
    }
83
84
    /**
85
     * Define an OpenAPI discriminator spec for an interface or base class that have a discriminator column.
86
     *
87
     * @param string $resourceInterface
88
     * @param string $discriminatorColumn
89
     * @param array $subclasses
90
     * @param string $operation
91
     * @param string[] $groups
92
     * @return Schema
93
     */
94
    public function defineSchemaForPolymorphicObject(
95
        string $resourceInterface,
96
        string $discriminatorColumn,
97
        array $subclasses,
98
        string $operation = 'get',
99
        array $groups = []
100
    ): Schema {
101
        $cacheKey = $this->getCacheKey($resourceInterface, $operation, $groups) . ',0';
102
        $subschemas = [];
103
        foreach ($subclasses as $keyValue => $subclass) {
104
            $subschemas[$keyValue] = $this->createSchema($subclass, $operation, $groups);
105
        }
106
        $this->alreadyDefined[$cacheKey] = new Schema([
107
            'oneOf' => array_values($subschemas),
108
            'discriminator' => new Discriminator($discriminatorColumn, $subschemas)
109
        ]);
110
        return $this->alreadyDefined[$cacheKey];
111
    }
112
113
    /**
114
     * Creates a schema recursively.
115
     *
116
     * @param string $resourceClass
117
     * @param string $operation
118
     * @param string[] $groups
119
     * @param int $recursion
120
     * @return Schema
121
     */
122
    private function createSchemaRecursive(string $resourceClass, string $operation, array $groups, int $recursion = 0): Schema
123
    {
124
        $metaData = $this->classMetadataFactory->getMetadataFor($resourceClass);
125
        $cacheKey = $this->getCacheKey($resourceClass, $operation, $groups) . ',' . $recursion;
126
        if (isset($this->alreadyDefined[$cacheKey])) {
127
            return $this->alreadyDefined[$cacheKey];
128
        }
129
130
        foreach ($this->predefined as $className => $schema) {
131
            if (is_a($resourceClass, $className, true)) {
132
                $this->alreadyDefined[$cacheKey] = $schema;
133
134
                return $this->alreadyDefined[$cacheKey];
135
            }
136
        }
137
138
        if (is_a($resourceClass, ValueObjectInterface::class, true)) {
139
            return $this->alreadyDefined[$cacheKey] = $resourceClass::toSchema();
140
        }
141
142
        $name = $this->converter->normalize($resourceClass);
143
        $this->alreadyDefined[$cacheKey] = $schema = new Schema([
144
            'title'       => $name,
145
            'description' => $name . ' ' . $operation,
146
            'type'        => 'object',
147
        ]);
148
        if ($groups) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groups of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
149
            $schema->description .= ' for groups ' . implode(', ', $groups);
150
        }
151
        $properties = [];
152
        foreach ($metaData->getAttributesMetadata() as $attributeMetadata) {
153
            $name = $attributeMetadata->getSerializedName() ?? $this->nameConverter->normalize($attributeMetadata->getName());
154
            if (!$this->isPropertyApplicable($resourceClass, $attributeMetadata, $operation, $groups)) {
155
                continue;
156
            }
157
            $properties[$name] = new Schema([
158
                'type'        => 'string',
159
                'nullable'    => true,
160
            ]);
161
            $types = $this->propertyInfoExtractor->getTypes($resourceClass, $attributeMetadata->getName()) ?? [];
162
            $type = reset($types);
163
            if ($type instanceof Type) {
164
                $properties[$name] = $this->convertTypeToSchema($type, $operation, $groups, $recursion);
165
            }
166
            if (!$properties[$name]->description) {
167
                $properties[$name]->description = $this->propertyInfoExtractor->getShortDescription(
168
                    $resourceClass,
169
                    $attributeMetadata->getName()
170
                );
171
            }
172
        }
173
        $schema->properties = $properties;
174
        $this->alreadyDefined[$cacheKey] = $schema;
175
176
        return $schema;
177
    }
178
179
    /**
180
     * Convert Type into Schema.
181
     *
182
     * @param Type $type
183
     * @param string $operation
184
     * @param string[] $groups
185
     * @param int $recursion
186
     * @return Schema
187
     */
188
    private function convertTypeToSchema(Type $type, string $operation, array $groups, int $recursion): Schema
189
    {
190
        $propertySchema = new Schema([
191
            'type'        => 'string',
192
            'nullable'    => true,
193
        ]);
194
        $propertySchema->type = $this->translateType($type->getBuiltinType());
195
        if (!$type->isNullable()) {
196
            $propertySchema->nullable = false;
197
        }
198
        if ($type->isCollection()) {
199
            $propertySchema->type = 'array';
200
            $propertySchema->items = new Schema([
201
                'oneOf' => [
202
                    new Schema(['type' => 'string', 'nullable' => true]),
203
                    new Schema(['type' => 'integer']),
204
                    new Schema(['type' => 'boolean']),
205
                ],
206
            ]);
207
            $arrayType = $type->getCollectionValueType();
208
            if ($arrayType) {
209
                if ($arrayType->getClassName()) {
210
                    $propertySchema->items = $this->createSchemaRecursive($arrayType->getClassName(), $operation, $groups, $recursion + 1);
211
                }
212
                if ($arrayType->getBuiltinType()) {
213
                    $type = $this->translateType($arrayType->getBuiltinType());
214
                    $propertySchema->items = new Schema([
215
                        'type' => $type,
216
                        'format' => ($type === 'number') ? $arrayType->getBuiltinType() : null,
217
                    ]);
218
                }
219
            }
220
            return $propertySchema;
221
        }
222
        if ($propertySchema->type === 'number') {
223
            $propertySchema->format = $type->getBuiltinType();
224
        }
225
        $className = $type->getClassName();
226
        if ('object' === $type->getBuiltinType() && $recursion < 2 && !is_null($className)) {
227
            return $this->createSchemaRecursive($className, $operation, $groups, $recursion + 1);
228
        }
229
        return $propertySchema;
230
    }
231
232
    /**
233
     * Returns true if a property is applicable for a specific operation and a specific serialization group.
234
     *
235
     * @param string $resourceClass
236
     * @param AttributeMetadataInterface $attributeMetadata
237
     * @param string $operation
238
     * @param string[] $groups
239
     * @return bool
240
     */
241
    private function isPropertyApplicable(string $resourceClass, AttributeMetadataInterface $attributeMetadata, string $operation, array $groups): bool
242
    {
243
        if (!array_intersect($attributeMetadata->getGroups(), $groups)) {
244
            return false;
245
        }
246
        switch ($operation) {
247
            case 'put':
248
                return $this->propertyInfoExtractor->isReadable($resourceClass, $attributeMetadata->getName())
249
                    && $this->propertyInfoExtractor->isWritable($resourceClass, $attributeMetadata->getName());
250
            case 'get':
251
                return (bool) $this->propertyInfoExtractor->isReadable($resourceClass, $attributeMetadata->getName());
252
            case 'post':
253
                return $this->propertyInfoExtractor->isWritable($resourceClass, $attributeMetadata->getName())
254
                    || $this->propertyInfoExtractor->isInitializable($resourceClass, $attributeMetadata->getName());
255
        }
256
257
        // @codeCoverageIgnoreStart
258
        return true;
259
        // @codeCoverageIgnoreEnd
260
    }
261
262
    /**
263
     * Returns a Schema for a resource class, operation and serialization group tuple.
264
     *
265
     * @param string $resourceClass
266
     * @param string $operation
267
     * @param string[] $groups
268
     * @return Schema
269
     */
270
    public function createSchema(string $resourceClass, string $operation, array $groups): Schema
271
    {
272
        return $this->createSchemaRecursive($resourceClass, $operation, $groups);
273
    }
274
275
    /**
276
     * Creates a unique cache key to be used for already defined schemas for performance reasons.
277
     *
278
     * @param string $resourceClass
279
     * @param string $operation
280
     * @param string[] $groups
281
     * @return string
282
     */
283
    private function getCacheKey(string $resourceClass, string $operation, array $groups)
284
    {
285
        return $resourceClass . ',' . $operation . ',' . implode(', ', $groups);
286
    }
287
288
    /**
289
     * Returns OpenApi property type for scalars.
290
     *
291
     * @param string $type
292
     * @return string
293
     */
294
    private function translateType(string $type): string
295
    {
296
        switch ($type) {
297
            case 'int': return 'integer';
298
            case 'bool': return 'boolean';
299
            case 'float': return 'number';
300
            case 'double': return 'number';
301
        }
302
303
        return $type;
304
    }
305
}
306