Passed
Push — trunk ( 97fbe0...f153cd )
by Christian
28:35 queued 16:34
created

ElasticsearchProductDefinition::buildTermQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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