Passed
Push — master ( c6cb64...3f2e18 )
by Christian
11:15 queued 12s
created

CriteriaParser::parseNestedAggregation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Elasticsearch\Framework\DataAbstractionLayer;
4
5
use ONGR\ElasticsearchDSL\Aggregation\AbstractAggregation;
6
use ONGR\ElasticsearchDSL\Aggregation\Bucketing;
7
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\CompositeAggregation;
8
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\NestedAggregation;
9
use ONGR\ElasticsearchDSL\Aggregation\Metric;
10
use ONGR\ElasticsearchDSL\Aggregation\Metric\ValueCountAggregation;
11
use ONGR\ElasticsearchDSL\BuilderInterface;
12
use ONGR\ElasticsearchDSL\Query\Compound\BoolQuery;
13
use ONGR\ElasticsearchDSL\Query\Joining\NestedQuery;
14
use ONGR\ElasticsearchDSL\Query\TermLevel\ExistsQuery;
15
use ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery;
16
use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery;
17
use ONGR\ElasticsearchDSL\Query\TermLevel\TermsQuery;
18
use ONGR\ElasticsearchDSL\Query\TermLevel\WildcardQuery;
19
use ONGR\ElasticsearchDSL\Sort\FieldSort;
20
use Shopware\Core\Framework\Context;
21
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
22
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
23
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
24
use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField;
25
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
26
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Aggregation;
27
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\DateHistogramAggregation;
28
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
29
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
30
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\AvgAggregation;
31
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
32
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
33
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MaxAggregation;
34
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MinAggregation;
35
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\StatsAggregation;
36
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\SumAggregation;
37
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
38
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
39
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
40
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter;
41
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
42
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
43
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
44
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
45
use Shopware\Elasticsearch\Framework\ElasticsearchHelper;
46
47
class CriteriaParser
48
{
49
    /**
50
     * @var EntityDefinitionQueryHelper
51
     */
52
    private $helper;
53
54
    public function __construct(EntityDefinitionQueryHelper $helper)
55
    {
56
        $this->helper = $helper;
57
    }
58
59
    public function buildAccessor(EntityDefinition $definition, string $fieldName, Context $context): string
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

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

59
    public function buildAccessor(EntityDefinition $definition, string $fieldName, /** @scrutinizer ignore-unused */ Context $context): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
60
    {
61
        $root = $definition->getEntityName();
62
63
        $parts = explode('.', $fieldName);
64
        if ($root === $parts[0]) {
65
            array_shift($parts);
66
        }
67
68
        $field = $this->helper->getField($fieldName, $definition, $root, false);
69
        if ($field instanceof TranslatedField) {
70
            $ordered = [];
71
            foreach ($parts as $part) {
72
                if ($part === $field->getPropertyName()) {
73
                    $ordered[] = 'translated';
74
                }
75
                $ordered[] = $part;
76
            }
77
            $parts = $ordered;
78
        }
79
80
        if ($field instanceof PriceField) {
81
            $parts[] = 'gross';
82
        }
83
84
        return implode('.', $parts);
85
    }
86
87
    public function parseSorting(FieldSorting $sorting, EntityDefinition $definition, Context $context): FieldSort
88
    {
89
        $accessor = $this->buildAccessor($definition, $sorting->getField(), $context);
90
91
        return new FieldSort($accessor, $sorting->getDirection());
92
    }
93
94
    public function parseAggregation(Aggregation $aggregation, EntityDefinition $definition, Context $context): ?AbstractAggregation
95
    {
96
        $fieldName = $this->buildAccessor($definition, $aggregation->getField(), $context);
97
98
        $fields = $aggregation->getFields();
99
100
        $path = null;
101
        if (\count($fields) > 0) {
102
            $path = $this->getNestedPath($definition, $fields[0]);
103
        }
104
105
        $esAggregation = $this->createAggregation($aggregation, $fieldName, $definition, $context);
106
107
        if (!$path) {
108
            return $esAggregation;
109
        }
110
111
        $nested = new NestedAggregation($aggregation->getName(), $path);
112
        $nested->addAggregation($esAggregation);
113
114
        return $nested;
115
    }
116
117
    public function parseFilter(Filter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
118
    {
119
        switch (true) {
120
            case $filter instanceof NotFilter:
121
                return $this->parseNotFilter($filter, $definition, $root, $context);
122
123
            case $filter instanceof MultiFilter:
124
                return $this->parseMultiFilter($filter, $definition, $root, $context);
125
126
            case $filter instanceof EqualsFilter:
127
                return $this->parseEqualsFilter($filter, $definition, $context);
128
129
            case $filter instanceof EqualsAnyFilter:
130
                return $this->parseEqualsAnyFilter($filter, $definition, $context);
131
132
            case $filter instanceof ContainsFilter:
133
                return $this->parseContainsFilter($filter, $definition, $context);
134
135
            case $filter instanceof RangeFilter:
136
                return $this->parseRangeFilter($filter, $definition, $context);
137
138
            default:
139
                throw new \RuntimeException(sprintf('Unsupported filter %s', \get_class($filter)));
140
        }
141
    }
142
143
    protected function parseFilterAggregation(FilterAggregation $aggregation, EntityDefinition $definition, Context $context): Bucketing\FilterAggregation
144
    {
145
        $query = new BoolQuery();
146
        foreach ($aggregation->getFilter() as $filter) {
147
            $parsed = $this->parseFilter($filter, $definition, $definition->getEntityName(), $context);
148
            if ($parsed instanceof NestedQuery) {
149
                $parsed = $parsed->getQuery();
150
            }
151
            $query->add($parsed);
152
        }
153
154
        $filter = new Bucketing\FilterAggregation($aggregation->getName(), $query);
155
156
        $nested = $aggregation->getAggregation();
157
158
        if (!$nested) {
159
            throw new \RuntimeException(sprintf('Filter aggregation %s contains no nested aggregation.', $aggregation->getName()));
160
        }
161
162
        $filter->addAggregation(
163
            $this->parseNestedAggregation($nested, $definition, $context)
164
        );
165
166
        return $filter;
167
    }
168
169
    protected function parseTermsAggregation(TermsAggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): AbstractAggregation
170
    {
171
        if ($aggregation->getSorting() === null) {
172
            $terms = new Bucketing\TermsAggregation($aggregation->getName(), $fieldName);
173
174
            if ($nested = $aggregation->getAggregation()) {
175
                $terms->addAggregation(
176
                    $this->parseNestedAggregation($nested, $definition, $context)
177
                );
178
            }
179
180
            // set default size to 10.000 => max for default configuration
181
            $terms->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE);
182
183
            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...
184
                $terms->addParameter('size', (string) $aggregation->getLimit());
185
            }
186
187
            return $terms;
188
        }
189
190
        $composite = new CompositeAggregation($aggregation->getName());
191
192
        $accessor = $this->buildAccessor($definition, $aggregation->getSorting()->getField(), $context);
193
194
        $sorting = new Bucketing\TermsAggregation($aggregation->getName() . '.sorting', $accessor);
195
        $sorting->addParameter('order', $aggregation->getSorting()->getDirection());
196
        $composite->addSource($sorting);
197
198
        $terms = new Bucketing\TermsAggregation($aggregation->getName() . '.key', $fieldName);
199
        $composite->addSource($terms);
200
201
        if ($nested = $aggregation->getAggregation()) {
202
            $composite->addAggregation(
203
                $this->parseNestedAggregation($nested, $definition, $context)
204
            );
205
        }
206
207
        // set default size to 10.000 => max for default configuration
208
        $composite->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE);
