DynamicFieldFactory::defineField()   F
last analyzed

Complexity

Conditions 36
Paths 153

Size

Total Lines 283
Code Lines 143

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 36
eloc 143
nc 153
nop 4
dl 0
loc 283
rs 2.98
c 0
b 0
f 0

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 declare(strict_types=1);
2
3
namespace Shopware\Core\System\CustomEntity\Schema;
4
5
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
6
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
7
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
8
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEventFactory;
9
use Shopware\Core\Framework\DataAbstractionLayer\Field\BoolField;
10
use Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField;
11
use Shopware\Core\Framework\DataAbstractionLayer\Field\EmailField;
12
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
13
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
14
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\AllowHtml;
15
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
16
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\CascadeDelete;
17
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension as DalExtension;
18
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Flag;
19
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
20
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
21
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\RestrictDelete;
22
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ReverseInherited;
23
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\SetNullOnDelete;
24
use Shopware\Core\Framework\DataAbstractionLayer\Field\FloatField;
25
use Shopware\Core\Framework\DataAbstractionLayer\Field\IntField;
26
use Shopware\Core\Framework\DataAbstractionLayer\Field\JsonField;
27
use Shopware\Core\Framework\DataAbstractionLayer\Field\LongTextField;
28
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
29
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
30
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
31
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
32
use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField;
33
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
34
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
35
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
36
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
37
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
38
use Shopware\Core\Framework\DataAbstractionLayer\Read\EntityReaderInterface;
39
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntityAggregatorInterface;
40
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearcherInterface;
41
use Shopware\Core\Framework\DataAbstractionLayer\VersionManager;
42
use Shopware\Core\Framework\Log\Package;
43
use Shopware\Core\System\CustomEntity\Xml\Field\AssociationField;
44
use Symfony\Component\DependencyInjection\ContainerInterface;
45
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
46
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
47
48
/**
49
 * @internal
50
 *
51
 * @phpstan-import-type CustomEntityField from CustomEntitySchemaUpdater
52
 */
