Passed
Push — trunk ( bd931d...b51308 )
by Christian
13:33 queued 12s
created

CriteriaParser::parseTermsAggregation()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 46
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 23
nc 8
nop 4
dl 0
loc 46
rs 8.9297
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Elasticsearch\Framework\DataAbstractionLayer;
4
5
use OpenSearchDSL\Aggregation\AbstractAggregation;
6
use OpenSearchDSL\Aggregation\Bucketing;
7
use OpenSearchDSL\Aggregation\Bucketing\CompositeAggregation;
8
use OpenSearchDSL\Aggregation\Bucketing\NestedAggregation;
9
use OpenSearchDSL\Aggregation\Bucketing\ReverseNestedAggregation;
10
use OpenSearchDSL\Aggregation\Metric;
11
use OpenSearchDSL\Aggregation\Metric\ValueCountAggregation;
12
use OpenSearchDSL\BuilderInterface;
13
use OpenSearchDSL\Query\Compound\BoolQuery;
14
use OpenSearchDSL\Query\Joining\NestedQuery;
15
use OpenSearchDSL\Query\TermLevel\ExistsQuery;
16
use OpenSearchDSL\Query\TermLevel\PrefixQuery;
17
use OpenSearchDSL\Query\TermLevel\RangeQuery;
18
use OpenSearchDSL\Query\TermLevel\TermQuery;
19
use OpenSearchDSL\Query\TermLevel\TermsQuery;
20
use OpenSearchDSL\Query\TermLevel\WildcardQuery;
21
use OpenSearchDSL\Sort\FieldSort;
22
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
23
use Shopware\Core\Defaults;
24
use Shopware\Core\Framework\Context;
25
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
26
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
27
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
28
use Shopware\Core\Framework\DataAbstractionLayer\Field\BoolField;
29
use Shopware\Core\Framework\DataAbstractionLayer\Field\CustomFields;
30
use Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField;
31
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
32
use Shopware\Core\Framework\DataAbstractionLayer\Field\FloatField;
33
use Shopware\Core\Framework\DataAbstractionLayer\Field\IntField;
34
use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField;
35
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
36
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Aggregation;
37
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\DateHistogramAggregation;
38
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
39
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
40
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\AvgAggregation;
41
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
42
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
43
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MaxAggregation;
44
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MinAggregation;
45
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\RangeAggregation;
46
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\StatsAggregation;
47
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\SumAggregation;
48
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter;
49
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
50
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
51
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
52
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter;
53
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
54
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
55
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
56
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\PrefixFilter;
57
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
58
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\SingleFieldFilter;
59
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\SuffixFilter;
60
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\XOrFilter;
61
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\CountSorting;
62
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
63
use Shopware\Core\Framework\Log\Package;
64
use Shopware\Core\Framework\Uuid\Uuid;
65
use Shopware\Core\System\CustomField\CustomFieldService;
66
use Shopware\Elasticsearch\Framework\ElasticsearchDateHistogramAggregation;
67
use Shopware\Elasticsearch\Framework\ElasticsearchHelper;
68
use Shopware\Elasticsearch\Sort\CountSort;
69
70
#[Package('core')]
71
class CriteriaParser
72
{
73
    /**
74
     * @internal
75
     */
76
    public function __construct(
77
        private readonly EntityDefinitionQueryHelper $helper,
78
        private readonly CustomFieldService $customFieldService
79
    ) {
80
    }
81
82
    public function buildAccessor(EntityDefinition $definition, string $fieldName, Context $context): string
83
    {
84
        $root = $definition->getEntityName();
85
86
        $parts = explode('.', $fieldName);
87
        if ($root === $parts[0]) {
88
            array_shift($parts);
89
        }
90
91
        $field = $this->helper->getField($fieldName, $definition, $root, false);
92
        if ($field instanceof TranslatedField) {
93
            $ordered = [];
94
            foreach ($parts as $part) {
95
                $ordered[] = $part;
96
            }
97
            $parts = $ordered;
98
        }
99
100
        if (!$field instanceof PriceField) {
101
            return implode('.', $parts);
102
        }
103
104
        if (\in_array(end($parts), ['net', 'gross'], true)) {
105
            $taxState = end($parts);
106
            array_pop($parts);
107
        } elseif ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) {
108
            $taxState = 'gross';
109
        } else {
110
            $taxState = 'net';
111
        }
112
113
        $currencyId = $context->getCurrencyId();
114
        if (Uuid::isValid((string) end($parts))) {
115
            $currencyId = end($parts);
116
            array_pop($parts);
117
        }
118
119
        $parts[] = 'c_' . $currencyId;
120
        $parts[] = $taxState;
121
122
        return implode('.', $parts);
123
    }
