Passed
Push — trunk ( 654a13...700e2c )
by Christian
15:14 queued 02:51
created

ElasticsearchProductDefinition::fetchPropertyGroups()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 45
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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