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\Compound\DisMaxQuery; |
15
|
|
|
use OpenSearchDSL\Query\FullText\MultiMatchQuery; |
16
|
|
|
use OpenSearchDSL\Query\Joining\NestedQuery; |
17
|
|
|
use OpenSearchDSL\Query\TermLevel\ExistsQuery; |
18
|
|
|
use OpenSearchDSL\Query\TermLevel\PrefixQuery; |
19
|
|
|
use OpenSearchDSL\Query\TermLevel\RangeQuery; |
20
|
|
|
use OpenSearchDSL\Query\TermLevel\TermQuery; |
21
|
|
|
use OpenSearchDSL\Query\TermLevel\TermsQuery; |
22
|
|
|
use OpenSearchDSL\Query\TermLevel\WildcardQuery; |
23
|
|
|
use OpenSearchDSL\Sort\FieldSort; |
24
|
|
|
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice; |
25
|
|
|
use Shopware\Core\Defaults; |
26
|
|
|
use Shopware\Core\Framework\Adapter\Storage\AbstractKeyValueStorage; |
27
|
|
|
use Shopware\Core\Framework\Context; |
28
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper; |
29
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition; |
30
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField; |
31
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\BoolField; |
32
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\CustomFields; |
33
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField; |
34
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field; |
35
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\FloatField; |
36
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\IntField; |
37
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField; |
38
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField; |
39
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Aggregation; |
40
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\DateHistogramAggregation; |
41
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation; |
42
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation; |
43
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\AvgAggregation; |
44
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation; |
45
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation; |
46
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MaxAggregation; |
47
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MinAggregation; |
48
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\RangeAggregation; |
49
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\StatsAggregation; |
50
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\SumAggregation; |
51
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter; |
52
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter; |
53
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter; |
54
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; |
55
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter; |
56
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter; |
57
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter; |
58
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter; |
59
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\PrefixFilter; |
60
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter; |
61
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\SingleFieldFilter; |
62
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\SuffixFilter; |
63
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\XOrFilter; |
64
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\CountSorting; |
65
|
|
|
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting; |
66
|
|
|
use Shopware\Core\Framework\Log\Package; |
67
|
|
|
use Shopware\Core\Framework\Uuid\Uuid; |
68
|
|
|
use Shopware\Core\System\CustomField\CustomFieldService; |
69
|
|
|
use Shopware\Elasticsearch\Framework\ElasticsearchDateHistogramAggregation; |
70
|
|
|
use Shopware\Elasticsearch\Framework\ElasticsearchHelper; |
71
|
|
|
use Shopware\Elasticsearch\Framework\Indexing\ElasticsearchIndexer; |
72
|
|
|
use Shopware\Elasticsearch\Sort\CountSort; |
73
|
|
|
|
74
|
|
|
#[Package('core')] |
75
|
|
|
class CriteriaParser |
76
|
|
|
{ |
77
|
|
|
/** |
78
|
|
|
* @internal |
79
|
|
|
*/ |
80
|
|
|
public function __construct( |
81
|
|
|
private readonly EntityDefinitionQueryHelper $helper, |
82
|
|
|
private readonly CustomFieldService $customFieldService, |
83
|
|
|
private readonly AbstractKeyValueStorage $keyValueStorage |
84
|
|
|
) { |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
public function buildAccessor(EntityDefinition $definition, string $fieldName, Context $context): string |
88
|
|
|
{ |
89
|
|
|
$root = $definition->getEntityName(); |
90
|
|
|
|
91
|
|
|
$parts = explode('.', $fieldName); |
92
|
|
|
if ($root === $parts[0]) { |
93
|
|
|
array_shift($parts); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
$field = $this->helper->getField($fieldName, $definition, $root, false); |
97
|
|
|
if ($field instanceof TranslatedField) { |
98
|
|
|
$ordered = []; |
99
|
|
|
foreach ($parts as $part) { |
100
|
|
|
$ordered[] = $part; |
101
|
|
|
} |
102
|
|
|
$parts = $ordered; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
if (!$field instanceof PriceField) { |
106
|
|
|
return implode('.', $parts); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
if (\in_array(end($parts), ['net', 'gross'], true)) { |
110
|
|
|
$taxState = end($parts); |
111
|
|
|
array_pop($parts); |
112
|
|
|
} elseif ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) { |
113
|
|
|
$taxState = 'gross'; |
114
|
|
|
} else { |
115
|
|
|
$taxState = 'net'; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$currencyId = $context->getCurrencyId(); |
119
|
|
|
if (Uuid::isValid((string) end($parts))) { |
120
|
|
|
$currencyId = end($parts); |
121
|
|
|
array_pop($parts); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$parts[] = 'c_' . $currencyId; |
125
|
|
|
$parts[] = $taxState; |
126
|
|
|
|
127
|
|
|
return implode('.', $parts); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
public function parseSorting(FieldSorting $sorting, EntityDefinition $definition, Context $context): FieldSort |
131
|
|
|
{ |
132
|
|
|
if ($this->isCheapestPriceField($sorting->getField())) { |
133
|
|
|
return new FieldSort('_script', $sorting->getDirection(), null, [ |
134
|
|
|
'type' => 'number', |
135
|
|
|
'script' => [ |
136
|
|
|
'id' => 'cheapest_price', |
137
|
|
|
'params' => $this->getCheapestPriceParameters($context), |
138
|
|
|
], |
139
|
|
|
]); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
if ($this->isCheapestPriceField($sorting->getField(), true)) { |
143
|
|
|
return new FieldSort('_script', $sorting->getDirection(), null, [ |
144
|
|
|
'type' => 'number', |
145
|
|
|
'script' => [ |
146
|
|
|
'id' => 'cheapest_price_percentage', |
147
|
|
|
'params' => ['accessors' => $this->getCheapestPriceAccessors($context, true)], |
148
|
|
|
], |
149
|
|
|
]); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
153
|
|
|
$field = $this->helper->getField($sorting->getField(), $definition, $definition->getEntityName(), false); |
154
|
|
|
|
155
|
|
|
if ($field instanceof TranslatedField) { |
156
|
|
|
return $this->createTranslatedSorting($definition->getEntityName(), $sorting, $context); |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
$accessor = $this->buildAccessor($definition, $sorting->getField(), $context); |
161
|
|
|
|
162
|
|
|
if ($sorting instanceof CountSorting) { |
163
|
|
|
return new CountSort($accessor, $sorting->getDirection()); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
return new FieldSort($accessor, $sorting->getDirection()); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
public function parseAggregation(Aggregation $aggregation, EntityDefinition $definition, Context $context): ?AbstractAggregation |
170
|
|
|
{ |
171
|
|
|
$fieldName = $this->buildAccessor($definition, $aggregation->getField(), $context); |
172
|
|
|
|
173
|
|
|
$fields = $aggregation->getFields(); |
174
|
|
|
|
175
|
|
|
$path = null; |
176
|
|
|
if (\count($fields) > 0) { |
177
|
|
|
$path = $this->getNestedPath($definition, $fields[0]); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
$esAggregation = $this->createAggregation($aggregation, $fieldName, $definition, $context); |
181
|
|
|
|
182
|
|
|
if (!$path || $aggregation instanceof FilterAggregation) { |
183
|
|
|
return $esAggregation; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$nested = new NestedAggregation($aggregation->getName(), $path); |
187
|
|
|
$nested->addAggregation($esAggregation); |
188
|
|
|
|
189
|
|
|
return $nested; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
public function parseFilter(Filter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface |
193
|
|
|
{ |
194
|
|
|
return match (true) { |
195
|
|
|
$filter instanceof NotFilter => $this->parseNotFilter($filter, $definition, $root, $context), |
196
|
|
|
$filter instanceof MultiFilter => $this->parseMultiFilter($filter, $definition, $root, $context), |
197
|
|
|
$filter instanceof EqualsFilter => $this->parseEqualsFilter($filter, $definition, $context), |
198
|
|
|
$filter instanceof EqualsAnyFilter => $this->parseEqualsAnyFilter($filter, $definition, $context), |
199
|
|
|
$filter instanceof ContainsFilter => $this->parseContainsFilter($filter, $definition, $context), |
200
|
|
|
$filter instanceof PrefixFilter => $this->parsePrefixFilter($filter, $definition, $context), |
201
|
|
|
$filter instanceof SuffixFilter => $this->parseSuffixFilter($filter, $definition, $context), |
202
|
|
|
$filter instanceof RangeFilter => $this->parseRangeFilter($filter, $definition, $context), |
203
|
|
|
default => throw new \RuntimeException(sprintf('Unsupported filter %s', $filter::class)), |
204
|
|
|
}; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
protected function parseFilterAggregation(FilterAggregation $aggregation, EntityDefinition $definition, Context $context): AbstractAggregation |
208
|
|
|
{ |
209
|
|
|
if ($aggregation->getAggregation() === null) { |
210
|
|
|
throw new \RuntimeException(sprintf('Filter aggregation %s contains no nested aggregation.', $aggregation->getName())); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$nested = $this->parseAggregation($aggregation->getAggregation(), $definition, $context); |
214
|
|
|
if ($nested === null) { |
215
|
|
|
throw new \RuntimeException(sprintf('Nested filter aggregation %s can not be parsed.', $aggregation->getName())); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// when aggregation inside the filter aggregation points to a nested object (e.g. product.properties.id) we have to add all filters |
219
|
|
|
// which points to the same association to the same "nesting" level like the nested aggregation for this association |
220
|
|
|
$path = $nested instanceof NestedAggregation ? $nested->getPath() : null; |
|
|
|
|
221
|
|
|
$bool = new BoolQuery(); |
222
|
|
|
|
223
|
|
|
$filters = []; |
224
|
|
|
foreach ($aggregation->getFilter() as $filter) { |
225
|
|
|
$query = $this->parseFilter($filter, $definition, $definition->getEntityName(), $context); |
226
|
|
|
|
227
|
|
|
if (!$query instanceof NestedQuery) { |
228
|
|
|
$filters[] = new Bucketing\FilterAggregation($aggregation->getName(), $query); |
229
|
|
|
|
230
|
|
|
continue; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
// same property path as the "real" aggregation |
234
|
|
|
if ($query->getPath() === $path) { |
235
|
|
|
$bool->add($query->getQuery()); |
236
|
|
|
|
237
|
|
|
continue; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
// query points to a nested document property - we have to define that the filter points to this field |
241
|
|
|
$parsed = new NestedAggregation($aggregation->getName(), $query->getPath()); |
242
|
|
|
|
243
|
|
|
// now we can defined a filter which points to the nested field (remove NestedQuery layer) |
244
|
|
|
$filter = new Bucketing\FilterAggregation($aggregation->getName(), $query->getQuery()); |
245
|
|
|
|
246
|
|
|
// afterwards we reset the nesting to allow following filters to point to another nested property |
247
|
|
|
$reverse = new ReverseNestedAggregation($aggregation->getName()); |
248
|
|
|
|
249
|
|
|
$filter->addAggregation($reverse); |
250
|
|
|
|
251
|
|
|
$parsed->addAggregation($filter); |
252
|
|
|
|
253
|
|
|
$filters[] = $parsed; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
// nested aggregation should have filters - we have to remap the nesting |
257
|
|
|
$mapped = $nested; |
258
|
|
|
if (\count($bool->getQueries()) > 0 && $nested instanceof NestedAggregation) { |
259
|
|
|
$real = $nested->getAggregation($nested->getName()); |
260
|
|
|
if (!$real instanceof AbstractAggregation) { |
261
|
|
|
throw new \RuntimeException(sprintf('Nested filter aggregation %s can not be parsed.', $aggregation->getName())); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
$filter = new Bucketing\FilterAggregation($aggregation->getName(), $bool); |
265
|
|
|
$filter->addAggregation($real); |
266
|
|
|
|
267
|
|
|
$mapped = new NestedAggregation($aggregation->getName(), $nested->getPath()); |
268
|
|
|
$mapped->addAggregation($filter); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
// at this point we have to walk over all filters and create one nested filter for it |
272
|
|
|
$parent = null; |
273
|
|
|
$root = $mapped; |
274
|
|
|
foreach ($filters as $filter) { |
275
|
|
|
if ($parent === null) { |
276
|
|
|
$parent = $filter; |
277
|
|
|
$root = $filter; |
278
|
|
|
|
279
|
|
|
continue; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
$parent->addAggregation($filter); |
283
|
|
|
|
284
|
|
|
if (!$filter instanceof NestedAggregation) { |
285
|
|
|
$parent = $filter; |
286
|
|
|
|
287
|
|
|
continue; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
$filter = $filter->getAggregation($filter->getName()); |
291
|
|
|
if (!$filter instanceof AbstractAggregation) { |
292
|
|
|
throw new \RuntimeException('Expected nested+filter+reverse pattern for parsed filters to set next parent correctly'); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
$parent = $filter->getAggregation($filter->getName()); |
296
|
|
|
if (!$parent instanceof AbstractAggregation) { |
297
|
|
|
throw new \RuntimeException('Expected nested+filter+reverse pattern for parsed filters to set next parent correctly'); |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
// it can happen, that $parent is not defined if the "real" aggregation is a nested and all filters points to the same property |
302
|
|
|
// than we return the following structure: [nested-agg] + filter-agg + real-agg ( [] = optional ) |
303
|
|
|
if ($parent === null) { |
304
|
|
|
return $root; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
// at this point we have some other filters which point to another nested object as the "real" aggregation |
308
|
|
|
// than we return the following structure: [nested-agg] + filter-agg + [reverse-nested-agg] + [nested-agg] + real-agg ( [] = optional ) |
309
|
|
|
$parent->addAggregation($mapped); |
310
|
|
|
|
311
|
|
|
return $root; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
protected function parseTermsAggregation(TermsAggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): AbstractAggregation |
315
|
|
|
{ |
316
|
|
|
if ($aggregation->getSorting() === null) { |
317
|
|
|
$terms = new Bucketing\TermsAggregation($aggregation->getName(), $fieldName); |
318
|
|
|
|
319
|
|
|
if ($nested = $aggregation->getAggregation()) { |
320
|
|
|
$terms->addAggregation( |
321
|
|
|
$this->parseNestedAggregation($nested, $definition, $context) |
322
|
|
|
); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
// set default size to 10.000 => max for default configuration |
326
|
|
|
$terms->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE); |
327
|
|
|
|
328
|
|
|
if ($aggregation->getLimit()) { |
|
|
|
|
329
|
|
|
$terms->addParameter('size', (string) $aggregation->getLimit()); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
return $terms; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$composite = new CompositeAggregation($aggregation->getName()); |
336
|
|
|
|
337
|
|
|
$accessor = $this->buildAccessor($definition, $aggregation->getSorting()->getField(), $context); |
338
|
|
|
|
339
|
|
|
$sorting = new Bucketing\TermsAggregation($aggregation->getName() . '.sorting', $accessor); |
340
|
|
|
$sorting->addParameter('order', $aggregation->getSorting()->getDirection()); |
341
|
|
|
$composite->addSource($sorting); |
342
|
|
|
|
343
|
|
|
$terms = new Bucketing\TermsAggregation($aggregation->getName() . '.key', $fieldName); |
344
|
|
|
$composite->addSource($terms); |
345
|
|
|
|
346
|
|
|
if ($nested = $aggregation->getAggregation()) { |
347
|
|
|
$composite->addAggregation( |
348
|
|
|
$this->parseNestedAggregation($nested, $definition, $context) |
349
|
|
|
); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
// set default size to 10.000 => max for default configuration |
353
|
|
|
$composite->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE); |
354
|
|
|
|
355
|
|
|
if ($aggregation->getLimit()) { |
|
|
|
|
356
|
|
|
$composite->addParameter('size', (string) $aggregation->getLimit()); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
return $composite; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
protected function parseStatsAggregation(StatsAggregation $aggregation, string $fieldName, Context $context): Metric\StatsAggregation |
363
|
|
|
{ |
364
|
|
|
if ($this->isCheapestPriceField($aggregation->getField())) { |
365
|
|
|
return new Metric\StatsAggregation($aggregation->getName(), null, [ |
366
|
|
|
'id' => 'cheapest_price', |
367
|
|
|
'params' => $this->getCheapestPriceParameters($context), |
368
|
|
|
]); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
if ($this->isCheapestPriceField($aggregation->getField(), true)) { |
372
|
|
|
return new Metric\StatsAggregation($aggregation->getName(), null, [ |
373
|
|
|
'id' => 'cheapest_price_percentage', |
374
|
|
|
'params' => ['accessors' => $this->getCheapestPriceAccessors($context, true)], |
375
|
|
|
]); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
return new Metric\StatsAggregation($aggregation->getName(), $fieldName); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
protected function parseEntityAggregation(EntityAggregation $aggregation, string $fieldName): Bucketing\TermsAggregation |
382
|
|
|
{ |
383
|
|
|
$bucketingAggregation = new Bucketing\TermsAggregation($aggregation->getName(), $fieldName); |
384
|
|
|
|
385
|
|
|
$bucketingAggregation->addParameter('size', ElasticsearchHelper::MAX_SIZE_VALUE); |
386
|
|
|
|
387
|
|
|
return $bucketingAggregation; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
protected function parseDateHistogramAggregation(DateHistogramAggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): CompositeAggregation |
391
|
|
|
{ |
392
|
|
|
$composite = new CompositeAggregation($aggregation->getName()); |
393
|
|
|
|
394
|
|
|
if ($fieldSorting = $aggregation->getSorting()) { |
395
|
|
|
$accessor = $this->buildAccessor($definition, $fieldSorting->getField(), $context); |
396
|
|
|
|
397
|
|
|
$sorting = new Bucketing\TermsAggregation($aggregation->getName() . '.sorting', $accessor); |
398
|
|
|
$sorting->addParameter('order', $fieldSorting->getDirection()); |
399
|
|
|
|
400
|
|
|
$composite->addSource($sorting); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
$histogram = new ElasticsearchDateHistogramAggregation( |
404
|
|
|
$aggregation->getName() . '.key', |
405
|
|
|
$fieldName, |
406
|
|
|
$aggregation->getInterval(), |
407
|
|
|
'yyyy-MM-dd HH:mm:ss' |
408
|
|
|
); |
409
|
|
|
|
410
|
|
|
if ($aggregation->getTimeZone()) { |
411
|
|
|
$histogram->addParameter('time_zone', $aggregation->getTimeZone()); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
$composite->addSource($histogram); |
415
|
|
|
|
416
|
|
|
if ($nested = $aggregation->getAggregation()) { |
417
|
|
|
$composite->addAggregation( |
418
|
|
|
$this->parseNestedAggregation($nested, $definition, $context) |
419
|
|
|
); |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
return $composite; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
protected function parseRangeAggregation(RangeAggregation $aggregation, string $fieldName): Bucketing\RangeAggregation |
426
|
|
|
{ |
427
|
|
|
return new Bucketing\RangeAggregation( |
428
|
|
|
$aggregation->getName(), |
429
|
|
|
$fieldName, |
430
|
|
|
$aggregation->getRanges() |
431
|
|
|
); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* @return array<string, mixed> |
436
|
|
|
*/ |
437
|
|
|
private function getCheapestPriceParameters(Context $context): array |
438
|
|
|
{ |
439
|
|
|
return [ |
440
|
|
|
'accessors' => $this->getCheapestPriceAccessors($context), |
441
|
|
|
'decimals' => 10 ** $context->getRounding()->getDecimals(), |
442
|
|
|
'round' => $this->useCashRounding($context), |
443
|
|
|
'multiplier' => 100 / ($context->getRounding()->getInterval() * 100), |
444
|
|
|
]; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
private function useCashRounding(Context $context): bool |
448
|
|
|
{ |
449
|
|
|
if ($context->getRounding()->getDecimals() !== 2) { |
450
|
|
|
return false; |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
if ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) { |
454
|
|
|
return true; |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
return $context->getRounding()->roundForNet(); |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
/** |
461
|
|
|
* @return array<int, array<string, string|float>> |
462
|
|
|
*/ |
463
|
|
|
private function getCheapestPriceAccessors(Context $context, bool $percentage = false): array |
464
|
|
|
{ |
465
|
|
|
$accessors = []; |
466
|
|
|
|
467
|
|
|
$tax = $context->getTaxState() === CartPrice::TAX_STATE_GROSS ? 'gross' : 'net'; |
468
|
|
|
|
469
|
|
|
$ruleIds = array_merge($context->getRuleIds(), ['default']); |
470
|
|
|
|
471
|
|
|
foreach ($ruleIds as $ruleId) { |
472
|
|
|
$key = implode('_', [ |
473
|
|
|
'cheapest_price', |
474
|
|
|
'rule' . $ruleId, |
475
|
|
|
'currency' . $context->getCurrencyId(), |
476
|
|
|
$tax, |
477
|
|
|
]); |
478
|
|
|
|
479
|
|
|
if ($percentage) { |
480
|
|
|
$key .= '_percentage'; |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
$accessors[] = ['key' => $key, 'factor' => 1]; |
484
|
|
|
|
485
|
|
|
if ($context->getCurrencyId() === Defaults::CURRENCY) { |
486
|
|
|
continue; |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
$key = implode('_', [ |
490
|
|
|
'cheapest_price', |
491
|
|
|
'rule' . $ruleId, |
492
|
|
|
'currency' . Defaults::CURRENCY, |
493
|
|
|
$tax, |
494
|
|
|
]); |
495
|
|
|
|
496
|
|
|
if ($percentage) { |
497
|
|
|
$key .= '_percentage'; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
$accessors[] = ['key' => $key, 'factor' => $context->getCurrencyFactor()]; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
return $accessors; |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
private function parseNestedAggregation(Aggregation $aggregation, EntityDefinition $definition, Context $context): AbstractAggregation |
507
|
|
|
{ |
508
|
|
|
$fieldName = $this->buildAccessor($definition, $aggregation->getField(), $context); |
509
|
|
|
|
510
|
|
|
return $this->createAggregation($aggregation, $fieldName, $definition, $context); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
private function createAggregation(Aggregation $aggregation, string $fieldName, EntityDefinition $definition, Context $context): AbstractAggregation |
514
|
|
|
{ |
515
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
516
|
|
|
$field = $this->getField($definition, $fieldName); |
517
|
|
|
|
518
|
|
|
if ($field instanceof TranslatedField) { |
519
|
|
|
$fieldName = $this->getTranslatedFieldName($fieldName, $context->getLanguageId()); |
520
|
|
|
} |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
return match (true) { |
524
|
|
|
$aggregation instanceof StatsAggregation => $this->parseStatsAggregation($aggregation, $fieldName, $context), |
525
|
|
|
$aggregation instanceof AvgAggregation => new Metric\AvgAggregation($aggregation->getName(), $fieldName), |
526
|
|
|
$aggregation instanceof EntityAggregation => $this->parseEntityAggregation($aggregation, $fieldName), |
527
|
|
|
$aggregation instanceof MaxAggregation => new Metric\MaxAggregation($aggregation->getName(), $fieldName), |
528
|
|
|
$aggregation instanceof MinAggregation => new Metric\MinAggregation($aggregation->getName(), $fieldName), |
529
|
|
|
$aggregation instanceof SumAggregation => new Metric\SumAggregation($aggregation->getName(), $fieldName), |
530
|
|
|
$aggregation instanceof CountAggregation => new ValueCountAggregation($aggregation->getName(), $fieldName), |
531
|
|
|
$aggregation instanceof FilterAggregation => $this->parseFilterAggregation($aggregation, $definition, $context), |
532
|
|
|
$aggregation instanceof TermsAggregation => $this->parseTermsAggregation($aggregation, $fieldName, $definition, $context), |
|
|
|
|
533
|
|
|
$aggregation instanceof DateHistogramAggregation => $this->parseDateHistogramAggregation($aggregation, $fieldName, $definition, $context), |
|
|
|
|
534
|
|
|
$aggregation instanceof RangeAggregation => $this->parseRangeAggregation($aggregation, $fieldName), |
|
|
|
|
535
|
|
|
default => throw new \RuntimeException(sprintf('Provided aggregation of class %s not supported', $aggregation::class)), |
536
|
|
|
}; |
537
|
|
|
} |
538
|
|
|
|
539
|
|
|
private function parseEqualsFilter(EqualsFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface |
540
|
|
|
{ |
541
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
542
|
|
|
$fieldName = $this->buildAccessor($definition, $filter->getField(), $context); |
543
|
|
|
|
544
|
|
|
$field = $this->getField($definition, $fieldName); |
545
|
|
|
|
546
|
|
|
if ($filter->getValue() === null) { |
547
|
|
|
$query = new BoolQuery(); |
548
|
|
|
|
549
|
|
|
if ($field instanceof TranslatedField) { |
550
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
551
|
|
|
$query->add(new ExistsQuery(sprintf('%s.%s', $fieldName, $languageId)), BoolQuery::MUST_NOT); |
552
|
|
|
} |
553
|
|
|
} else { |
554
|
|
|
$query->add(new ExistsQuery($fieldName), BoolQuery::MUST_NOT); |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
return $this->createNestedQuery($query, $definition, $filter->getField()); |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
$value = $this->parseValue($definition, $filter, $filter->getValue()); |
561
|
|
|
$query = new TermQuery($fieldName, $value); |
562
|
|
|
|
563
|
|
|
if ($field instanceof TranslatedField) { |
564
|
|
|
$multiMatchFields = []; |
565
|
|
|
|
566
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
567
|
|
|
$multiMatchFields[] = $this->getTranslatedFieldName($fieldName, $languageId); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
$query = new MultiMatchQuery($multiMatchFields, $value, [ |
571
|
|
|
'type' => 'best_fields', |
572
|
|
|
]); |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
return $this->createNestedQuery($query, $definition, $filter->getField()); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$fieldName = $this->buildAccessor($definition, $filter->getField(), $context); |
579
|
|
|
|
580
|
|
|
if ($filter->getValue() === null) { |
581
|
|
|
$query = new BoolQuery(); |
582
|
|
|
$query->add(new ExistsQuery($fieldName), BoolQuery::MUST_NOT); |
583
|
|
|
|
584
|
|
|
return $this->createNestedQuery($query, $definition, $filter->getField()); |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
$value = $this->parseValue($definition, $filter, $filter->getValue()); |
588
|
|
|
|
589
|
|
|
$query = new TermQuery($fieldName, $value); |
590
|
|
|
|
591
|
|
|
return $this->createNestedQuery($query, $definition, $filter->getField()); |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
private function parseEqualsAnyFilter(EqualsAnyFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface |
595
|
|
|
{ |
596
|
|
|
$fieldName = $this->buildAccessor($definition, $filter->getField(), $context); |
597
|
|
|
|
598
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
599
|
|
|
$field = $this->getField($definition, $fieldName); |
600
|
|
|
|
601
|
|
|
$value = $this->parseValue($definition, $filter, \array_values($filter->getValue())); |
602
|
|
|
|
603
|
|
|
$query = new TermsQuery($fieldName, $value); |
604
|
|
|
|
605
|
|
|
if ($field instanceof TranslatedField) { |
606
|
|
|
$query = new DisMaxQuery(); |
607
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
608
|
|
|
$accessor = $this->getTranslatedFieldName($fieldName, $languageId); |
609
|
|
|
$query->addQuery(new TermsQuery($accessor, $value)); |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
return $this->createNestedQuery( |
614
|
|
|
$query, |
615
|
|
|
$definition, |
616
|
|
|
$filter->getField() |
617
|
|
|
); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
$value = $this->parseValue($definition, $filter, \array_values($filter->getValue())); |
621
|
|
|
|
622
|
|
|
return $this->createNestedQuery( |
623
|
|
|
new TermsQuery($fieldName, $value), |
624
|
|
|
$definition, |
625
|
|
|
$filter->getField() |
626
|
|
|
); |
627
|
|
|
} |
628
|
|
|
|
629
|
|
|
private function parseContainsFilter(ContainsFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface |
630
|
|
|
{ |
631
|
|
|
$accessor = $this->buildAccessor($definition, $filter->getField(), $context); |
632
|
|
|
|
633
|
|
|
/** @var string $value */ |
634
|
|
|
$value = $filter->getValue(); |
635
|
|
|
|
636
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
637
|
|
|
$field = $this->getField($definition, $filter->getField()); |
638
|
|
|
|
639
|
|
|
$query = new WildcardQuery($accessor, '*' . $value . '*'); |
640
|
|
|
|
641
|
|
|
if ($field instanceof TranslatedField) { |
642
|
|
|
$query = new DisMaxQuery(); |
643
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
644
|
|
|
$fieldName = $this->getTranslatedFieldName($accessor, $languageId); |
645
|
|
|
$query->addQuery(new WildcardQuery($fieldName, '*' . $value . '*')); |
646
|
|
|
} |
647
|
|
|
} |
648
|
|
|
|
649
|
|
|
return $this->createNestedQuery( |
650
|
|
|
$query, |
651
|
|
|
$definition, |
652
|
|
|
$filter->getField() |
653
|
|
|
); |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
return $this->createNestedQuery( |
657
|
|
|
new WildcardQuery($accessor, '*' . $value . '*'), |
658
|
|
|
$definition, |
659
|
|
|
$filter->getField() |
660
|
|
|
); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
private function parsePrefixFilter(PrefixFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface |
664
|
|
|
{ |
665
|
|
|
$accessor = $this->buildAccessor($definition, $filter->getField(), $context); |
666
|
|
|
|
667
|
|
|
$value = $filter->getValue(); |
668
|
|
|
|
669
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
670
|
|
|
$field = $this->getField($definition, $filter->getField()); |
671
|
|
|
|
672
|
|
|
$query = new PrefixQuery($accessor, $value); |
673
|
|
|
|
674
|
|
|
if ($field instanceof TranslatedField) { |
675
|
|
|
$multiMatchFields = []; |
676
|
|
|
|
677
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
678
|
|
|
$multiMatchFields[] = $this->getTranslatedFieldName($accessor, $languageId) . '.search'; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
$query = new MultiMatchQuery($multiMatchFields, $value, [ |
682
|
|
|
'type' => 'phrase_prefix', |
683
|
|
|
'slop' => 5, |
684
|
|
|
]); |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
return $this->createNestedQuery( |
688
|
|
|
$query, |
689
|
|
|
$definition, |
690
|
|
|
$filter->getField() |
691
|
|
|
); |
692
|
|
|
} |
693
|
|
|
|
694
|
|
|
return $this->createNestedQuery( |
695
|
|
|
new PrefixQuery($accessor, $value), |
696
|
|
|
$definition, |
697
|
|
|
$filter->getField() |
698
|
|
|
); |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
private function parseSuffixFilter(SuffixFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface |
702
|
|
|
{ |
703
|
|
|
$accessor = $this->buildAccessor($definition, $filter->getField(), $context); |
704
|
|
|
|
705
|
|
|
$value = $filter->getValue(); |
706
|
|
|
|
707
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
708
|
|
|
$field = $this->getField($definition, $filter->getField()); |
709
|
|
|
|
710
|
|
|
$query = new WildcardQuery($accessor, '*' . $value); |
711
|
|
|
|
712
|
|
|
if ($field instanceof TranslatedField) { |
713
|
|
|
$query = new DisMaxQuery(); |
714
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
715
|
|
|
$fieldName = $this->getTranslatedFieldName($accessor, $languageId); |
716
|
|
|
$query->addQuery(new WildcardQuery($fieldName, '*' . $value)); |
717
|
|
|
} |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
return $this->createNestedQuery( |
721
|
|
|
$query, |
722
|
|
|
$definition, |
723
|
|
|
$filter->getField() |
724
|
|
|
); |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
return $this->createNestedQuery( |
728
|
|
|
new WildcardQuery($accessor, '*' . $value), |
729
|
|
|
$definition, |
730
|
|
|
$filter->getField() |
731
|
|
|
); |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
private function parseRangeFilter(RangeFilter $filter, EntityDefinition $definition, Context $context): BuilderInterface |
735
|
|
|
{ |
736
|
|
|
if ($this->isCheapestPriceField($filter->getField())) { |
737
|
|
|
return new ScriptIdQuery('cheapest_price_filter', [ |
738
|
|
|
'params' => array_merge( |
739
|
|
|
$this->getRangeParameters($filter), |
740
|
|
|
$this->getCheapestPriceParameters($context) |
741
|
|
|
), |
742
|
|
|
]); |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
if ($this->isCheapestPriceField($filter->getField(), true)) { |
746
|
|
|
return new ScriptIdQuery('cheapest_price_percentage_filter', [ |
747
|
|
|
'params' => array_merge( |
748
|
|
|
$this->getRangeParameters($filter), |
749
|
|
|
['accessors' => $this->getCheapestPriceAccessors($context, true)] |
750
|
|
|
), |
751
|
|
|
]); |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
$accessor = $this->buildAccessor($definition, $filter->getField(), $context); |
755
|
|
|
|
756
|
|
|
if ($this->keyValueStorage->get(ElasticsearchIndexer::ENABLE_MULTILINGUAL_INDEX_KEY, false)) { |
|
|
|
|
757
|
|
|
$field = $this->getField($definition, $filter->getField()); |
758
|
|
|
|
759
|
|
|
$value = $this->parseValue($definition, $filter, $filter->getParameters()); |
760
|
|
|
$query = new RangeQuery($accessor, $value); |
761
|
|
|
|
762
|
|
|
if ($field instanceof TranslatedField) { |
763
|
|
|
$query = new DisMaxQuery(); |
764
|
|
|
foreach ($context->getLanguageIdChain() as $languageId) { |
765
|
|
|
$fieldName = $this->getTranslatedFieldName($accessor, $languageId); |
766
|
|
|
$query->addQuery(new RangeQuery($fieldName, $value)); |
767
|
|
|
} |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
return $this->createNestedQuery( |
771
|
|
|
$query, |
772
|
|
|
$definition, |
773
|
|
|
$filter->getField() |
774
|
|
|
); |
775
|
|
|
} |
776
|
|
|
|
777
|
|
|
return $this->createNestedQuery( |
778
|
|
|
new RangeQuery($accessor, $this->parseValue($definition, $filter, $filter->getParameters())), |
779
|
|
|
$definition, |
780
|
|
|
$filter->getField() |
781
|
|
|
); |
782
|
|
|
} |
783
|
|
|
|
784
|
|
|
private function isCheapestPriceField(string $field, bool $percentage = false): bool |
785
|
|
|
{ |
786
|
|
|
if ($percentage) { |
787
|
|
|
$haystack = ['product.cheapestPrice.percentage', 'cheapestPrice.percentage']; |
788
|
|
|
} else { |
789
|
|
|
$haystack = ['product.cheapestPrice', 'cheapestPrice']; |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
return \in_array($field, $haystack, true); |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
/** |
796
|
|
|
* @return array<string, float> |
797
|
|
|
*/ |
798
|
|
|
private function getRangeParameters(RangeFilter $filter): array |
799
|
|
|
{ |
800
|
|
|
$params = []; |
801
|
|
|
foreach ($filter->getParameters() as $key => $value) { |
802
|
|
|
$params[$key] = (float) $value; |
803
|
|
|
} |
804
|
|
|
|
805
|
|
|
return $params; |
806
|
|
|
} |
807
|
|
|
|
808
|
|
|
private function parseNotFilter(NotFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface |
809
|
|
|
{ |
810
|
|
|
$bool = new BoolQuery(); |
811
|
|
|
if (\count($filter->getQueries()) === 0) { |
812
|
|
|
return $bool; |
813
|
|
|
} |
814
|
|
|
|
815
|
|
|
if (\count($filter->getQueries()) === 1) { |
816
|
|
|
$bool->add( |
817
|
|
|
$this->parseFilter($filter->getQueries()[0], $definition, $root, $context), |
818
|
|
|
BoolQuery::MUST_NOT |
819
|
|
|
); |
820
|
|
|
|
821
|
|
|
return $bool; |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
$multiFilter = match ($filter->getOperator()) { |
825
|
|
|
MultiFilter::CONNECTION_OR => new OrFilter(), |
826
|
|
|
MultiFilter::CONNECTION_XOR => new XOrFilter(), |
827
|
|
|
default => new AndFilter(), |
828
|
|
|
}; |
829
|
|
|
|
830
|
|
|
foreach ($filter->getQueries() as $query) { |
831
|
|
|
$multiFilter->addQuery($query); |
832
|
|
|
} |
833
|
|
|
|
834
|
|
|
$bool->add( |
835
|
|
|
$this->parseFilter($multiFilter, $definition, $root, $context), |
836
|
|
|
BoolQuery::MUST_NOT |
837
|
|
|
); |
838
|
|
|
|
839
|
|
|
return $bool; |
840
|
|
|
} |
841
|
|
|
|
842
|
|
|
private function parseMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface |
843
|
|
|
{ |
844
|
|
|
return match ($filter->getOperator()) { |
845
|
|
|
MultiFilter::CONNECTION_OR => $this->parseOrMultiFilter($filter, $definition, $root, $context), |
846
|
|
|
MultiFilter::CONNECTION_AND => $this->parseAndMultiFilter($filter, $definition, $root, $context), |
847
|
|
|
MultiFilter::CONNECTION_XOR => $this->parseXorMultiFilter($filter, $definition, $root, $context), |
848
|
|
|
default => throw new \InvalidArgumentException('Operator ' . $filter->getOperator() . ' not allowed'), |
849
|
|
|
}; |
850
|
|
|
} |
851
|
|
|
|
852
|
|
|
private function parseAndMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface |
853
|
|
|
{ |
854
|
|
|
$grouped = []; |
855
|
|
|
$bool = new BoolQuery(); |
856
|
|
|
|
857
|
|
|
foreach ($filter->getQueries() as $nested) { |
858
|
|
|
$query = $this->parseFilter($nested, $definition, $root, $context); |
859
|
|
|
|
860
|
|
|
if (!$query instanceof NestedQuery) { |
861
|
|
|
$bool->add($query, BoolQuery::MUST); |
862
|
|
|
|
863
|
|
|
continue; |
864
|
|
|
} |
865
|
|
|
|
866
|
|
|
if (!\array_key_exists($query->getPath(), $grouped)) { |
867
|
|
|
$grouped[$query->getPath()] = new BoolQuery(); |
868
|
|
|
$bool->add(new NestedQuery($query->getPath(), $grouped[$query->getPath()])); |
869
|
|
|
} |
870
|
|
|
|
871
|
|
|
$grouped[$query->getPath()]->add($query->getQuery()); |
872
|
|
|
} |
873
|
|
|
|
874
|
|
|
return $bool; |
875
|
|
|
} |
876
|
|
|
|
877
|
|
|
private function parseOrMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface |
878
|
|
|
{ |
879
|
|
|
$bool = new BoolQuery(); |
880
|
|
|
|
881
|
|
|
foreach ($filter->getQueries() as $nested) { |
882
|
|
|
$bool->add( |
883
|
|
|
$this->parseFilter($nested, $definition, $root, $context), |
884
|
|
|
BoolQuery::SHOULD |
885
|
|
|
); |
886
|
|
|
} |
887
|
|
|
|
888
|
|
|
return $bool; |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
private function parseXorMultiFilter(MultiFilter $filter, EntityDefinition $definition, string $root, Context $context): BuilderInterface |
892
|
|
|
{ |
893
|
|
|
$bool = new BoolQuery(); |
894
|
|
|
|
895
|
|
|
foreach ($filter->getQueries() as $nested) { |
896
|
|
|
$xorQuery = new BoolQuery(); |
897
|
|
|
foreach ($filter->getQueries() as $mustNot) { |
898
|
|
|
if ($nested === $mustNot) { |
899
|
|
|
$xorQuery->add($this->parseFilter($nested, $definition, $root, $context), BoolQuery::MUST); |
900
|
|
|
|
901
|
|
|
continue; |
902
|
|
|
} |
903
|
|
|
|
904
|
|
|
$xorQuery->add($this->parseFilter($mustNot, $definition, $root, $context), BoolQuery::MUST_NOT); |
905
|
|
|
} |
906
|
|
|
|
907
|
|
|
$bool->add( |
908
|
|
|
$xorQuery, |
909
|
|
|
BoolQuery::SHOULD |
910
|
|
|
); |
911
|
|
|
} |
912
|
|
|
|
913
|
|
|
return $bool; |
914
|
|
|
} |
915
|
|
|
|
916
|
|
|
private function createNestedQuery(BuilderInterface $query, EntityDefinition $definition, string $field): BuilderInterface |
917
|
|
|
{ |
918
|
|
|
$path = $this->getNestedPath($definition, $field); |
919
|
|
|
|
920
|
|
|
if ($path) { |
921
|
|
|
return new NestedQuery($path, $query); |
922
|
|
|
} |
923
|
|
|
|
924
|
|
|
return $query; |
925
|
|
|
} |
926
|
|
|
|
927
|
|
|
private function getField(EntityDefinition $definition, string $fieldName): ?Field |
928
|
|
|
{ |
929
|
|
|
$root = $definition->getEntityName(); |
930
|
|
|
|
931
|
|
|
$parts = explode('.', $fieldName); |
932
|
|
|
if ($root === $parts[0]) { |
933
|
|
|
array_shift($parts); |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
return $this->helper->getField($fieldName, $definition, $root, false); |
937
|
|
|
} |
938
|
|
|
|
939
|
|
|
private function getNestedPath(EntityDefinition $definition, string $accessor): ?string |
940
|
|
|
{ |
941
|
|
|
if (mb_strpos($accessor, $definition->getEntityName() . '.') === false) { |
942
|
|
|
$accessor = $definition->getEntityName() . '.' . $accessor; |
943
|
|
|
} |
944
|
|
|
|
945
|
|
|
$fields = EntityDefinitionQueryHelper::getFieldsOfAccessor($definition, $accessor); |
946
|
|
|
|
947
|
|
|
$path = []; |
948
|
|
|
foreach ($fields as $field) { |
949
|
|
|
if (!$field instanceof AssociationField) { |
950
|
|
|
break; |
951
|
|
|
} |
952
|
|
|
|
953
|
|
|
$path[] = $field->getPropertyName(); |
954
|
|
|
} |
955
|
|
|
|
956
|
|
|
if (empty($path)) { |
957
|
|
|
return null; |
958
|
|
|
} |
959
|
|
|
|
960
|
|
|
return implode('.', $path); |
961
|
|
|
} |
962
|
|
|
|
963
|
|
|
private function parseValue(EntityDefinition $definition, SingleFieldFilter $filter, mixed $value): mixed |
964
|
|
|
{ |
965
|
|
|
$field = $this->getField($definition, $filter->getField()); |
966
|
|
|
|
967
|
|
|
if ($field instanceof TranslatedField) { |
968
|
|
|
$field = EntityDefinitionQueryHelper::getTranslatedField($definition, $field); |
969
|
|
|
} |
970
|
|
|
|
971
|
|
|
if ($field instanceof CustomFields) { |
972
|
|
|
$accessor = \explode('.', $filter->getField()); |
973
|
|
|
$last = \array_pop($accessor); |
974
|
|
|
|
975
|
|
|
$temp = $this->customFieldService->getCustomField($last); |
|
|
|
|
976
|
|
|
|
977
|
|
|
$field = $temp ?? $field; |
978
|
|
|
} |
979
|
|
|
|
980
|
|
|
if ($field instanceof BoolField) { |
981
|
|
|
return match (true) { |
982
|
|
|
$value === null => null, |
983
|
|
|
\is_array($value) => \array_map(fn ($value) => (bool) $value, $value), |
984
|
|
|
default => (bool) $value, |
985
|
|
|
}; |
986
|
|
|
} |
987
|
|
|
|
988
|
|
|
if ($field instanceof DateTimeField) { |
989
|
|
|
return match (true) { |
990
|
|
|
$value === null => null, |
991
|
|
|
\is_array($value) => \array_map(fn ($value) => (new \DateTime($value))->format('Y-m-d H:i:s.000'), $value), |
992
|
|
|
default => (new \DateTime($value))->format('Y-m-d H:i:s.000'), |
993
|
|
|
}; |
994
|
|
|
} |
995
|
|
|
|
996
|
|
|
if ($field instanceof FloatField) { |
997
|
|
|
return match (true) { |
998
|
|
|
$value === null => null, |
999
|
|
|
\is_array($value) => \array_map(fn ($value) => (float) $value, $value), |
1000
|
|
|
default => (float) $value, |
1001
|
|
|
}; |
1002
|
|
|
} |
1003
|
|
|
|
1004
|
|
|
if ($field instanceof IntField) { |
1005
|
|
|
return match (true) { |
1006
|
|
|
$value === null => null, |
1007
|
|
|
\is_array($value) => \array_map(fn ($value) => (int) $value, $value), |
1008
|
|
|
default => (int) $value, |
1009
|
|
|
}; |
1010
|
|
|
} |
1011
|
|
|
|
1012
|
|
|
return $value; |
1013
|
|
|
} |
1014
|
|
|
|
1015
|
|
|
private function createTranslatedSorting(string $root, FieldSorting $sorting, Context $context): FieldSort |
1016
|
|
|
{ |
1017
|
|
|
$parts = explode('.', $sorting->getField()); |
1018
|
|
|
if ($root === $parts[0]) { |
1019
|
|
|
array_shift($parts); |
1020
|
|
|
} |
1021
|
|
|
|
1022
|
|
|
if ($parts[0] === 'customFields') { |
1023
|
|
|
$customField = $this->customFieldService->getCustomField($parts[1]); |
|
|
|
|
1024
|
|
|
|
1025
|
|
|
if ($customField instanceof IntField || $customField instanceof FloatField) { |
1026
|
|
|
return new FieldSort('_script', $sorting->getDirection(), null, [ |
1027
|
|
|
'type' => 'number', |
1028
|
|
|
'script' => [ |
1029
|
|
|
'id' => 'numeric_translated_field_sorting', |
1030
|
|
|
'params' => [ |
1031
|
|
|
'field' => 'customFields', |
1032
|
|
|
'languages' => $context->getLanguageIdChain(), |
1033
|
|
|
'suffix' => $parts[1] ?? '', |
1034
|
|
|
'order' => strtolower($sorting->getDirection()), |
1035
|
|
|
], |
1036
|
|
|
], |
1037
|
|
|
]); |
1038
|
|
|
} |
1039
|
|
|
|
1040
|
|
|
return new FieldSort('_script', $sorting->getDirection(), null, [ |
1041
|
|
|
'type' => 'string', |
1042
|
|
|
'script' => [ |
1043
|
|
|
'id' => 'translated_field_sorting', |
1044
|
|
|
'params' => [ |
1045
|
|
|
'field' => 'customFields', |
1046
|
|
|
'languages' => $context->getLanguageIdChain(), |
1047
|
|
|
'suffix' => $parts[1] ?? '', |
1048
|
|
|
], |
1049
|
|
|
], |
1050
|
|
|
]); |
1051
|
|
|
} |
1052
|
|
|
|
1053
|
|
|
return new FieldSort('_script', $sorting->getDirection(), null, [ |
1054
|
|
|
'type' => 'string', |
1055
|
|
|
'script' => [ |
1056
|
|
|
'id' => 'translated_field_sorting', |
1057
|
|
|
'params' => [ |
1058
|
|
|
'field' => implode('.', $parts), |
1059
|
|
|
'languages' => $context->getLanguageIdChain(), |
1060
|
|
|
], |
1061
|
|
|
], |
1062
|
|
|
]); |
1063
|
|
|
} |
1064
|
|
|
|
1065
|
|
|
private function getTranslatedFieldName(string $accessor, string $languageId): string |
1066
|
|
|
{ |
1067
|
|
|
$parts = explode('.', $accessor); |
1068
|
|
|
|
1069
|
|
|
if ($parts[0] !== 'customFields') { |
1070
|
|
|
return sprintf('%s.%s', $accessor, $languageId); |
1071
|
|
|
} |
1072
|
|
|
|
1073
|
|
|
return sprintf('%s.%s.%s', $parts[0], $languageId, $parts[1]); |
1074
|
|
|
} |
1075
|
|
|
} |
1076
|
|
|
|
This class constant has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.