124
125
    public function parseSorting(FieldSorting $sorting, EntityDefinition $definition, Context $context): FieldSort
126
    {
127
        if ($this->isCheapestPriceField($sorting->getField())) {
128
            return new FieldSort('_script', $sorting->getDirection(), null, [
129
                'type' => 'number',
130
                'script' => [
131
                    'id' => 'cheapest_price',
132
                    'params' => $this->getCheapestPriceParameters($context),
133
                ],
134
            ]);
135
        }
136
137
        if ($this->isCheapestPriceField($sorting->getField(), true)) {
138
            return new FieldSort('_script', $sorting->getDirection(), null, [
139
                'type' => 'number',
140
                'script' => [
141
                    'id' => 'cheapest_price_percentage',
142
                    'params' => ['accessors' => $this->getCheapestPriceAccessors($context, true)],
143
                ],
144
            ]);
145
        }
146
147
        $accessor = $this->buildAccessor($definition, $sorting->getField(), $context);
148
149
        if ($sorting instanceof CountSorting) {
150
            return new CountSort($accessor, $sorting->getDirection());
151
        }
152
153
        return new FieldSort($accessor, $sorting->getDirection());
154
    }
155
156
    public function parseAggregation(Aggregation $aggregation, EntityDefinition $definition, Context $context): ?AbstractAggregation
157
    {
158
        $fieldName = $this->buildAccessor($definition, $aggregation->getField(), $context);
159
160
        $fields = $aggregation->getFields();
161
162
        $path = null;
163
        if (\count($fields) > 0) {
164
            $path = $this->getNestedPath($definition, $fields[0]);
165
        }
166
167
        $esAggregation = $this->createAggregation($aggregation, $fieldName, $definition, $context);
168
169
        if (!$path || $aggregation instanceof FilterAggregation) {
170
            return $esAggregation;
171
        }
172
173
        $nested = new NestedAggregation($aggregation->getName(), $path);
174
        $nested->addAggregation($esAggregation);
175
176
        return $nested;
177
    }