209
210
        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...
211
            $composite->addParameter('size', (string) $aggregation->getLimit());
212
        }
213
214
        return $composite;
215
    }
216
217
    protected function parseEntityAggregation(EntityAggregation $aggregation, string $fieldName): Bucketing\TermsAggregation
218
    {
219
        $bucketingAggregation = new Bucketing\TermsAggregation($aggregation->getName(), $fieldName);
220
221
        $bucketingAggregation->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE);
222
223
        return $bucketingAggregation;
224
    }
225
226
    protected function parseDateHistogramAggregation(DateHistogramAggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): CompositeAggregation
227
    {
228
        $composite = new CompositeAggregation($aggregation->getName());
229
230
        if ($aggregation->getSorting()) {
231
            $accessor = $this->buildAccessor($definition, $aggregation->getSorting()->getField(), $context);
232
233
            $sorting = new Bucketing\TermsAggregation($aggregation->getName() . '.sorting', $accessor);
234
            $sorting->addParameter('order', $aggregation->getSorting()->getDirection());
235
236
            $composite->addSource($sorting);
237
        }
238
239
        $histogram = new Bucketing\DateHistogramAggregation(
240
            $aggregation->getName() . '.key',
241
            $fieldName,
242
            $aggregation->getInterval(),
243
            'yyyy-MM-dd HH:mm:ss'
244
        );
245
        $composite->addSource($histogram);
246
247
        if ($nested = $aggregation->getAggregation()) {
248
            $composite->addAggregation(
249
                $this->parseNestedAggregation($nested, $definition, $context)
250
            );
251
        }
252
253
        return $composite;
254
    }
