OpenApiDefinitionSchemaBuilder   F
last analyzed

Complexity

Total Complexity 104

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 274
dl 0
loc 524
rs 2
c 1
b 1
f 0
wmc 104

14 Methods

Rating   Name   Duplication   Size   Complexity  
B getPropertyAssocsByField() 0 27 7
A createToOneLinkage() 0 27 1
B resolveJsonField() 0 47 11
A createToManyLinkage() 0 34 2
B shouldFieldBeIncluded() 0 19 7
A getType() 0 19 6
B getPropertyByField() 0 40 9
A getRelationShipEntity() 0 14 3
A snakeCaseToCamelCase() 0 3 1
A getRelationShipProperty() 0 20 2
F getSchemaByDefinition() 0 173 40
A isDeprecated() 0 9 2
B getExtensions() 0 35 10
A isWriteProtected() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like OpenApiDefinitionSchemaBuilder 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 OpenApiDefinitionSchemaBuilder, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Framework\Api\ApiDefinition\Generator\OpenApi;
4
5
use OpenApi\Annotations\Property;
6
use OpenApi\Annotations\Schema;
7
use Shopware\Core\Framework\Api\ApiDefinition\DefinitionService;
8
use Shopware\Core\Framework\Api\Context\AdminApiSource;
9
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
10
use Shopware\Core\Framework\Context;
11
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
12
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
13
use Shopware\Core\Framework\DataAbstractionLayer\Field\BoolField;
14
use Shopware\Core\Framework\DataAbstractionLayer\Field\BreadcrumbField;
15
use Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField;
16
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
17
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
18
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
19
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Deprecated;
20
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
21
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
22
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
23
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Since;
24
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\WriteProtected;
25
use Shopware\Core\Framework\DataAbstractionLayer\Field\FloatField;
26
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
27
use Shopware\Core\Framework\DataAbstractionLayer\Field\IntField;
28
use Shopware\Core\Framework\DataAbstractionLayer\Field\JsonField;
29
use Shopware\Core\Framework\DataAbstractionLayer\Field\ListField;
30
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
31
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
32
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
33
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
34
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
35
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
36
use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
37
use Shopware\Core\Framework\Log\Package;
38
use Shopware\Core\Framework\Uuid\Uuid;
39
40
#[Package('core')]
41
class OpenApiDefinitionSchemaBuilder
42
{
43
    /**
44
     * @return Schema[]
45
     */
46
    public function getSchemaByDefinition(
47
        EntityDefinition $definition,
48
        string $path,
49
        bool $forSalesChannel,
50
        bool $onlyFlat = false,
51
        string $apiType = DefinitionService::TYPE_JSON_API
52
    ): array {
53
        $schema = [];
54
        $attributes = [];
55
        $requiredAttributes = [];
56
        $relationships = [];
57
58
        $schemaName = $this->snakeCaseToCamelCase($definition->getEntityName());
59
        $uuid = Uuid::fromStringToHex($schemaName);
60
        $exampleDetailPath = $path . '/' . $uuid;
61
62
        $extensions = [];
63
        $extensionRelationships = [];
64
65
        foreach ($definition->getFields() as $field) {
66
            if (!$this->shouldFieldBeIncluded($field, $forSalesChannel)) {
0 ignored issues
show
Bug introduced by
$field of type array is incompatible with the type Shopware\Core\Framework\...actionLayer\Field\Field expected by parameter $field of Shopware\Core\Framework\...shouldFieldBeIncluded(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

66
            if (!$this->shouldFieldBeIncluded(/** @scrutinizer ignore-type */ $field, $forSalesChannel)) {
Loading history...
67
                continue;
68
            }
69
70
            if ($field->is(Extension::class)) {
71
                $extensions[] = $field;
72
73
                continue;
74
            }
75
76
            if ($field->is(Required::class) && !$field instanceof VersionField && !$field instanceof ReferenceVersionField) {
77
                $requiredAttributes[] = $field->getPropertyName();
78
            }
79
80
            if ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) {
81
                $relationships[] = $this->createToOneLinkage($field, $exampleDetailPath);
82
83
                continue;
84
            }
85
86
            if ($field instanceof AssociationField) {
87
                $relationships[] = $this->createToManyLinkage($field, $exampleDetailPath);
88
89
                continue;
90
            }
91
92
            if ($field instanceof TranslatedField && $definition->getTranslationDefinition()) {
93
                $field = $definition->getTranslationDefinition()->getFields()->get($field->getPropertyName());
94
            }
95
96
            if ($field === null) {
97
                continue;
98
            }
99
100
            if ($field instanceof JsonField) {
101
                $attributes[] = $this->resolveJsonField($field);
102
103
                continue;
104
            }
105
106
            $attr = $this->getPropertyByField($field);
0 ignored issues
show
Bug introduced by
It seems like $field can also be of type array; however, parameter $field of Shopware\Core\Framework\...r::getPropertyByField() does only seem to accept Shopware\Core\Framework\...actionLayer\Field\Field, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

106
            $attr = $this->getPropertyByField(/** @scrutinizer ignore-type */ $field);
Loading history...
107
108
            if (\in_array($field->getPropertyName(), ['createdAt', 'updatedAt'], true) || $this->isWriteProtected($field)) {
0 ignored issues
show
Bug introduced by
It seems like $field can also be of type array; however, parameter $field of Shopware\Core\Framework\...der::isWriteProtected() does only seem to accept Shopware\Core\Framework\...actionLayer\Field\Field, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

108
            if (\in_array($field->getPropertyName(), ['createdAt', 'updatedAt'], true) || $this->isWriteProtected(/** @scrutinizer ignore-type */ $field)) {
Loading history...
109
                $attr->readOnly = true;
110
            }
111
112
            if ($this->isDeprecated($field)) {
0 ignored issues
show
Bug introduced by
It seems like $field can also be of type array; however, parameter $field of Shopware\Core\Framework\...Builder::isDeprecated() does only seem to accept Shopware\Core\Framework\...actionLayer\Field\Field, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
            if ($this->isDeprecated(/** @scrutinizer ignore-type */ $field)) {
Loading history...
113
                $attr->deprecated = true;
114
            }
115
116
            $attributes[] = $attr;
117
        }
118
119
        $extensionAttributes = $this->getExtensions($extensions, $exampleDetailPath);
120
121
        if (!empty($extensionAttributes)) {
122
            foreach ($extensions as $extension) {
123
                if (!$extension instanceof AssociationField) {
124
                    continue;
125
                }
126
127
                $extensionRelationships[] = $extensionAttributes[$extension->getPropertyName()];
128
            }
129
130
            $attributes[] = new Property([
131
                'property' => 'extensions',
132
                'type' => 'object',
133
                'properties' => $extensionAttributes,
134
            ]);
135
        }
136
137
        if ($definition->getTranslationDefinition()) {
138
            foreach ($definition->getTranslationDefinition()->getFields() as $field) {
139
                if ($field->getPropertyName() === 'translations' || $field->getPropertyName() === 'id') {
140
                    continue;
141
                }
142
143
                if ($field->is(Required::class) && !$field instanceof VersionField && !$field instanceof ReferenceVersionField && !$field instanceof FkField) {
144
                    $requiredAttributes[] = $field->getPropertyName();
145
                }
146
            }
147
        }
148
149
        $attributes = [...[new Property(['property' => 'id', 'type' => 'string', 'pattern' => '^[0-9a-f]{32}$'])], ...$attributes];
150
        $requiredAttributes = array_unique($requiredAttributes);
151
152
        if (!$onlyFlat && $apiType === 'jsonapi') {
153
            $schema[$schemaName . 'JsonApi'] = new Schema([
154
                'schema' => $schemaName . 'JsonApi',
155
                'allOf' => [
156
                    new Schema(['ref' => '#/components/schemas/resource']),
157
                    new Schema([
158
                        'type' => 'object',
159
                        'properties' => $attributes,
160
                    ]),
161
                ],
162
            ]);
163
164
            if (!empty($definition->since())) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $definition->since() targeting Shopware\Core\Framework\...tityDefinition::since() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
165
                $schema[$schemaName . 'JsonApi']->description = 'Added since version: ' . $definition->since();
166
            }
167
168
            if (\count($requiredAttributes)) {
169
                $schema[$schemaName . 'JsonApi']->allOf[1]->required = $requiredAttributes;
170
            }
171
172
            if (\count($relationships)) {
173
                $schema[$schemaName . 'JsonApi']->allOf[1]->properties[] = new Property([
174
                    'property' => 'relationships',
175
                    'type' => 'object',
176
                    'properties' => $relationships,
177
                ]);
178
            }
179
        }
180
181
        foreach ($relationships as $relationship) {
182
            $attributes[] = $this->getRelationShipProperty($relationship);
183
        }
184
185
        if (!empty($extensionRelationships)) {
186
            $extensionRelationshipsProperty = new Property([
187
                'property' => 'extensions',
188
                'type' => 'object',
189
                'properties' => $extensionAttributes,
190
            ]);
191
192
            foreach ($extensionRelationships as $property => $relationship) {
193
                $extensionRelationshipsProperty->properties[$property] = $this->getRelationShipProperty($relationship);
194
            }
195
196
            $attributes[] = $extensionRelationshipsProperty;
197
        }
198
199
        // In some entities all fields are hidden, but not the id. This creates unwanted schemas. This removes it again
200
        if (\count($attributes) === 1 && $attributes[0]->property === 'id') {
201
            return [];
202
        }
203
204
        $schema[$schemaName] = new Schema([
205
            'type' => 'object',
206
            'schema' => $schemaName,
207
            'properties' => $attributes,
208
        ]);
209
210
        if (!empty($definition->since())) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $definition->since() targeting Shopware\Core\Framework\...tityDefinition::since() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
211
            $schema[$schemaName]->description = 'Added since version: ' . $definition->since();
212
        }
213
214
        if (\count($requiredAttributes)) {
215
            $schema[$schemaName]->required = $requiredAttributes;
216
        }
217
218
        return $schema;
219
    }
220
221
    private function snakeCaseToCamelCase(string $input): string
222
    {
223
        return str_replace('_', '', ucwords($input, '_'));
224
    }
225
226
    private function shouldFieldBeIncluded(Field $field, bool $forSalesChannel): bool
227
    {
228
        if ($field->getPropertyName() === 'translations'
229
            || $field->getPropertyName() === 'id'
230
            || preg_match('#translations$#i', $field->getPropertyName())) {
231
            return false;
232
        }
233
234
        /** @var ApiAware|null $flag */
235
        $flag = $field->getFlag(ApiAware::class);
236
        if ($flag === null) {
237
            return false;
238
        }
239
240
        if (!$flag->isSourceAllowed($forSalesChannel ? SalesChannelApiSource::class : AdminApiSource::class)) {
241
            return false;
242
        }
243
244
        return true;
245
    }
246
247
    private function createToOneLinkage(ManyToOneAssociationField|OneToOneAssociationField $field, string $basePath): Property
248
    {
249
        return new Property([
250
            'type' => 'object',
251
            'property' => $field->getPropertyName(),
252
            'properties' => [
253
                'links' => [
254
                    'type' => 'object',
255
                    'properties' => [
256
                        'related' => [
257
                            'type' => 'string',
258
                            'format' => 'uri-reference',
259
                            'example' => $basePath . '/' . $field->getPropertyName(),
260
                        ],
261
                    ],
262
                ],
263
                'data' => [
264
                    'type' => 'object',
265
                    'properties' => [
266
                        'type' => [
267
                            'type' => 'string',
268
                            'example' => $field->getReferenceDefinition()->getEntityName(),
269
                        ],
270
                        'id' => [
271
                            'type' => 'string',
272
                            'pattern' => '^[0-9a-f]{32}$',
273
                            'example' => Uuid::fromStringToHex($field->getPropertyName()),
274
                        ],
275
                    ],
276
                ],
277
            ],
278
        ]);
279
    }
280
281
    private function createToManyLinkage(ManyToManyAssociationField|OneToManyAssociationField|AssociationField $field, string $basePath): Property
282
    {
283
        $associationEntityName = $field->getReferenceDefinition()->getEntityName();
284
285
        if ($field instanceof ManyToManyAssociationField) {
286
            $associationEntityName = $field->getToManyReferenceDefinition()->getEntityName();
287
        }
288
289
        return new Property([
290
            'type' => 'object',
291
            'property' => $field->getPropertyName(),
292
            'properties' => [
293
                'links' => [
294
                    'type' => 'object',
295
                    'properties' => [
296
                        'related' => [
297
                            'type' => 'string',
298
                            'format' => 'uri-reference',
299
                            'example' => $basePath . '/' . $field->getPropertyName(),
300
                        ],
301
                    ],
302
                ],
303
                'data' => [
304
                    'type' => 'array',
305
                    'items' => [
306
                        'type' => 'object',
307
                        'properties' => [
308
                            'type' => [
309
                                'type' => 'string',
310
                                'example' => $associationEntityName,
311
                            ],
312
                            'id' => [
313
                                'type' => 'string',
314
                                'example' => Uuid::fromStringToHex($field->getPropertyName()),
315
                            ],
316
                        ],
317
                    ],
318
                ],
319
            ],
320
        ]);
321
    }
322
323
    /**
324
     * @param Field[] $extensions
325
     *
326
     * @return Property[]
327
     */
328
    private function getExtensions(array $extensions, string $path): array
329
    {
330
        $attributes = [];
331
        foreach ($extensions as $field) {
332
            $property = $field->getPropertyName();
333
334
            $schema = null;
335
            if ($field instanceof OneToManyAssociationField || $field instanceof ManyToManyAssociationField) {
336
                $schema = $this->createToManyLinkage($field, $path);
337
            }
338
339
            if ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) {
340
                $schema = $this->createToOneLinkage($field, $path);
341
            }
342
343
            if ($field instanceof JsonField) {
344
                $schema = $this->resolveJsonField($field);
345
            }
346
347
            if ($schema === null) {
348
                continue;
349
            }
350
351
            if ($this->isWriteProtected($field)) {
352
                $schema->readOnly = true;
353
            }
354
355
            if ($this->isDeprecated($field)) {
356
                $schema->deprecated = true;
357
            }
358
359
            $attributes[$property] = $schema;
360
        }
361
362
        return $attributes;
363
    }
364
365
    private function resolveJsonField(JsonField $jsonField): Property
366
    {
367
        if ($jsonField instanceof ListField || $jsonField instanceof BreadcrumbField) {
368
            $definition = new Property([
369
                'type' => 'array',
370
                'property' => $jsonField->getPropertyName(),
371
                'items' => $this->getPropertyAssocsByField($jsonField instanceof ListField ? $jsonField->getFieldType() : null),
372
            ]);
373
        } else {
374
            $definition = new Property([
375
                'type' => 'object',
376
                'property' => $jsonField->getPropertyName(),
377
            ]);
378
        }
379
380
        $required = [];
381
382
        if (!empty($jsonField->getPropertyMapping())) {
383
            $definition->properties = [];
384
        }
385
386
        foreach ($jsonField->getPropertyMapping() as $field) {
387
            if ($field instanceof JsonField) {
388
                $definition->properties[] = $this->resolveJsonField($field);
389
390
                continue;
391
            }
392
393
            if ($field->is(Required::class)) {
394
                $required[] = $field->getPropertyName();
395
            }
396
397
            $definition->properties[] = $this->getPropertyByField($field);
398
        }
399
400
        if (\count($required)) {
401
            $definition->required = $required;
402
        }
403
        if ($this->isWriteProtected($jsonField)) {
404
            $definition->readOnly = true;
405
        }
406
407
        if ($this->isDeprecated($jsonField)) {
408
            $definition->deprecated = true;
409
        }
410
411
        return $definition;
412
    }
413
414
    private function getPropertyByField(Field $field): Property
415
    {
416
        $fieldClass = $field::class;
417
418
        $property = new Property([
419
            'type' => $this->getType($fieldClass),
420
            'property' => $field->getPropertyName(),
421
        ]);
422
423
        if (is_a($fieldClass, DateTimeField::class, true)) {
424
            $property->format = 'date-time';
425
        }
426
        if (is_a($fieldClass, FloatField::class, true)) {
427
            $property->format = 'float';
428
        }
429
        if (is_a($fieldClass, IntField::class, true)) {
430
            $property->format = 'int64';
431
        }
432
        if (is_a($fieldClass, IdField::class, true) || is_a($fieldClass, FkField::class, true)) {
433
            $property->type = 'string';
434
            $property->pattern = '^[0-9a-f]{32}$';
435
        }
436
437
        $description = [];
438
        $flag = $field->getFlag(Since::class);
439
        if ($flag instanceof Since) {
440
            $description[] = \sprintf('Added since version: %s.', $flag->getSince());
441
        }
442
443
        $flag = $field->getFlag(Runtime::class);
444
        if ($flag instanceof Runtime) {
445
            $description[] = 'Runtime field, cannot be used as part of the criteria.';
446
        }
447
448
        $description = \implode(' ', $description);
449
        if ($description !== '') {
450
            $property->description = $description;
451
        }
452
453
        return $property;
454
    }
455
456
    private function getPropertyAssocsByField(?string $fieldClass): object
457
    {
458
        $property = new \stdClass();
459
        if ($fieldClass === null) {
460
            $property->type = 'object';
461
            $property->additionalProperties = false;
462
463
            return $property;
464
        }
465
466
        $property->type = $this->getType($fieldClass);
467
468
        if (is_a($fieldClass, DateTimeField::class, true)) {
469
            $property->format = 'date-time';
470
        }
471
        if (is_a($fieldClass, FloatField::class, true)) {
472
            $property->format = 'float';
473
        }
474
        if (is_a($fieldClass, IntField::class, true)) {
475
            $property->format = 'int64';
476
        }
477
        if (is_a($fieldClass, IdField::class, true) || is_a($fieldClass, FkField::class, true)) {
478
            $property->type = 'string';
479
            $property->pattern = '^[0-9a-f]{32}$';
480
        }
481
482
        return $property;
483
    }
484
485
    private function getType(string $fieldClass): string
486
    {
487
        if (is_a($fieldClass, FloatField::class, true)) {
488
            return 'number';
489
        }
490
        if (is_a($fieldClass, IntField::class, true)) {
491
            return 'integer';
492
        }
493
        if (is_a($fieldClass, BoolField::class, true)) {
494
            return 'boolean';
495
        }
496
        if (is_a($fieldClass, ListField::class, true)) {
497
            return 'array';
498
        }
499
        if (is_a($fieldClass, JsonField::class, true)) {
500
            return 'object';
501
        }
502
503
        return 'string';
504
    }
505
506
    private function isWriteProtected(Field $field): bool
507
    {
508
        /** @var WriteProtected|null $writeProtection */
509
        $writeProtection = $field->getFlag(WriteProtected::class);
510
        if ($writeProtection && !$writeProtection->isAllowed(Context::USER_SCOPE)) {
511
            return true;
512
        }
513
514
        return false;
515
    }
516
517
    private function isDeprecated(Field $field): bool
518
    {
519
        /** @var Deprecated|null $deprecated */
520
        $deprecated = $field->getFlag(Deprecated::class);
521
        if ($deprecated) {
0 ignored issues
show
introduced by
$deprecated is of type Shopware\Core\Framework\...r\Field\Flag\Deprecated, thus it always evaluated to true.
Loading history...
522
            return true;
523
        }
524
525
        return false;
526
    }
527
528
    private function getRelationShipEntity(Property $relationship): string
529
    {
530
        /** @var array<mixed> $relationshipData */
531
        $relationshipData = $relationship->properties['data'];
532
        $type = $relationshipData['type'];
533
        $entity = '';
534
535
        if ($type === 'object') {
536
            $entity = $relationshipData['properties']['type']['example'];
537
        } elseif ($type === 'array') {
538
            $entity = $relationshipData['items']['properties']['type']['example'];
539
        }
540
541
        return $entity;
542
    }
543
544
    private function getRelationShipProperty(Property $relationship): Property
545
    {
546
        $entity = $this->getRelationShipEntity($relationship);
547
        $entityName = $this->snakeCaseToCamelCase($entity);
548
549
        /** @var array<mixed> $relationshipData */
550
        $relationshipData = $relationship->properties['data'];
551
        $type = $relationshipData['type'];
552
553
        if ($type === 'array') {
554
            return new Property([
555
                'property' => $relationship->property,
556
                'type' => 'array',
557
                'items' => new Schema(['ref' => '#/components/schemas/' . $entityName]),
558
            ]);
559
        }
560
561
        return new Property([
562
            'property' => $relationship->property,
563
            'ref' => '#/components/schemas/' . $entityName,
564
        ]);
565
    }
566
}
567