Passed
Push — trunk ( 74bc07...45b10d )
by Christian
11:29 queued 12s
created

getCustomFieldsMapping()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 17
rs 9.9666
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Elasticsearch\Product;
4
5
use Doctrine\DBAL\Connection;
6
use OpenSearchDSL\Query\Compound\BoolQuery;
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Shopware\Core\Content\Product\ProductDefinition;
9
use Shopware\Core\Defaults;
10
use Shopware\Core\Framework\Context;
11
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
12
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
13
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
14
use Shopware\Core\Framework\Uuid\Uuid;
15
use Shopware\Core\System\CustomField\CustomFieldTypes;
16
use Shopware\Elasticsearch\Framework\AbstractElasticsearchDefinition;
17
use Shopware\Elasticsearch\Product\Event\ElasticsearchProductCustomFieldsMappingEvent;
18
19
/**
20
 * @package core
21
 */
22
class ElasticsearchProductDefinition extends AbstractElasticsearchDefinition
23
{
24
    public const KEYWORD_FIELD = [
25
        'type' => 'keyword',
26
        'normalizer' => 'sw_lowercase_normalizer',
27
    ];
28
29
    public const BOOLEAN_FIELD = ['type' => 'boolean'];
30
31
    public const FLOAT_FIELD = ['type' => 'double'];
32
33
    public const INT_FIELD = ['type' => 'long'];
34
35
    private const SEARCH_FIELD = [
36
        'fields' => [
37
            'search' => ['type' => 'text'],
38
            'ngram' => ['type' => 'text', 'analyzer' => 'sw_ngram_analyzer'],
39
        ],
40
    ];
41
42
    protected ProductDefinition $definition;
43
44
    protected EventDispatcherInterface $eventDispatcher;
45
46
    /**
47
     * @var array<string, string>
48
     */
49
    private array $customMapping;
50
51
    private Connection $connection;
52
53
    /**
54
     * @var array<string, string>|null
55
     */
56
    private ?array $customFieldsTypes = null;
57
58
    private AbstractProductSearchQueryBuilder $searchQueryBuilder;
59
60
    /**
61
     * @internal
62
     *
63
     * @param array<string, string> $customMapping
64
     */
65
    public function __construct(
66
        ProductDefinition $definition,
67
        Connection $connection,
68
        array $customMapping,
69
        EventDispatcherInterface $eventDispatcher,
70
        AbstractProductSearchQueryBuilder $searchQueryBuilder
71
    ) {
72
        $this->definition = $definition;
73
        $this->connection = $connection;
74
        $this->customMapping = $customMapping;
75
        $this->eventDispatcher = $eventDispatcher;
76
        $this->searchQueryBuilder = $searchQueryBuilder;
77
    }
78
79
    public function getEntityDefinition(): EntityDefinition
80
    {
81
        return $this->definition;
82
    }
83
84
    /**
85
     * @return array{_source: array{includes: string[]}, properties: array<mixed>}
86
     */
87
    public function getMapping(Context $context): array
88
    {
89
        return [
90
            '_source' => ['includes' => ['id', 'autoIncrement']],
91
            'properties' => [
92
                'id' => self::KEYWORD_FIELD,
93
                'parentId' => self::KEYWORD_FIELD,
94
                'active' => self::BOOLEAN_FIELD,
95
                'available' => self::BOOLEAN_FIELD,
96
                'isCloseout' => self::BOOLEAN_FIELD,
97
                'categoriesRo' => [
98
                    'type' => 'nested',
99
                    'properties' => [
100
                        'id' => self::KEYWORD_FIELD,
101
                        '_count' => self::INT_FIELD,
102
                    ],
103
                ],
104
                'categories' => [
105
                    'type' => 'nested',
106
                    'properties' => [
107
                        'id' => self::KEYWORD_FIELD,
108
                        'name' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
109
                        '_count' => self::INT_FIELD,
110
                    ],
111
                ],
112
                'childCount' => self::INT_FIELD,
113
                'autoIncrement' => self::INT_FIELD,
114
                'manufacturerNumber' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
115
                'description' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
116
                'metaTitle' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
117
                'metaDescription' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
118
                'displayGroup' => self::KEYWORD_FIELD,
119
                'ean' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
120
                'height' => self::FLOAT_FIELD,
121
                'length' => self::FLOAT_FIELD,
122
                'manufacturer' => [
123
                    'type' => 'nested',
124
                    'properties' => [
125
                        'id' => self::KEYWORD_FIELD,
126
                        'name' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
127
                        '_count' => self::INT_FIELD,
128
                    ],
129
                ],
130
                'markAsTopseller' => self::BOOLEAN_FIELD,
131
                'name' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
132
                'options' => [
133
                    'type' => 'nested',
134
                    'properties' => [
135
                        'id' => self::KEYWORD_FIELD,
136
                        'name' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
137
                        'groupId' => self::KEYWORD_FIELD,
138
                        '_count' => self::INT_FIELD,
139
                    ],
140
                ],
141
                'productNumber' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
142
                'properties' => [
143
                    'type' => 'nested',
144
                    'properties' => [
145
                        'id' => self::KEYWORD_FIELD,
146
                        'name' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
147
                        'groupId' => self::KEYWORD_FIELD,
148
                        '_count' => self::INT_FIELD,
149
                    ],
150
                ],
151
                'ratingAverage' => self::FLOAT_FIELD,
152
                'releaseDate' => [
153
                    'type' => 'date',
154
                ],
155
                'createdAt' => [
156
                    'type' => 'date',
157
                ],
158
                'sales' => self::INT_FIELD,
159
                'stock' => self::INT_FIELD,
160
                'availableStock' => self::INT_FIELD,
161
                'shippingFree' => self::BOOLEAN_FIELD,
162
                'taxId' => self::KEYWORD_FIELD,
163
                'tags' => [
164
                    'type' => 'nested',
165
                    'properties' => [
166
                        'id' => self::KEYWORD_FIELD,
167
                        'name' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
168
                        '_count' => self::INT_FIELD,
169
                    ],
170
                ],
171
                'visibilities' => [
172
                    'type' => 'nested',
173
                    'properties' => [
174
                        'id' => self::KEYWORD_FIELD,
175
                        'visibility' => self::INT_FIELD,
176
                        '_count' => self::INT_FIELD,
177
                    ],
178
                ],
179
                'coverId' => self::KEYWORD_FIELD,
180
                'weight' => self::FLOAT_FIELD,
181
                'width' => self::FLOAT_FIELD,
182
                'customFields' => $this->getCustomFieldsMapping($context),
183
                'customSearchKeywords' => self::KEYWORD_FIELD + self::SEARCH_FIELD,
184
            ],
185
            'dynamic_templates' => [
186
                [
187
                    'cheapest_price' => [
188
                        'match' => 'cheapest_price_rule*',
189
                        'mapping' => ['type' => 'double'],
190
                    ],
191
                ],
192
                [
193
                    'price_percentage' => [
194
                        'path_match' => 'price.*.percentage.*',
195
                        'mapping' => ['type' => 'double'],
196
                    ],
197
                ],
198
                [
199
                    'long_to_double' => [
200
                        'match_mapping_type' => 'long',
201
                        'mapping' => ['type' => 'double'],
202
                    ],
203
                ],
204
            ],
205
        ];
206
    }
207
208
    public function buildTermQuery(Context $context, Criteria $criteria): BoolQuery
209
    {
210
        return $this->searchQueryBuilder->build($criteria, $context);
211
    }
212
213
    /**
214
     * @param array<string> $ids
215
     *
216
     * @return array<mixed>
217
     */
218
    public function fetch(array $ids, Context $context): array
219
    {
220
        $data = $this->fetchProducts($ids, $context);
221
222
        $groupIds = [];
223
        foreach ($data as $row) {
224
            foreach (json_decode($row['propertyIds'] ?? '[]', true, 512, \JSON_THROW_ON_ERROR) as $id) {
225
                $groupIds[(string) $id] = true;
226
            }
227
            foreach (json_decode($row['optionIds'] ?? '[]', true, 512, \JSON_THROW_ON_ERROR) as $id) {
228
                $groupIds[(string) $id] = true;
229
            }
230
        }
231
232
        $groups = $this->fetchPropertyGroups(\array_keys($groupIds), $context);
233
234
        $documents = [];
235
236
        foreach ($data as $id => $item) {
237
            $visibilities = array_values(array_unique(array_filter(explode('|', $item['visibilities'] ?? ''))));
238
239
            $visibilities = array_map(static function (string $text) {
240
                [$visibility, $salesChannelId] = explode(',', $text);
241
242
                return [
243
                    'visibility' => $visibility,
244
                    'salesChannelId' => $salesChannelId,
245
                    '_count' => 1,
246
                ];
247
            }, $visibilities);
248
249
            $optionIds = json_decode($item['optionIds'] ?? '[]', true, 512, \JSON_THROW_ON_ERROR);
250
            $propertyIds = json_decode($item['propertyIds'] ?? '[]', true, 512, \JSON_THROW_ON_ERROR);
251
            $tagIds = json_decode($item['tagIds'] ?? '[]', true, 512, \JSON_THROW_ON_ERROR);
252
            $categoriesRo = json_decode($item['categoryIds'] ?? '[]', true, 512, \JSON_THROW_ON_ERROR);
253
254
            $translations = $this->filterToOne(json_decode($item['translation'], true, 512, \JSON_THROW_ON_ERROR));
255
            $parentTranslations = $this->filterToOne(json_decode($item['translation_parent'], true, 512, \JSON_THROW_ON_ERROR));
256
            $manufacturer = $this->filterToOne(json_decode($item['manufacturer_translation'], true, 512, \JSON_THROW_ON_ERROR));
257
            $tags = $this->filterToOne(json_decode($item['tags'], true, 512, \JSON_THROW_ON_ERROR), 'id');
258
            $categories = $this->filterToMany(json_decode($item['categories'], true, 512, \JSON_THROW_ON_ERROR));
259
260
            $customFields = $this->takeItem('customFields', $context, $translations, $parentTranslations) ?? [];
261
262
            // MariaDB servers gives the result as string and not directly decoded
263
            // @codeCoverageIgnoreStart
264
            if (\is_string($customFields)) {
265
                $customFields = json_decode($customFields, true, 512, \JSON_THROW_ON_ERROR);
266
            }
267
            // @codeCoverageIgnoreEnd
268
269
            $document = [
270
                'id' => $id,
271
                'name' => $this->stripText($this->takeItem('name', $context, $translations, $parentTranslations) ?? ''),
272
                'description' => $this->stripText($this->takeItem('description', $context, $translations, $parentTranslations) ?? ''),
273
                'metaTitle' => $this->stripText($this->takeItem('metaTitle', $context, $translations, $parentTranslations) ?? ''),
274
                'metaDescription' => $this->stripText($this->takeItem('metaDescription', $context, $translations, $parentTranslations) ?? ''),
275
                'customSearchKeywords' => $this->takeItem('customSearchKeywords', $context, $translations, $parentTranslations) ?? '[]',
276
                'ratingAverage' => (float) $item['ratingAverage'],
277
                'active' => (bool) $item['active'],
278
                'available' => (bool) $item['available'],
279
                'isCloseout' => (bool) $item['isCloseout'],
280
                'shippingFree' => (bool) $item['shippingFree'],
281
                'markAsTopseller' => (bool) $item['markAsTopseller'],
282
                'customFields' => $this->formatCustomFields($customFields, $context),
283
                'visibilities' => $visibilities,
284
                'availableStock' => (int) $item['availableStock'],
285
                'productNumber' => $item['productNumber'],
286
                'ean' => $item['ean'],
287
                'displayGroup' => $item['displayGroup'],
288
                'sales' => (int) $item['sales'],
289
                'stock' => (int) $item['stock'],
290
                'weight' => (float) $item['weight'],
291
                'width' => (float) $item['width'],
292
                'length' => (float) $item['length'],
293
                'height' => (float) $item['height'],
294
                'manufacturerId' => $item['productManufacturerId'],
295
                'manufacturerNumber' => $item['manufacturerNumber'],
296
                'manufacturer' => [
297
                    'id' => $item['productManufacturerId'],
298
                    'name' => $this->takeItem('name', $context, $manufacturer) ?? '',
299
                    '_count' => 1,
300
                ],
301
                'releaseDate' => isset($item['releaseDate']) ? (new \DateTime($item['releaseDate']))->format('c') : null,
302
                'createdAt' => isset($item['createdAt']) ? (new \DateTime($item['createdAt']))->format('c') : null,
303
                'optionIds' => $optionIds,
304
                'options' => array_values(array_map(fn (string $optionId) => ['id' => $optionId, 'name' => $groups[$optionId]['name'], 'groupId' => $groups[$optionId]['property_group_id'], '_count' => 1], $optionIds)),
305
                'categories' => array_values(array_map(function ($category) use ($context) {
306
                    return [
307
                        'id' => $category[Defaults::LANGUAGE_SYSTEM]['id'],
308
                        'name' => $this->takeItem('name', $context, $category) ?? '',
309
                    ];
310
                }, $categories)),
311
                'categoriesRo' => array_values(array_map(fn (string $categoryId) => ['id' => $categoryId, '_count' => 1], $categoriesRo)),
312
                'properties' => array_values(array_map(fn (string $propertyId) => ['id' => $propertyId, 'name' => $groups[$propertyId]['name'], 'groupId' => $groups[$propertyId]['property_group_id'], '_count' => 1], $propertyIds)),
313
                'propertyIds' => $propertyIds,
314
                'taxId' => $item['taxId'],
315
                'tags' => array_values(array_map(fn (string $tagId) => ['id' => $tagId, 'name' => $tags[$tagId]['name'], '_count' => 1], $tagIds)),
316
                'tagIds' => $tagIds,
317
                'parentId' => $item['parentId'],
318
                'coverId' => $item['coverId'],
319
                'childCount' => (int) $item['childCount'],
320
            ];
321
322
            if ($item['cheapest_price_accessor']) {
323
                $cheapestPriceAccessor = json_decode($item['cheapest_price_accessor'], true, 512, \JSON_THROW_ON_ERROR);
324
325
                foreach ($cheapestPriceAccessor as $rule => $cheapestPriceCurrencies) {
326
                    foreach ($cheapestPriceCurrencies as $currency => $taxes) {
327
                        $key = 'cheapest_price_' . $rule . '_' . $currency . '_gross';
328
                        $document[$key] = $taxes['gross'];
329
330
                        $key = 'cheapest_price_' . $rule . '_' . $currency . '_net';
331
                        $document[$key] = $taxes['net'];
332
333
                        if (empty($taxes['percentage'])) {
334
                            continue;
335
                        }
336
337
                        $key = 'cheapest_price_' . $rule . '_' . $currency . '_gross_percentage';
338
                        $document[$key] = $taxes['percentage']['gross'];
339
340
                        $key = 'cheapest_price_' . $rule . '_' . $currency . '_net_percentage';
341
                        $document[$key] = $taxes['percentage']['net'];
342
                    }
343
                }
344
            }
345
346
            $documents[$id] = $document;
347
        }
348
349
        return $documents;
350
    }
351
352
    /**
353
     * @param array<string> $ids
354
     *
355
     * @return array<mixed>
356
     */
357
    private function fetchProducts(array $ids, Context $context): array
358
    {
359
        $sql = <<<'SQL'
360
SELECT
361
    LOWER(HEX(p.id)) AS id,
362
    IFNULL(p.active, pp.active) AS active,
363
    p.available AS available,
364
    CONCAT(
365
        '[',
366
            GROUP_CONCAT(DISTINCT
367
                JSON_OBJECT(
368
                    'languageId', lower(hex(product_main.language_id)),
369
                    'name', product_main.name,
370
                    'description', product_main.description,
371
                    'metaTitle', product_main.meta_title,
372
                    'metaDescription', product_main.meta_description,
373
                    'customSearchKeywords', product_main.custom_search_keywords,
374
                    'customFields', product_main.custom_fields
375
                )
376
            ),
377
        ']'
378
    ) as translation,
379
    CONCAT(
380
        '[',
381
            GROUP_CONCAT(DISTINCT
382
                JSON_OBJECT(
383
                    'languageId', lower(hex(product_parent.language_id)),
384
                    'name', product_parent.name,
385
                    'description', product_parent.description,
386
                    'metaTitle', product_parent.meta_title,
387
                    'metaDescription', product_parent.meta_description,
388
                    'customSearchKeywords', product_parent.custom_search_keywords,
389
                    'customFields', product_parent.custom_fields
390
                )
391
            ),
392
        ']'
393
    ) as translation_parent,
394
    CONCAT(
395
        '[',
396
            GROUP_CONCAT(DISTINCT
397
                JSON_OBJECT(
398
                    'languageId', lower(hex(product_manufacturer_translation.language_id)),
399
                    'name', product_manufacturer_translation.name
400
                )
401
            ),
402
        ']'
403
    ) as manufacturer_translation,
404
405
    CONCAT(
406
        '[',
407
        GROUP_CONCAT(DISTINCT
408
                JSON_OBJECT(
409
                    'id', lower(hex(tag.id)),
410
                    'name', tag.name
411
                )
412
            ),
413
        ']'
414
    ) as tags,
415
416
    CONCAT(
417
        '[',
418
        GROUP_CONCAT(DISTINCT
419
                JSON_OBJECT(
420
                    'id', lower(hex(category_translation.category_id)),
421
                    'languageId', lower(hex(category_translation.language_id)),
422
                    'name', category_translation.name
423
                )
424
            ),
425
        ']'
426
    ) as categories,
427
428
    IFNULL(p.manufacturer_number, pp.manufacturer_number) AS manufacturerNumber,
429
    IFNULL(p.available_stock, pp.available_stock) AS availableStock,
430
    IFNULL(p.rating_average, pp.rating_average) AS ratingAverage,
431
    p.product_number as productNumber,
432
    p.sales,
433
    LOWER(HEX(IFNULL(p.product_manufacturer_id, pp.product_manufacturer_id))) AS productManufacturerId,
434
    IFNULL(p.shipping_free, pp.shipping_free) AS shippingFree,
435
    IFNULL(p.is_closeout, pp.is_closeout) AS isCloseout,
436
    LOWER(HEX(IFNULL(p.product_media_id, pp.product_media_id))) AS coverId,
437
    IFNULL(p.weight, pp.weight) AS weight,
438
    IFNULL(p.length, pp.length) AS length,
439
    IFNULL(p.height, pp.height) AS height,
440
    IFNULL(p.width, pp.width) AS width,
441
    IFNULL(p.release_date, pp.release_date) AS releaseDate,
442
    IFNULL(p.created_at, pp.created_at) AS createdAt,
443
    IFNULL(p.category_tree, pp.category_tree) AS categoryIds,
444
    IFNULL(p.option_ids, pp.option_ids) AS optionIds,
445
    IFNULL(p.property_ids, pp.property_ids) AS propertyIds,
446
    IFNULL(p.tag_ids, pp.tag_ids) AS tagIds,
447
    LOWER(HEX(IFNULL(p.tax_id, pp.tax_id))) AS taxId,
448
    IFNULL(p.stock, pp.stock) AS stock,
449
    IFNULL(p.ean, pp.ean) AS ean,
450
    IFNULL(p.mark_as_topseller, pp.mark_as_topseller) AS markAsTopseller,
451
    p.auto_increment as autoIncrement,
452
    GROUP_CONCAT(CONCAT(product_visibility.visibility, ',', LOWER(HEX(product_visibility.sales_channel_id))) SEPARATOR '|') AS visibilities,
453
    p.display_group as displayGroup,
454
    IFNULL(p.cheapest_price_accessor, pp.cheapest_price_accessor) as cheapest_price_accessor,
455
    LOWER(HEX(p.parent_id)) as parentId,
456
    p.child_count as childCount
457
458
FROM product p
459
    LEFT JOIN product pp ON(p.parent_id = pp.id AND pp.version_id = :liveVersionId)
460
    LEFT JOIN product_visibility ON(product_visibility.product_id = p.visibilities AND product_visibility.product_version_id = p.version_id)
461
    LEFT JOIN product_translation product_main ON (product_main.product_id = p.id AND product_main.product_version_id = p.version_id AND product_main.language_id IN(:languageIds))
462
    LEFT JOIN product_translation product_parent ON (product_parent.product_id = p.parent_id AND product_parent.product_version_id = p.version_id AND product_parent.language_id IN(:languageIds))
463
464
    LEFT JOIN product_manufacturer_translation on (product_manufacturer_translation.product_manufacturer_id = IFNULL(p.product_manufacturer_id, pp.product_manufacturer_id) AND product_manufacturer_translation.product_manufacturer_version_id = p.version_id AND product_manufacturer_translation.language_id IN(:languageIds))
465
466
    LEFT JOIN product_tag ON (product_tag.product_id = p.tags AND product_tag.product_version_id = p.version_id)
467
    LEFT JOIN tag ON (tag.id = product_tag.tag_id)
468
469
    LEFT JOIN product_category ON (product_category.product_id = p.categories AND product_category.product_version_id = p.version_id)
470
    LEFT JOIN category_translation ON (category_translation.category_id = product_category.category_id AND category_translation.category_version_id = product_category.category_version_id AND category_translation.language_id IN(:languageIds))
471
472
WHERE p.id IN (:ids) AND p.version_id = :liveVersionId AND (p.child_count = 0 OR p.parent_id IS NOT NULL OR JSON_EXTRACT(`p`.`variant_listing_config`, "$.displayParent") = 1)
473
474
GROUP BY p.id
475
SQL;
476
477
        $data = $this->connection->fetchAllAssociative(
478
            $sql,
479
            [
480
                'ids' => $ids,
481
                'languageIds' => Uuid::fromHexToBytesList($context->getLanguageIdChain()),
482
                'liveVersionId' => Uuid::fromHexToBytes($context->getVersionId()),
483
            ],
484
            [
485
                'ids' => Connection::PARAM_STR_ARRAY,
486
                'languageIds' => Connection::PARAM_STR_ARRAY,
487
            ]
488
        );
489
490
        return FetchModeHelper::groupUnique($data);
491
    }
492
493
    /**
494
     * @return array<string, mixed>
495
     */
496
    private function getCustomFieldsMapping(Context $context): array
497
    {
498
        $fieldMapping = $this->getCustomFieldTypes($context);
499
        $mapping = [
500
            'type' => 'object',
501
            'dynamic' => true,
502
            'properties' => [],
503
        ];
504
505
        foreach ($fieldMapping as $name => $type) {
506
            /** @var array<mixed> $esType */
507
            $esType = CustomFieldUpdater::getTypeFromCustomFieldType($type);
508
509
            $mapping['properties'][$name] = $esType;
510
        }
511
512
        return $mapping;
513
    }
514
515
    /**
516
     * @param array<string, mixed> $customFields
517
     *
518
     * @return array<string, mixed>
519
     */
520
    private function formatCustomFields(array $customFields, Context $context): array
521
    {
522
        $types = $this->getCustomFieldTypes($context);
523
524
        foreach ($customFields as $name => $customField) {
525
            $type = $types[$name] ?? null;
526
527
            if ($type === null) {
528
                unset($customFields[$name]);
529
530
                continue;
531
            }
532
533
            if ($type === CustomFieldTypes::BOOL) {
534
                $customFields[$name] = (bool) $customField;
535
            } elseif (\is_int($customField)) {
536
                $customFields[$name] = (float) $customField;
537
            }
538
        }
539
540
        return $customFields;
541
    }
542
543
    /**
544
     * @param array<string> $propertyIds
545
     *
546
     * @return array<string, array{id: string, name: string, property_group_id: string, translations: string}>
547
     */
548
    private function fetchPropertyGroups(array $propertyIds, Context $context): array
549
    {
550
        $sql = <<<'SQL'
551
SELECT
552
       LOWER(HEX(id)) as id,
553
       LOWER(HEX(property_group_id)) as property_group_id,
554
       CONCAT(
555
               '[',
556
               GROUP_CONCAT(
557
                       JSON_OBJECT(
558
                               'languageId', lower(hex(property_group_option_translation.language_id)),
559
                               'name', property_group_option_translation.name
560
                           )
561
                   ),
562
               ']'
563
           ) as translations
564
FROM property_group_option
565
         LEFT JOIN property_group_option_translation
566
                   ON (property_group_option_translation.property_group_option_id = property_group_option.id AND
567
                       property_group_option_translation.language_id IN (:languageIds))
568
569
WHERE property_group_option.id in (:ids)
570
GROUP BY property_group_option.id
571
SQL;
572
573
        /** @var array<string, array{id: string, property_group_id: string, translations: string}> $options */
574
        $options = $this->connection->fetchAllAssociativeIndexed(
575
            $sql,
576
            [
577
                'ids' => Uuid::fromHexToBytesList($propertyIds),
578
                'languageIds' => Uuid::fromHexToBytesList($context->getLanguageIdChain()),
579
            ],
580
            [
581
                'ids' => Connection::PARAM_STR_ARRAY,
582
                'languageIds' => Connection::PARAM_STR_ARRAY,
583
            ]
584
        );
585
586
        foreach ($options as $optionId => $option) {
587
            $translation = $this->filterToOne(json_decode($option['translations'], true));
588
589
            $options[(string) $optionId]['name'] = (string) $this->takeItem('name', $context, $translation);
590
        }
591
592
        return $options;
593
    }
594
595
    /**
596
     * @return array<string, string>
597
     */
598
    private function getCustomFieldTypes(Context $context): array
599
    {
600
        if ($this->customFieldsTypes !== null) {
601
            return $this->customFieldsTypes;
602
        }
603
604
        $event = new ElasticsearchProductCustomFieldsMappingEvent($this->customMapping, $context);
605
        $this->eventDispatcher->dispatch($event);
606
607
        $this->customFieldsTypes = $event->getMappings();
608
609
        return $this->customFieldsTypes;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->customFieldsTypes returns the type null which is incompatible with the type-hinted return array.
Loading history...
610
    }
611
612
    /**
613
     * @param array<mixed> $items
614
     *
615
     * @return mixed|null
616
     */
617
    private function takeItem(string $key, Context $context, ...$items)
618
    {
619
        foreach ($context->getLanguageIdChain() as $languageId) {
620
            foreach ($items as $item) {
621
                if (isset($item[$languageId][$key])) {
622
                    return $item[$languageId][$key];
623
                }
624
            }
625
        }
626
627
        return null;
628
    }
629
630
    /**
631
     * @param array<mixed>[] $items
632
     *
633
     * @return array<int|string, mixed>
634
     */
635
    private function filterToOne(array $items, string $key = 'languageId'): array
636
    {
637
        $filtered = [];
638
639
        foreach ($items as $item) {
640
            // Empty row
641
            if ($item[$key] === null) {
642
                continue;
643
            }
644
645
            $filtered[$item[$key]] = $item;
646
        }
647
648
        return $filtered;
649
    }
650
651
    /**
652
     * @param array<mixed> $items
653
     *
654
     * @return array<mixed>
655
     */
656
    private function filterToMany(array $items): array
657
    {
658
        $filtered = [];
659
660
        foreach ($items as $item) {
661
            if ($item['id'] === null) {
662
                continue;
663
            }
664
665
            if (!isset($filtered[$item['id']])) {
666
                $filtered[$item['id']] = [];
667
            }
668
669
            $filtered[$item['id']][$item['languageId']] = $item;
670
        }
671
672
        return $filtered;
673
    }
674
}
675