178
179
    public function parseFilter(Filter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
180
    {
181
        return match (true) {
182
            $filter instanceof NotFilter => $this->parseNotFilter($filter, $definition, $root, $context),
183
            $filter instanceof MultiFilter => $this->parseMultiFilter($filter, $definition, $root, $context),
184
            $filter instanceof EqualsFilter => $this->parseEqualsFilter($filter, $definition, $context),
185
            $filter instanceof EqualsAnyFilter => $this->parseEqualsAnyFilter($filter, $definition, $context),
186
            $filter instanceof ContainsFilter => $this->parseContainsFilter($filter, $definition, $context),
187
            $filter instanceof PrefixFilter => $this->parsePrefixFilter($filter, $definition, $context),
188
            $filter instanceof SuffixFilter => $this->parseSuffixFilter($filter, $definition, $context),
189
            $filter instanceof RangeFilter => $this->parseRangeFilter($filter, $definition, $context),
190
            default => throw new \RuntimeException(sprintf('Unsupported filter %s', $filter::class)),
191
        };
192
    }
193
194
    protected function parseFilterAggregation(FilterAggregation $aggregation, EntityDefinition $definition, Context $context): AbstractAggregation
195
    {
196
        if ($aggregation->getAggregation() === null) {
197
            throw new \RuntimeException(sprintf('Filter aggregation %s contains no nested aggregation.', $aggregation->getName()));
198
        }
199
200
        $nested = $this->parseAggregation($aggregation->getAggregation(), $definition, $context);
201
        if ($nested === null) {
202
            throw new \RuntimeException(sprintf('Nested filter aggregation %s can not be parsed.', $aggregation->getName()));
203
        }
204
205
        // when aggregation inside the filter aggregation points to a nested object (e.g. product.properties.id) we have to add all filters
206
        // which points to the same association to the same "nesting" level like the nested aggregation for this association
207
        $path = $nested instanceof NestedAggregation ? $nested->getPath() : null;
0 ignored issues
show
introduced by
$nested is always a sub-type of OpenSearchDSL\Aggregatio...eting\NestedAggregation.
Loading history...
208
        $bool = new BoolQuery();
209
210
        $filters = [];
211
        foreach ($aggregation->getFilter() as $filter) {
212
            $query = $this->parseFilter($filter, $definition, $definition->getEntityName(), $context);
213
214
            if (!$query instanceof NestedQuery) {
215
                $filters[] = new Bucketing\FilterAggregation($aggregation->getName(), $query);
216
217
                continue;
218
            }
219
220
            // same property path as the "real" aggregation
221
            if ($query->getPath() === $path) {
222
                $bool->add($query->getQuery());
223
224
                continue;
225
            }
226
227
            // query points to a nested document property - we have to define that the filter points to this field
228
            $parsed = new NestedAggregation($aggregation->getName(), $query->getPath());
229
230
            // now we can defined a filter which points to the nested field (remove NestedQuery layer)
231
            $filter = new Bucketing\FilterAggregation($aggregation->getName(), $query->getQuery());
232
233
            // afterwards we reset the nesting to allow following filters to point to another nested property
234
            $reverse = new ReverseNestedAggregation($aggregation->getName());
235
236
            $filter->addAggregation($reverse);
237
238
            $parsed->addAggregation($filter);
239
240
            $filters[] = $parsed;
241
        }
242
243
        // nested aggregation should have filters - we have to remap the nesting
244
        $mapped = $nested;
245
        if (\count($bool->getQueries()) > 0 && $nested instanceof NestedAggregation) {
246
            $real = $nested->getAggregation($nested->getName());
247
            if (!$real instanceof AbstractAggregation) {
248
                throw new \RuntimeException(sprintf('Nested filter aggregation %s can not be parsed.', $aggregation->getName()));
249
            }
250
251
            $filter = new Bucketing\FilterAggregation($aggregation->getName(), $bool);
252
            $filter->addAggregation($real);
253
254
            $mapped = new NestedAggregation($aggregation->getName(), $nested->getPath());
255
            $mapped->addAggregation($filter);
256
        }
257
258
        // at this point we have to walk over all filters and create one nested filter for it
259
        $parent = null;
260
        $root = $mapped;
261
        foreach ($filters as $filter) {
262
            if ($parent === null) {
263
                $parent = $filter;
264
                $root = $filter;
265
266
                continue;
267
            }
268
269
            $parent->addAggregation($filter);
270
271
            if (!$filter instanceof NestedAggregation) {
272
                $parent = $filter;
273
274
                continue;
275
            }
276
277
            $filter = $filter->getAggregation($filter->getName());
278
            if (!$filter instanceof AbstractAggregation) {
279
                throw new \RuntimeException('Expected nested+filter+reverse pattern for parsed filters to set next parent correctly');
280
            }
281
282
            $parent = $filter->getAggregation($filter->getName());
283
            if (!$parent instanceof AbstractAggregation) {
284
                throw new \RuntimeException('Expected nested+filter+reverse pattern for parsed filters to set next parent correctly');
285
            }
286
        }
287
288
        // it can happen, that $parent is not defined if the "real" aggregation is a nested and all filters points to the same property
289
        // than we return the following structure:  [nested-agg] + filter-agg + real-agg    ( [] = optional )
290
        if ($parent === null) {
291
            return $root;
292
        }
293
294
        // at this point we have some other filters which point to another nested object as the "real" aggregation
295
        // than we return the following structure:  [nested-agg] + filter-agg + [reverse-nested-agg] + [nested-agg] + real-agg   ( [] = optional )
296
        $parent->addAggregation($mapped);
297
298
        return $root;
299
    }
300
301
    protected function parseTermsAggregation(TermsAggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): AbstractAggregation
302
    {
303
        if ($aggregation->getSorting() === null) {
304
            $terms = new Bucketing\TermsAggregation($aggregation->getName(), $fieldName);
305
306
            if ($nested = $aggregation->getAggregation()) {
307
                $terms->addAggregation(
308
                    $this->parseNestedAggregation($nested, $definition, $context)
309
                );
310
            }
311
312
            // set default size to 10.000 => max for default configuration
313
            $terms->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE);
314
315
            if ($aggregation->getLimit()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aggregation->getLimit() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
316
                $terms->addParameter('size', (string) $aggregation->getLimit());
317
            }
318
319
            return $terms;
320
        }
321
322
        $composite = new CompositeAggregation($aggregation->getName());
323
324
        $accessor = $this->buildAccessor($definition, $aggregation->getSorting()->getField(), $context);
325
326
        $sorting = new Bucketing\TermsAggregation($aggregation->getName() . '.sorting', $accessor);
327
        $sorting->addParameter('order', $aggregation->getSorting()->getDirection());
328
        $composite->addSource($sorting);
329
330
        $terms = new Bucketing\TermsAggregation($aggregation->getName() . '.key', $fieldName);
331
        $composite->addSource($terms);
332
333
        if ($nested = $aggregation->getAggregation()) {
334
            $composite->addAggregation(
335
                $this->parseNestedAggregation($nested, $definition, $context)
336
            );
337
        }
338
339
        // set default size to 10.000 => max for default configuration
340
        $composite->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE);