255
256
    private function parseNestedAggregation(Aggregation $aggregation, EntityDefinition $definition, Context $context): AbstractAggregation
257
    {
258
        $fieldName = $this->buildAccessor($definition, $aggregation->getField(), $context);
259
260
        return $this->createAggregation($aggregation, $fieldName, $definition, $context);
261
    }
262
263
    private function createAggregation(Aggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): AbstractAggregation
264
    {
265
        switch (true) {
266
            case $aggregation instanceof StatsAggregation:
267
                return new Metric\StatsAggregation($aggregation->getName(), $fieldName);
268
269
            case $aggregation instanceof AvgAggregation:
270
                return new Metric\AvgAggregation($aggregation->getName(), $fieldName);
271
272
            case $aggregation instanceof EntityAggregation:
273
                return $this->parseEntityAggregation($aggregation, $fieldName);
274
275
            case $aggregation instanceof MaxAggregation:
276
                return new Metric\MaxAggregation($aggregation->getName(), $fieldName);
277
278
            case $aggregation instanceof MinAggregation:
279
                return new Metric\MinAggregation($aggregation->getName(), $fieldName);
280
281
            case $aggregation instanceof SumAggregation:
282
                return new Metric\SumAggregation($aggregation->getName(), $fieldName);
283
284
            case $aggregation instanceof CountAggregation:
285
                return new ValueCountAggregation($aggregation->getName(), $fieldName);
286
287
            case $aggregation instanceof FilterAggregation:
288
                return $this->parseFilterAggregation($aggregation, $definition, $context);
289
290
            case $aggregation instanceof TermsAggregation:
291
                return $this->parseTermsAggregation($aggregation, $fieldName, $definition, $context);
292
293
            case $aggregation instanceof DateHistogramAggregation:
294
                return $this->parseDateHistogramAggregation($aggregation, $fieldName, $definition, $context);
295
            default:
296
                throw new \RuntimeException(sprintf('Provided aggregation of class %s not supported', \get_class($aggregation)));
297
        }
298
    }