53
#[Package('core')]
54
class DynamicFieldFactory
55
{
56
    /**
57
     * @param list<CustomEntityField> $fields
0 ignored issues
show
Bug introduced by
The type Shopware\Core\System\CustomEntity\Schema\list 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...
58
     */
59
    public static function create(ContainerInterface $container, string $entityName, array $fields): FieldCollection
60
    {
61
        $translated = [];
62
63
        $collection = new FieldCollection();
64
65
        foreach ($fields as $field) {
66
            $translatable = $field['translatable'] ?? false;
67
            if ($translatable) {
68
                $translated[] = $field;
69
70
                continue;
71
            }
72
73
            self::defineField($field, $collection, $entityName, $container);
74
        }
75
76
        if (empty($translated)) {
77
            return $collection;
78
        }
79
80
        $translations = new TranslationsAssociationField($entityName . '_translation', $entityName . '_id', 'translations', 'id');
81
        $collection->add($translations);
82
83
        foreach ($translated as &$field) {
84
            $required = $field['required'] ?? false;
85
            $apiAware = $field['storeApiAware'] ?? false;
86
87
            $property = self::kebabCaseToCamelCase($field['name']);
88
            unset($field['translatable']);
89
90
            $translatedField = new TranslatedField($property);
91
            if ($required) {
92
                $translations->addFlags(new Required());
93
            }
94
            if ($apiAware) {
95
                $translations->addFlags(new ApiAware());
96
                $translatedField->addFlags(new ApiAware());
97
            }
98
            $collection->add($translatedField);
99
        }
100
101
        unset($field);
102
103
        $registry = $container->get(DefinitionInstanceRegistry::class);
104
        if (!$registry instanceof DefinitionInstanceRegistry) {
105
            throw new ServiceNotFoundException(DefinitionInstanceRegistry::class);
106
        }
107
108
        $translation = DynamicTranslationEntityDefinition::create($entityName, $translated, $container);
109
        $container->set($translation->getEntityName(), $translation);
110
        $container->set($translation->getEntityName() . '.repository', self::createRepository($container, $translation));
111
112
        $registry->register($translation, $translation->getEntityName());
113
114
        return $collection;
115
    }
116
117
    private static function kebabCaseToCamelCase(string $string): string
118
    {
119
        return (new CamelCaseToSnakeCaseNameConverter())->denormalize(str_replace('-', '_', $string));
120
    }
121
122
    private static function createRepository(ContainerInterface $container, EntityDefinition $definition): EntityRepository
123
    {
124
        return new EntityRepository(
125
            $definition,
126
            $container->get(EntityReaderInterface::class),
0 ignored issues
show
Bug introduced by
It seems like $container->get(Shopware...ReaderInterface::class) can also be of type null; however, parameter $reader of Shopware\Core\Framework\...pository::__construct() does only seem to accept Shopware\Core\Framework\...d\EntityReaderInterface, 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

126
            /** @scrutinizer ignore-type */ $container->get(EntityReaderInterface::class),
Loading history...
127
            $container->get(VersionManager::class),
0 ignored issues
show
Bug introduced by
It seems like $container->get(Shopware...\VersionManager::class) can also be of type null; however, parameter $versionManager of Shopware\Core\Framework\...pository::__construct() does only seem to accept Shopware\Core\Framework\...ionLayer\VersionManager, 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

127
            /** @scrutinizer ignore-type */ $container->get(VersionManager::class),
Loading history...
128
            $container->get(EntitySearcherInterface::class),
0 ignored issues
show
Bug introduced by
It seems like $container->get(Shopware...archerInterface::class) can also be of type null; however, parameter $searcher of Shopware\Core\Framework\...pository::__construct() does only seem to accept Shopware\Core\Framework\...EntitySearcherInterface, 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

128
            /** @scrutinizer ignore-type */ $container->get(EntitySearcherInterface::class),
Loading history...
129
            $container->get(EntityAggregatorInterface::class),
0 ignored issues
show
Bug introduced by
It seems like $container->get(Shopware...egatorInterface::class) can also be of type null; however, parameter $aggregator of Shopware\Core\Framework\...pository::__construct() does only seem to accept Shopware\Core\Framework\...tityAggregatorInterface, 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

129
            /** @scrutinizer ignore-type */ $container->get(EntityAggregatorInterface::class),
Loading history...
130
            $container->get('event_dispatcher'),
0 ignored issues
show
Bug introduced by
It seems like $container->get('event_dispatcher') can also be of type null; however, parameter $eventDispatcher of Shopware\Core\Framework\...pository::__construct() does only seem to accept Symfony\Component\EventD...ventDispatcherInterface, 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

130
            /** @scrutinizer ignore-type */ $container->get('event_dispatcher'),
Loading history...
131
            $container->get(EntityLoadedEventFactory::class)
0 ignored issues
show
Bug introduced by
It seems like $container->get(Shopware...dedEventFactory::class) can also be of type null; however, parameter $eventFactory of Shopware\Core\Framework\...pository::__construct() does only seem to accept Shopware\Core\Framework\...ntityLoadedEventFactory, 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

131
            /** @scrutinizer ignore-type */ $container->get(EntityLoadedEventFactory::class)
Loading history...
132
        );
133
    }
134
135
    /**
136
     * @param CustomEntityField $field
0 ignored issues
show
Bug introduced by
The type Shopware\Core\System\Cus...chema\CustomEntityField 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...
137
     */
138
    private static function defineField(array $field, FieldCollection $collection, string $entityName, ContainerInterface $container): void
139
    {
140
        $registry = $container->get(DefinitionInstanceRegistry::class);
141
        if (!$registry instanceof DefinitionInstanceRegistry) {
142
            throw new ServiceNotFoundException(DefinitionInstanceRegistry::class);
143
        }
144
145
        $name = $field['name'];
146
        $required = ($field['required'] ?? false) ? new Required() : null;
147
        $inherited = $field['inherited'] ?? false;
148
        $apiAware = ($field['storeApiAware'] ?? false) ? new ApiAware() : null;
149
150
        $flags = \array_filter([$required, $apiAware]);
151
152
        $property = self::kebabCaseToCamelCase($name);
153
154
        switch ($field['type']) {
155
            case 'int':
156
                $collection->add(
157
                    (new IntField($name, $property))
158
                        ->addFlags(...$flags)
159
                );
160
161
                break;
162
            case 'bool':
163
                $collection->add(
164
                    (new BoolField($name, $property))
165
                        ->addFlags(...$flags)
166
                );
167
168
                break;
169
            case 'float':
170
                $collection->add(
171
                    (new FloatField($name, $property))
172
                        ->addFlags(...$flags)
173
                );
174
175
                break;
176
            case 'email':
177
                $collection->add(
178
                    (new EmailField($name, $property))
179
                        ->addFlags(...$flags)
180
                );
181
182
                break;
183
            case 'text':
184
                $instance = (new LongTextField($name, $property))
185
                    ->addFlags(...$flags);
186
187
                if ($field['allowHtml'] ?? false) {
188
                    $instance->addFlags(new AllowHtml(true));
189
                }
190
191
                $collection->add($instance);
192
193
                break;
194
            case 'price':
195
                $collection->add(
196
                    (new PriceField($name, $property))
197
                        ->addFlags(...$flags)
198
                );
199
200
                break;
201
            case 'date':
202
                $collection->add(
203
                    (new DateTimeField($name, $property))
204
                        ->addFlags(...$flags)
205
                );
206
207
                break;
208
209
            case 'json':
210
                $collection->add(
211
                    (new JsonField($name, $property))
212
                        ->addFlags(...$flags)
213
                );
214
215
                break;
216
            case 'many-to-many':
217
                // get reference entity definition to create bi-directionally associations
218
                $reference = $registry->getByEntityName($field['reference']);
219
220
                // build mapping name:   'custom_entity_blog_products'  => use field name instead of reference entity name to allow multiple references to same entity
221
                $mappingName = implode('_', [$entityName, $field['name']]);
222
223
                // create many-to-many association field for custom entity definition
224
                $association = new ManyToManyAssociationField($property, $field['reference'], $mappingName, $entityName . '_id', $field['reference'] . '_id', 'id', 'id');
225
226
                // mapping table records can always be deleted
227
                $association->addFlags(new CascadeDelete());
228
229
                // field is maybe flag to be store-api aware
230
                self::addFlag($association, $apiAware);
231
232
                // check product inheritance and add ReverseInherited(reverse-property-name)
233
                if ($reference->isInheritanceAware() && $inherited) {
234
                    $association->addFlags(new ReverseInherited(self::kebabCaseToCamelCase($mappingName)));
235
                }
236
237
                // association for custom entity definition: done
238
                $collection->add($association);
239
240
                // define mapping entity definition, fields are defined inside the definition class
241
                $definition = DynamicMappingEntityDefinition::create($entityName, $field['reference'], $mappingName);
242
243
                // register definition in container and definition registry
244
                $container->set($definition->getEntityName(), $definition);
245
                $container->set($definition->getEntityName() . '.repository', self::createRepository($container, $definition));
246
                $registry->register($definition, $definition->getEntityName());
247
248
                // define reverse side
249
                $property = self::kebabCaseToCamelCase($definition->getEntityName());
250
251
                // reverse property schema: #table#_#column# -  custom_entity_blog_products
252
                $association = new ManyToManyAssociationField($property, $entityName, $definition->getEntityName(), $field['reference'] . '_id', $entityName . '_id');
253
                $association->addFlags(new CascadeDelete());
254
255
                // if reference is not a custom entity definition, we need to add the dal extension flag to get the hydrated objects as `entity.extensions` value
256
                self::addFlag($association, self::getExtension($reference));
257
258
                // check for product inheritance use case
259
                if ($reference->isInheritanceAware() && $inherited) {
260
                    $association->addFlags(new Inherited());
261
                }
262
263
                $association->compile($registry);
264
                $reference->getFields()->addField($association);
265
266
                break;
267
268
            case 'many-to-one':
269
                // get reference entity definition to create bi-directionally associations
270
                $reference = $registry->getByEntityName($field['reference']);
271
272
                // build reverse property name: #table# _ #field#:  custom_entity_blog_top_seller: customEntityBlogTopSeller
273
                $reverse = self::kebabCaseToCamelCase($entityName . '_' . $name);
274
275
                // build foreign key field for custom entity table: custom_entity_blog_top_seller_id
276
                $foreignKey = (new FkField(self::id($name), $property . 'Id', $field['reference'], 'id'))->addFlags(...$flags);
277
                $collection->add($foreignKey);
278
279
                // now build association field for custom entity definition
280
                $association = new ManyToOneAssociationField($property, self::id($name), $field['reference'], 'id', false);
281
282
                // add flag for store-api awareness
283
                self::addFlag($association, $apiAware);
284
285
                // check for product inheritance use case and define reverse inherited flag. Used when joining from custom entity table to product table
286
                if ($reference->isInheritanceAware() && $inherited) {
287
                    $association->addFlags(new ReverseInherited($reverse));
288
                }
289
                $collection->add($association);
290
291
                if ($reference->isVersionAware()) {
292
                    // if reference is version aware, we need a reference version field inside the custom entity definition
293
                    $collection->add((new ReferenceVersionField($reference->getEntityName(), $name . '_version_id'))->addFlags(new Required()));
294
                }
295
296
                // now define reverse association
297
                $association = new OneToManyAssociationField($reverse, $entityName, self::id($name), 'id');
298
299
                // in sql we define the on-delete flag on the foreign key, for the DAL we need the flag on the reverse side, so we can check which association are affected when deleting the record (e.g. product)
300
                $association->addFlags(self::getOnDeleteFlag($field));
301
302
                // if reference is not a custom entity definition, we need to add the dal extension flag to get the hydrated objects as `entity.extensions` value
303
                self::addFlag($association, self::getExtension($reference));
304
305
                // check for product inheritance use case
306
                if ($reference->isInheritanceAware() && $inherited) {
307
                    $association->addFlags(new Inherited(self::id($field['name'])));
308
                }
309
310
                $association->compile($registry);
311
                $reference->getFields()->add($association);
312
313
                break;
314
            case 'one-to-one':
315
                // get reference entity definition to create bi-directionally associations
316
                $reference = $registry->getByEntityName($field['reference']);
317
318
                // build reverse property name: #table# _ #field#:  custom_entity_blog_top_seller: customEntityBlogTopSeller
319
                $reverse = self::kebabCaseToCamelCase($entityName . '_' . $name);
320
321
                // build foreign key field for custom entity table: custom_entity_blog_top_seller_id
322
                $foreignKey = (new FkField(self::id($name), $property . 'Id', $field['reference'], 'id'))->addFlags(...$flags);
323
                $collection->add($foreignKey);
324
325
                // now build association field for custom entity definition
326
                $association = new OneToOneAssociationField($property, self::id($name), 'id', $field['reference'], false);
327
328
                // add flag for store-api awareness
329
                self::addFlag($association, $apiAware);
330
331
                // check for product inheritance use case and define reverse inherited flag. Used when joining from custom entity table to product table
332
                if ($reference->isInheritanceAware() && $inherited) {
333
                    $association->addFlags(new ReverseInherited($reverse));
334
                }
335
336
                $collection->add($association);
337
338
                if ($reference->isVersionAware()) {
339
                    // if reference is version aware, we need a reference version field inside the custom entity definition
340
                    $collection->add((new ReferenceVersionField($reference->getEntityName(), $name . '_version_id'))->addFlags(new Required()));
341
                }
342
343
                // now define reverse association
344
                $association = new OneToOneAssociationField($reverse, 'id', self::id($name), $entityName, false);
345
346
                // in sql we define the on-delete flag on the foreign key, for the DAL we need the flag on the reverse side, so we can check which association are affected when deleting the record (e.g. product)
347
                $association->addFlags(self::getOnDeleteFlag($field));
348
349
                // if reference is not a custom entity definition, we need to add the dal extension flag to get the hydrated objects as `entity.extensions` value
350
                self::addFlag($association, self::getExtension($reference));
351
352
                // check for product inheritance use case
353
                if ($reference->isInheritanceAware() && $inherited) {
354
                    $association->addFlags(new Inherited(self::id($field['name'])));
355
                }
356
357
                $association->compile($registry);
358
                $reference->getFields()->addField($association);
359
360
                break;
361
            case 'one-to-many':
362
                // get reference entity definition to create bi-directionally associations
363
                $reference = $registry->getByEntityName($field['reference']);
364
365
                // build reverse property name: #table# _ #field#:  custom_entity_blog_comments/customEntityBlogComments
366
                $reverse = $entityName . '_' . $name;
367
368
                // build association for custom entity table: customEntityComments
369
                $association = new OneToManyAssociationField($property, $field['reference'], self::id($reverse), 'id');
370
371
                // in sql we define the on-delete flag on the foreign key, for the DAL we need the flag on the reverse side, so we can check which association are affected when deleting the record (e.g. product)
372
                $association->addFlags(self::getOnDeleteFlag($field));
373
374
                // add flag for store-api awareness
375
                self::addFlag($association, $apiAware);
376
377
                // check for product inheritance use case and define reverse inherited flag. Used when joining from custom entity table to product table
378
                if ($reference->isInheritanceAware() && $inherited) {
379
                    $association->addFlags(new ReverseInherited(self::kebabCaseToCamelCase($reverse)));
380
                }
381
                $collection->add($association);
382
383
                // now define the reverse side, starting with the foreign key field: custom_entity_blog_comments_id
384
                $fk = new FkField(self::id($reverse), self::kebabCaseToCamelCase(self::id($reverse)), $entityName, 'id');
385
386
                // add flag for store-api awareness
387
                self::addFlag($fk, $apiAware);
388
389
                // if reference is not a custom entity definition, we need to add the dal extension flag to get the hydrated objects as `entity.extensions` value
390
                $extension = self::getExtension($reference);
391
                self::addFlag($fk, $extension);
392
393
                // add required flag, should be set to true for aggregated entities (blog 1:N comments)
394
                $required = ($field['reverseRequired'] ?? false) ? new ApiAware() : null;
395
                self::addFlag($fk, $required);
396
397
                // compile foreign key and add to reference field collection - only compiled fields can be added after the field collection built
398
                $fk->compile($registry);
399
                $reference->getFields()->add($fk);
400
401
                // now build reverse many-to-one association: custom_entity_blog_comments::custom_entity_blog_comments
402
                $association = new ManyToOneAssociationField(self::kebabCaseToCamelCase($reverse), self::id($reverse), $entityName, 'id', false);
403
                self::addFlag($association, $extension);
404
405
                // check for product inheritance use case
406
                if ($reference->isInheritanceAware() && $inherited) {
407
                    $association->addFlags(new Inherited(self::id($field['name'])));
408
                }
409
410
                $association->compile($registry);
411
                $reference->getFields()->add($association);
412
413
                break;
414
            default:
415
                $collection->add(
416
                    (new StringField($name, $property))
417
                        ->addFlags(...$flags)
418
                );
419
420
                break;
421
        }
422
    }
423
424
    private static function addFlag(Field $field, ?Flag $flag): void
425
    {
426
        if ($flag !== null) {
427
            $field->addFlags($flag);
428
        }
429
    }
430
431
    private static function id(string $name): string
432
    {
433
        return $name . '_id';
434
    }
435
436
    /**
437
     * @param CustomEntityField $field
438
     */
439
    private static function getOnDeleteFlag(array $field): Flag
440
    {
441
        return match ($field['onDelete']) {
442
            AssociationField::CASCADE => new CascadeDelete(),
443
            AssociationField::SET_NULL => new SetNullOnDelete(),
444
            AssociationField::RESTRICT => new RestrictDelete(),
445
            default => throw new \RuntimeException(\sprintf('onDelete property %s are not supported on field %s', $field['onDelete'], $field['name'])),
446
        };
447
    }
448
449
    private static function getExtension(EntityDefinition $reference): ?DalExtension
450
    {
451
        if (str_starts_with($reference->getEntityName(), 'custom_entity_') || str_starts_with($reference->getEntityName(), 'ce_')) {
452
            return null;
453
        }
454
455
        return new DalExtension();
456
    }
457
}
458