Passed
Push — trunk ( 325349...f71779 )
by Christian
12:37 queued 16s
created

ElasticsearchProductDefinition   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 650
Duplicated Lines 0 %

Importance

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