341
342
        if ($aggregation->getLimit()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aggregation->getLimit() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
343
            $composite->addParameter('size', (string) $aggregation->getLimit());
344
        }
345
346
        return $composite;
347
    }
348
349
    protected function parseStatsAggregation(StatsAggregation $aggregation, string $fieldName, Context $context): Metric\StatsAggregation
350
    {
351
        if ($this->isCheapestPriceField($aggregation->getField())) {
352
            return new Metric\StatsAggregation($aggregation->getName(), null, [
353
                'id' => 'cheapest_price',
354
                'params' => $this->getCheapestPriceParameters($context),
355
            ]);
356
        }
357
358
        if ($this->isCheapestPriceField($aggregation->getField(), true)) {
359
            return new Metric\StatsAggregation($aggregation->getName(), null, [
360
                'id' => 'cheapest_price_percentage',
361
                'params' => ['accessors' => $this->getCheapestPriceAccessors($context, true)],
362
            ]);
363
        }
364
365
        return new Metric\StatsAggregation($aggregation->getName(), $fieldName);
366
    }
367
368
    protected function parseEntityAggregation(EntityAggregation $aggregation, string $fieldName): Bucketing\TermsAggregation
369
    {
370
        $bucketingAggregation = new Bucketing\TermsAggregation($aggregation->getName(), $fieldName);
371
372
        $bucketingAggregation->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE);
373
374
        return $bucketingAggregation;
375
    }
376
377
    protected function parseDateHistogramAggregation(DateHistogramAggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): CompositeAggregation
378
    {
379
        $composite = new CompositeAggregation($aggregation->getName());
380
381
        if ($fieldSorting = $aggregation->getSorting()) {
382
            $accessor = $this->buildAccessor($definition, $fieldSorting->getField(), $context);
383
384
            $sorting = new Bucketing\TermsAggregation($aggregation->getName() . '.sorting', $accessor);
385
            $sorting->addParameter('order', $fieldSorting->getDirection());
386
387
            $composite->addSource($sorting);
388
        }
389
390
        $histogram = new ElasticsearchDateHistogramAggregation(
391
            $aggregation->getName() . '.key',
392
            $fieldName,
393
            $aggregation->getInterval(),
394
            'yyyy-MM-dd HH:mm:ss'
395
        );
396
397
        if ($aggregation->getTimeZone()) {
398
            $histogram->addParameter('time_zone', $aggregation->getTimeZone());
399
        }
400
401
        $composite->addSource($histogram);
402
403
        if ($nested = $aggregation->getAggregation()) {
404
            $composite->addAggregation(
405
                $this->parseNestedAggregation($nested, $definition, $context)
406
            );
407
        }
408
409
        return $composite;
410
    }