299
300
    private function parseEqualsFilter(EqualsFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
301
    {
302
        $fieldName = $this->buildAccessor($definition, $filter->getField(), $context);
303
304
        if ($filter->getValue() === null) {
305
            $query = new BoolQuery();
306
            $query->add(new ExistsQuery($fieldName), BoolQuery::MUST_NOT);
307
        } else {
308
            $query = new TermQuery($fieldName, $filter->getValue());
309
        }
310
311
        return $this->createNestedQuery($query, $definition, $filter->getField());
312
    }
313
314
    private function parseEqualsAnyFilter(EqualsAnyFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
315
    {
316
        $fieldName = $this->buildAccessor($definition, $filter->getField(), $context);
317
318
        return $this->createNestedQuery(
319
            new TermsQuery($fieldName, array_values($filter->getValue())),
320
            $definition,
321
            $filter->getField()
322
        );
323
    }
324
325
    private function parseContainsFilter(ContainsFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
326
    {
327
        $accessor = $this->buildAccessor($definition, $filter->getField(), $context);
328
329
        /** @var string $value */
330
        $value = $filter->getValue();
331
332
        return $this->createNestedQuery(
333
            new WildcardQuery($accessor, '*' . $value . '*'),
334
            $definition,
335
            $filter->getField()
336
        );
337
    }
338
339
    private function parseRangeFilter(RangeFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface
340
    {
341
        $accessor = $this->buildAccessor($definition, $filter->getField(), $context);
342
343
        return $this->createNestedQuery(
344
            new RangeQuery($accessor, $filter->getParameters()),
345
            $definition,
346
            $filter->getField()
347
        );
348
    }
349
350
    private function parseNotFilter(NotFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
351
    {
352
        $bool = new BoolQuery();
353
        foreach ($filter->getQueries() as $nested) {
354
            $bool->add(
355
                $this->parseFilter($nested, $definition, $root, $context),
356
                BoolQuery::MUST_NOT
357
            );
358
        }
359
360
        return $bool;
361
    }
362
363
    private function parseMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
364
    {
365
        switch ($filter->getOperator()) {
366
            case MultiFilter::CONNECTION_OR:
367
                return $this->parseOrMultiFilter($filter, $definition, $root, $context);
368
            case MultiFilter::CONNECTION_AND:
369
                return $this->parseAndMultiFilter($filter, $definition, $root, $context);
370
            case MultiFilter::CONNECTION_XOR:
371
                return $this->parseXorMultiFilter($filter, $definition, $root, $context);
372
        }
373
374
        throw new \InvalidArgumentException('Operator ' . $filter->getOperator() . ' not allowed');
375
    }
376
377
    private function parseAndMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
378
    {
379
        $bool = new BoolQuery();
380
381
        foreach ($filter->getQueries() as $nested) {
382
            $bool->add(
383
                $this->parseFilter($nested, $definition, $root, $context),
384
                BoolQuery::MUST
385
            );
386
        }
387
388
        return $bool;
389
    }
390
391
    private function parseOrMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
392
    {
393
        $bool = new BoolQuery();
394
395
        foreach ($filter->getQueries() as $nested) {
396
            $bool->add(
397
                $this->parseFilter($nested, $definition, $root, $context),
398
                BoolQuery::SHOULD
399
            );
400
        }
401
402
        return $bool;
403
    }
404
405
    private function parseXorMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface
406
    {
407
        $bool = new BoolQuery();
408
409
        foreach ($filter->getQueries() as $nested) {
410
            $xorQuery = new BoolQuery();
411
            foreach ($filter->getQueries() as $mustNot) {
412
                if ($nested === $mustNot) {
413
                    $xorQuery->add($this->parseFilter($nested, $definition, $root, $context), BoolQuery::MUST);
414
415
                    continue;
416
                }
417
418
                $xorQuery->add($this->parseFilter($mustNot, $definition, $root, $context), BoolQuery::MUST_NOT);
419
            }
420
421
            $bool->add(
422
                $xorQuery,
423
                BoolQuery::SHOULD
424
            );
425
        }
426
427
        return $bool;
428
    }
429
430
    private function createNestedQuery(BuilderInterface $query, EntityDefinition $definition, string $field): BuilderInterface
431
    {
432
        $path = $this->getNestedPath($definition, $field);
433
434
        if ($path) {
435
            return new NestedQuery($path, $query);
436
        }
437
438
        return $query;
439
    }
440
441
    private function getNestedPath(EntityDefinition $definition, string $accessor): ?string
442
    {
443
        if (mb_strpos($accessor, $definition->getEntityName() . '.') === false) {
444
            $accessor = $definition->getEntityName() . '.' . $accessor;
445
        }
446
447
        $fields = EntityDefinitionQueryHelper::getFieldsOfAccessor($definition, $accessor);
448
449
        $path = [];
450
        foreach ($fields as $field) {
451
            if (!$field instanceof AssociationField) {
452
                break;
453
            }
454
455
            $path[] = $field->getPropertyName();
456
        }
457
458
        if (empty($path)) {
459
            return null;
460
        }
461
462
        return implode('.', $path);
463
    }
464
}
465