411
412
    protected function parseRangeAggregation(RangeAggregation $aggregation, string $fieldName): Bucketing\RangeAggregation
413
    {
414
        return new Bucketing\RangeAggregation(
415
            $aggregation->getName(),
416
            $fieldName,
417
            $aggregation->getRanges()
418
        );
419
    }
420
421
    private function getCheapestPriceParameters(Context $context): array
422
    {
423
        return [
424
            'accessors' => $this->getCheapestPriceAccessors($context),
425
            'decimals' => 10 ** $context->getRounding()->getDecimals(),
426
            'round' => $this->useCashRounding($context),
427
            'multiplier' => 100 / ($context->getRounding()->getInterval() * 100),
428
        ];
429
    }
430
431
    private function useCashRounding(Context $context): bool
432
    {
433
        if ($context->getRounding()->getDecimals() !== 2) {
434
            return false;
435
        }
436
437
        if ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) {
438
            return true;
439
        }
440
441
        return $context->getRounding()->roundForNet();
442
    }
443
444
    private function getCheapestPriceAccessors(Context $context, bool $percentage = false): array
445
    {
446
        $accessors = [];
447
448
        $tax = $context->getTaxState() === CartPrice::TAX_STATE_GROSS ? 'gross' : 'net';
449
450
        $ruleIds = array_merge($context->getRuleIds(), ['default']);
451
452
        foreach ($ruleIds as $ruleId) {
453
            $key = implode('_', [
454
                'cheapest_price',
455
                'rule' . $ruleId,
456
                'currency' . $context->getCurrencyId(),
457
                $tax,
458
            ]);
459
460
            if ($percentage) {
461
                $key .= '_percentage';
462
            }
463
464
            $accessors[] = ['key' => $key, 'factor' => 1];
465
466
            if ($context->getCurrencyId() === Defaults::CURRENCY) {
467
                continue;
468
            }
469
470
            $key = implode('_', [
471
                'cheapest_price',
472
                'rule' . $ruleId,
473
                'currency' . Defaults::CURRENCY,
474
                $tax,
475
            ]);
476
477
            if ($percentage) {
478
                $key .= '_percentage';
479
            }
480
481
            $accessors[] = ['key' => $key, 'factor' => $context->getCurrencyFactor()];
482
        }
483
484
        return $accessors;
485
    }
486
487
    private function parseNestedAggregation(Aggregation $aggregation, EntityDefinition $definition, Context $context): AbstractAggregation
488
    {
489
        $fieldName = $this->buildAccessor($definition, $aggregation->getField(), $context);
490
491
        return $this->createAggregation($aggregation, $fieldName, $definition, $context);
492
    }
493
494
    private function createAggregation(Aggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): AbstractAggregation
495
    {
496
        return match (true) {
497
            $aggregation instanceof StatsAggregation => $this->parseStatsAggregation($aggregation, $fieldName, $context),
498
            $aggregation instanceof AvgAggregation => new Metric\AvgAggregation($aggregation->getName(), $fieldName),
499
            $aggregation instanceof EntityAggregation => $this->parseEntityAggregation($aggregation, $fieldName),
500
            $aggregation instanceof MaxAggregation => new Metric\MaxAggregation($aggregation->getName(), $fieldName),
501
            $aggregation instanceof MinAggregation => new Metric\MinAggregation($aggregation->getName(), $fieldName),
502
            $aggregation instanceof SumAggregation => new Metric\SumAggregation($aggregation->getName(), $fieldName),
503
            $aggregation instanceof CountAggregation => new ValueCountAggregation($aggregation->getName(), $fieldName),
504
            $aggregation instanceof FilterAggregation => $this->parseFilterAggregation($aggregation, $definition, $context),
505
            $aggregation instanceof TermsAggregation => $this->parseTermsAggregation($aggregation, $fieldName, $definition, $context),
0 ignored issues
show
Bug introduced by
$aggregation of type Shopware\Core\Framework\...ucket\FilterAggregation is incompatible with the type Shopware\Core\Framework\...Bucket\TermsAggregation expected by parameter $aggregation of Shopware\Elasticsearch\F...parseTermsAggregation(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

505
            $aggregation instanceof TermsAggregation => $this->parseTermsAggregation(/** @scrutinizer ignore-type */ $aggregation, $fieldName, $definition, $context),
Loading history...
506
            $aggregation instanceof DateHistogramAggregation => $this->parseDateHistogramAggregation($aggregation, $fieldName, $definition, $context),
0 ignored issues
show
Bug introduced by
$aggregation of type Shopware\Core\Framework\...ucket\FilterAggregation is incompatible with the type Shopware\Core\Framework\...ateHistogramAggregation expected by parameter $aggregation of Shopware\Elasticsearch\F...eHistogramAggregation(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

506
            $aggregation instanceof DateHistogramAggregation => $this->parseDateHistogramAggregation(/** @scrutinizer ignore-type */ $aggregation, $fieldName, $definition, $context),
Loading history...
507
            $aggregation instanceof RangeAggregation => $this->parseRangeAggregation($aggregation, $fieldName),
0 ignored issues
show
Bug introduced by
$aggregation of type Shopware\Core\Framework\...ucket\FilterAggregation is incompatible with the type Shopware\Core\Framework\...Metric\RangeAggregation expected by parameter $aggregation of Shopware\Elasticsearch\F...parseRangeAggregation(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

507
            $aggregation instanceof RangeAggregation => $this->parseRangeAggregation(/** @scrutinizer ignore-type */ $aggregation, $fieldName),
Loading history...
508
            default => throw new \RuntimeException(sprintf('Provided aggregation of class %s not supported', $aggregation::class)),
509
        };
510
    }
511
512
    private function parseEqualsFilter(EqualsFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
513
    {
514
        $fieldName = $this->buildAccessor($definition, $filter->getField(), $context);
515
516
        if ($filter->getValue() === null) {
517
            $query = new BoolQuery();
518
            $query->add(new ExistsQuery($fieldName), BoolQuery::MUST_NOT);
519
520
            return $this->createNestedQuery($query, $definition, $filter->getField());
521
        }
522
523
        $value = $this->parseValue($definition, $filter, $filter->getValue());
524
525
        $query = new TermQuery($fieldName, $value);
526
527
        return $this->createNestedQuery($query, $definition, $filter->getField());
528
    }
529
530
    private function parseEqualsAnyFilter(EqualsAnyFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
531
    {
532
        $fieldName = $this->buildAccessor($definition, $filter->getField(), $context);
533
534
        $value = $this->parseValue($definition, $filter, \array_values($filter->getValue()));
535
536
        return $this->createNestedQuery(
537
            new TermsQuery($fieldName, $value),
538
            $definition,
539
            $filter->getField()
540
        );
541
    }
542
543
    private function parseContainsFilter(ContainsFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
544
    {
545
        $accessor = $this->buildAccessor($definition, $filter->getField(), $context);
546
547
        /** @var string $value */
548
        $value = $filter->getValue();
549
550
        return $this->createNestedQuery(
551
            new WildcardQuery($accessor, '*' . $value . '*'),
552
            $definition,
553
            $filter->getField()
554
        );
555
    }
556
557
    private function parsePrefixFilter(PrefixFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
558
    {
559
        $accessor = $this->buildAccessor($definition, $filter->getField(), $context);
560
561
        $value = $filter->getValue();
562
563
        return $this->createNestedQuery(
564
            new PrefixQuery($accessor, $value),
565
            $definition,
566
            $filter->getField()
567
        );
568
    }
569
570
    private function parseSuffixFilter(SuffixFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
571
    {
572
        $accessor = $this->buildAccessor($definition, $filter->getField(), $context);
573
574
        $value = $filter->getValue();
575
576
        return $this->createNestedQuery(
577
            new WildcardQuery($accessor, '*' . $value),
578
            $definition,
579
            $filter->getField()
580
        );
581
    }
582
583
    private function parseRangeFilter(RangeFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
584
    {
585
        if ($this->isCheapestPriceField($filter->getField())) {
586
            return new ScriptIdQuery('cheapest_price_filter', [
587
                'params' => array_merge(
588
                    $this->getRangeParameters($filter),
589
                    $this->getCheapestPriceParameters($context)
590
                ),
591
            ]);
592
        }
593
594
        if ($this->isCheapestPriceField($filter->getField(), true)) {
595
            return new ScriptIdQuery('cheapest_price_percentage_filter', [
596
                'params' => array_merge(
597
                    $this->getRangeParameters($filter),
598
                    ['accessors' => $this->getCheapestPriceAccessors($context, true)]
599
                ),
600
            ]);
601
        }
602
603
        $accessor = $this->buildAccessor($definition, $filter->getField(), $context);
604
605
        return $this->createNestedQuery(
606
            new RangeQuery($accessor, $this->parseValue($definition, $filter, $filter->getParameters())),
607
            $definition,
608
            $filter->getField()
609
        );
610
    }
611
612
    private function isCheapestPriceField(string $field, bool $percentage = false): bool
613
    {
614
        if ($percentage) {
615
            $haystack = ['product.cheapestPrice.percentage', 'cheapestPrice.percentage'];
616
        } else {
617
            $haystack = ['product.cheapestPrice', 'cheapestPrice'];
618
        }
619
620
        return \in_array($field, $haystack, true);
621
    }
622
623
    private function getRangeParameters(RangeFilter $filter): array
624
    {
625
        $params = [];
626
        foreach ($filter->getParameters() as $key => $value) {
627
            $params[$key] = (float) $value;
628
        }
629
630
        return $params;
631
    }
632
633
    private function parseNotFilter(NotFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
634
    {
635
        $bool = new BoolQuery();
636
        if (\count($filter->getQueries()) === 0) {
637
            return $bool;
638
        }
639
640
        if (\count($filter->getQueries()) === 1) {
641
            $bool->add(
642
                $this->parseFilter($filter->getQueries()[0], $definition, $root, $context),
643
                BoolQuery::MUST_NOT
644
            );
645
646
            return $bool;
647
        }
648
649
        $multiFilter = match ($filter->getOperator()) {
650
            MultiFilter::CONNECTION_OR => new OrFilter(),
651
            MultiFilter::CONNECTION_XOR => new XOrFilter(),
652
            default => new AndFilter(),
653
        };
654
655
        foreach ($filter->getQueries() as $query) {
656
            $multiFilter->addQuery($query);
657
        }
658
659
        $bool->add(
660
            $this->parseFilter($multiFilter, $definition, $root, $context),
661
            BoolQuery::MUST_NOT
662
        );
663
664
        return $bool;
665
    }
666
667
    private function parseMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
668
    {
669
        return match ($filter->getOperator()) {
670
            MultiFilter::CONNECTION_OR => $this->parseOrMultiFilter($filter, $definition, $root, $context),
671
            MultiFilter::CONNECTION_AND => $this->parseAndMultiFilter($filter, $definition, $root, $context),
672
            MultiFilter::CONNECTION_XOR => $this->parseXorMultiFilter($filter, $definition, $root, $context),
673
            default => throw new \InvalidArgumentException('Operator ' . $filter->getOperator() . ' not allowed'),
674
        };
675
    }
676
677
    private function parseAndMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
678
    {
679
        $grouped = [];
680
        $bool = new BoolQuery();
681
682
        foreach ($filter->getQueries() as $nested) {
683
            $query = $this->parseFilter($nested, $definition, $root, $context);
684
685
            if (!$query instanceof NestedQuery) {
686
                $bool->add($query, BoolQuery::MUST);
687
688
                continue;
689
            }
690
691
            if (!\array_key_exists($query->getPath(), $grouped)) {
692
                $grouped[$query->getPath()] = new BoolQuery();
693
                $bool->add(new NestedQuery($query->getPath(), $grouped[$query->getPath()]));
694
            }
695
696
            $grouped[$query->getPath()]->add($query->getQuery());
697
        }
698
699
        return $bool;
700
    }
701
702
    private function parseOrMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
703
    {
704
        $bool = new BoolQuery();
705
706
        foreach ($filter->getQueries() as $nested) {
707
            $bool->add(
708
                $this->parseFilter($nested, $definition, $root, $context),
709
                BoolQuery::SHOULD
710
            );
711
        }
712
713
        return $bool;
714
    }
715
716
    private function parseXorMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
717
    {
718
        $bool = new BoolQuery();
719
720
        foreach ($filter->getQueries() as $nested) {
721
            $xorQuery = new BoolQuery();
722
            foreach ($filter->getQueries() as $mustNot) {
723
                if ($nested === $mustNot) {
724
                    $xorQuery->add($this->parseFilter($nested, $definition, $root, $context), BoolQuery::MUST);
725
726
                    continue;
727
                }
728
729
                $xorQuery->add($this->parseFilter($mustNot, $definition, $root, $context), BoolQuery::MUST_NOT);
730
            }
731
732
            $bool->add(
733
                $xorQuery,
734
                BoolQuery::SHOULD
735
            );
736
        }
737
738
        return $bool;
739
    }
740
741
    private function createNestedQuery(BuilderInterface $query, EntityDefinition $definition, string $field): BuilderInterface
742
    {
743
        $path = $this->getNestedPath($definition, $field);
744
745
        if ($path) {
746
            return new NestedQuery($path, $query);
747
        }
748
749
        return $query;
750
    }
751
752
    private function getField(EntityDefinition $definition, string $fieldName): ?Field
753
    {
754
        $root = $definition->getEntityName();
755
756
        $parts = explode('.', $fieldName);
757
        if ($root === $parts[0]) {
758
            array_shift($parts);
759
        }
760
761
        return $this->helper->getField($fieldName, $definition, $root, false);
762
    }
763
764
    private function getNestedPath(EntityDefinition $definition, string $accessor): ?string
765
    {
766
        if (mb_strpos($accessor, $definition->getEntityName() . '.') === false) {
767
            $accessor = $definition->getEntityName() . '.' . $accessor;
768
        }
769
770
        $fields = EntityDefinitionQueryHelper::getFieldsOfAccessor($definition, $accessor);
771
772
        $path = [];
773
        foreach ($fields as $field) {
774
            if (!$field instanceof AssociationField) {
775
                break;
776
            }
777
778
            $path[] = $field->getPropertyName();
779
        }
780
781
        if (empty($path)) {
782
            return null;
783
        }
784
785
        return implode('.', $path);
786
    }
787
788
    private function parseValue(EntityDefinition $definition, SingleFieldFilter $filter, mixed $value): mixed
789
    {
790
        $field = $this->getField($definition, $filter->getField());
791
792
        if ($field instanceof TranslatedField) {
793
            $field = EntityDefinitionQueryHelper::getTranslatedField($definition, $field);
794
        }
795
796
        if ($field instanceof CustomFields) {
797
            $accessor = \explode('.', $filter->getField());
798
            $last = \array_pop($accessor);
799
800
            $temp = $this->customFieldService->getCustomField($last);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $temp is correct as $this->customFieldService->getCustomField($last) targeting Shopware\Core\System\Cus...rvice::getCustomField() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
801
802
            $field = $temp ?? $field;
803
        }
804
805
        if ($field instanceof BoolField) {
806
            return match (true) {
807
                $value === null => null,
808
                \is_array($value) => \array_map(fn ($value) => (bool) $value, $value),
809
                default => (bool) $value,
810
            };
811
        }
812
813
        if ($field instanceof DateTimeField) {
814
            return match (true) {
815
                $value === null => null,
816
                \is_array($value) => \array_map(fn ($value) => (new \DateTime($value))->format(Defaults::STORAGE_DATE_TIME_FORMAT), $value),
817
                default => (new \DateTime($value))->format(Defaults::STORAGE_DATE_TIME_FORMAT),
818
            };
819
        }
820
821
        if ($field instanceof FloatField) {
822
            return match (true) {
823
                $value === null => null,
824
                \is_array($value) => \array_map(fn ($value) => (float) $value, $value),
825
                default => (float) $value,
826
            };
827
        }
828
829
        if ($field instanceof IntField) {
830
            return match (true) {
831
                $value === null => null,
832
                \is_array($value) => \array_map(fn ($value) => (int) $value, $value),
833
                default => (int) $value,
834
            };
835
        }
836
837
        return $value;
838
    }
839
}
840