Passed
Push — master ( 4fca9e...461dc1 )
by Vitalik
05:17
created

Finder::prefabFilter()   C

Complexity

Conditions 15
Paths 77

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 15

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 29
c 1
b 0
f 0
nc 77
nop 1
dl 0
loc 50
ccs 28
cts 28
cp 1
crap 15
rs 5.9166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Sheerockoff\BitrixElastic;
4
5
use Elasticsearch\Client;
6
use Exception;
7
use InvalidArgumentException;
8
use stdClass;
9
10
class Finder
11
{
12
    private const LOGIC_AND = 'AND';
13
    private const LOGIC_OR = 'OR';
14
15
    /** @var Client */
16
    private $elastic;
17
18
    /** @var Mapper */
19
    private $mapper;
20
21
    /** @var bool */
22
    private $strictMode;
23
24 28
    public function __construct(Client $elastic, Mapper $mapper, $strictMode = true)
25
    {
26 28
        $this->elastic = $elastic;
27 28
        $this->mapper = $mapper;
28 28
        $this->strictMode = $strictMode;
29
    }
30
31
    /**
32
     * @throws Exception
33
     */
34 22
    public function search(string $index, array $filter, array $sort = ['SORT' => 'ASC', 'ID' => 'DESC'], array $parameters = []): array
35
    {
36 22
        $params = $this->prefabElasticSearchParams($index, $filter, $sort, $parameters);
37 12
        return $this->elastic->search($params);
38
    }
39
40
    /**
41
     * @throws Exception
42
     */
43 23
    public function prefabElasticSearchParams(string $index, array $filter, array $sort = ['SORT' => 'ASC', 'ID' => 'DESC'], array $parameters = []): array
44
    {
45 23
        $mapping = $this->mapper->getCachedMapping($index);
46 23
        $filter = $this->prefabFilter($filter);
47 23
        $filter = $this->normalizeFilter($mapping, $filter);
48 20
        $query = $this->prepareFilterQuery($filter);
49
50 15
        $params = ['index' => $index, 'body' => ['query' => $query]];
51 15
        $params['body'] = array_merge($params['body'], $this->normalizeSort($mapping, $sort));
52
53 13
        return array_merge($params, $parameters);
54
    }
55
56 23
    private function prefabFilter(array $filter): array
57
    {
58 23
        $changedFilter = $filter;
59 23
        $includeSubsections = false;
60 23
        foreach ($filter as $key => $rawValue) {
61 18
            if (!preg_match('/^(?<operator>\W*)(?<property>\w+)$/ui', $key, $matches)) {
62 2
                continue;
63
            }
64
65 16
            if ($matches['property'] !== 'INCLUDE_SUBSECTIONS') {
66 16
                continue;
67
            }
68
69 1
            $includeSubsections = $rawValue && $rawValue !== 'N';
70 1
            if (array_key_exists($key, $changedFilter)) {
71 1
                unset($changedFilter[$key]);
72
            }
73
74 1
            break;
75
        }
76
77 23
        foreach ($filter as $key => $rawValue) {
78 18
            if (!preg_match('/^(?<operator>\W*)(?<property>\w+)$/ui', $key, $matches)) {
79 2
                continue;
80
            }
81
82 16
            if ($matches['property'] === 'IBLOCK_SECTION_ID' || $matches['property'] === 'SECTION_ID') {
83 1
                if (array_key_exists($key, $changedFilter)) {
84 1
                    unset($changedFilter[$key]);
85
                }
86
87 1
                if ($includeSubsections) {
88 1
                    $changedFilter['NAV_CHAIN_IDS'] = $rawValue;
89
                } else {
90 1
                    $changedFilter['GROUP_IDS'] = $rawValue;
91
                }
92 16
            } elseif ($matches['property'] === 'SECTION_CODE') {
93 1
                if (array_key_exists($key, $changedFilter)) {
94 1
                    unset($changedFilter[$key]);
95
                }
96
97 1
                if ($includeSubsections) {
98 1
                    $changedFilter['NAV_CHAIN_CODES'] = $rawValue;
99
                } else {
100 1
                    $changedFilter['GROUP_CODES'] = $rawValue;
101
                }
102
            }
103
        }
104
105 23
        return $changedFilter;
106
    }
107
108
    /**
109
     * @throws InvalidArgumentException
110
     */
111 27
    private function normalizeFilter(IndexMapping $mapping, array $filter): array
112
    {
113 27
        $normalizedFilter = [];
114 27
        foreach ($filter as $k => $v) {
115 22
            if (is_array($v) && array_key_exists('LOGIC', $v)) {
116 8
                $subFilter = $v;
117 8
                unset($subFilter['LOGIC']);
118 8
                $subFilter = $this->normalizeFilter($mapping, $subFilter);
119 7
                $normalizedFilter[$k] = array_merge(['LOGIC' => $v['LOGIC']], $subFilter);
120 7
                continue;
121
            }
122
123 22
            if (!preg_match('/^(?<operator>\W*)(?<property>\w+)$/ui', $k, $matches)) {
124 4
                if ($this->strictMode) throw new InvalidArgumentException("Неверный ключ фильтра ($k).");
125 2
                continue;
126
            }
127
128 18
            $property = $matches['property'];
129
130 18
            if (!$mapping->getProperties()->offsetExists($property)) {
131 2
                if ($this->strictMode) throw new InvalidArgumentException("$property не найден в карте индекса.");
132 1
                continue;
133
            }
134
135 16
            $isAlias = $mapping->getProperty($property)->get('type') === 'alias';
136 16
            if ($isAlias) {
137 5
                $aliasPath = $mapping->getProperty($property)->get('path');
138 5
                if (!$aliasPath) {
139 2
                    if ($this->strictMode) throw new InvalidArgumentException("В $property типа alias не указан path.");
140 1
                    continue;
141
                }
142
143 3
                if (!$mapping->getProperties()->offsetExists($aliasPath)) {
144 2
                    if ($this->strictMode) {
145 1
                        throw new InvalidArgumentException(
146 1
                            "$property типа alias указывает на $aliasPath, который не найден в карте индекса."
147 1
                        );
148
                    }
149
150 1
                    continue;
151
                }
152
153 1
                $property = $aliasPath;
154
            }
155
156 12
            if (is_array($v)) {
157 5
                $value = array_map(function ($v) use ($mapping, $property) {
158 5
                    return $mapping->getProperty($property)->normalizeValue($v);
159 5
                }, $v);
160
            } else {
161 8
                $value = $mapping->getProperty($property)->normalizeValue($v);
162
            }
163
164 12
            $normalizedFilter[$k] = $value;
165
        }
166
167 22
        return $normalizedFilter;
168
    }
169
170
    /**
171
     * @throws InvalidArgumentException
172
     */
173 15
    private function normalizeSort(IndexMapping $mapping, array $sort): array
174
    {
175 15
        $elasticSorts = [];
176 15
        foreach ($sort as $property => $term) {
177 15
            if (!$mapping->getProperties()->offsetExists($property)) {
178 2
                if ($this->strictMode) throw new InvalidArgumentException("$property не найден в карте индекса для сортировки.");
179 1
                continue;
180
            }
181
182 13
            $emptySort = null;
183 13
            if (preg_match('/(?<first>nulls\s*,\s*)?(?<dir>asc|desc)(?<last>\s*,\s*nulls)?/ui', $term, $matches)) {
184 11
                $sortOrder = strtolower($matches['dir']);
185 11
                if (!empty($matches['first'])) {
186 1
                    $emptySort = 'asc';
187 11
                } elseif (!empty($matches['last'])) {
188 11
                    $emptySort = 'desc';
189
                }
190
            } else {
191 2
                if ($this->strictMode) throw new InvalidArgumentException("Неверный формат сортировки ($term).");
192 1
                continue;
193
            }
194
195 11
            $propMap = $mapping->getProperty($property);
196 11
            if (isset($propMap->getData()['fields']['enum'])) {
197 1
                $sortField = $property . '_VALUE';
198
            } else {
199 11
                $sortField = $property;
200
            }
201
202 11
            if ($emptySort) {
203 1
                $emptyValuesForTypes = [
204 1
                    'integer' => '0',
205 1
                    'long' => '0',
206 1
                    'float' => '0.0',
207 1
                    'double' => '0.0',
208 1
                    'date' => 'null',
209 1
                    'boolean' => 'false',
210 1
                    'string' => '',
211 1
                ];
212
213 1
                $elasticSorts[] = [
214 1
                    '_script' => [
215 1
                        'type' => 'number',
216 1
                        'script' => [
217 1
                            'lang' => 'painless',
218 1
                            'source' => sprintf(
219 1
                                "if (doc['%s'].empty || doc['%s'].value == %s) { return 0; } else { return 1; }",
220 1
                                $sortField,
221 1
                                $sortField,
222 1
                                $emptyValuesForTypes[$propMap->get('type')] ?? 'null'
223 1
                            )
224 1
                        ],
225 1
                        'order' => $emptySort
226 1
                    ]
227 1
                ];
228
            }
229
230 11
            $elasticSorts[] = [$sortField => ['order' => $sortOrder]];
231
        }
232
233 13
        return $elasticSorts ? ['sort' => $elasticSorts] : [];
234
    }
235
236
    /**
237
     * @throws InvalidArgumentException
238
     */
239 22
    private function prepareFilterQuery(array $filter, string $logic = self::LOGIC_AND): array
240
    {
241 22
        if (!in_array($logic, [self::LOGIC_AND, self::LOGIC_OR], true)) {
242 1
            throw new InvalidArgumentException("Неверный логический оператор ($logic).");
243
        }
244
245 22
        $terms = [];
246 22
        foreach ($filter as $k => $value) {
247 15
            if (is_array($value) && array_key_exists('LOGIC', $value)) {
248 7
                $subFilter = $value;
249 7
                unset($subFilter['LOGIC']);
250 7
                $subQuery = $this->prepareFilterQuery($subFilter, strtoupper($value['LOGIC']));
251 4
                $terms = array_merge_recursive($terms, [($logic === self::LOGIC_OR ? 'should' : 'must') => [$subQuery]]);
252
            } else {
253 13
                if (!preg_match('/^(?<operator>\W*)(?<field>\w+)$/ui', $k, $matches)) {
254 2
                    if ($this->strictMode) throw new InvalidArgumentException("Неверный ключ фильтра ($k).");
255 1
                    continue;
256
                }
257
258 11
                $operator = $matches['operator'] ?: '=';
259 11
                $field = $matches['field'];
260
261
                try {
262 11
                    if ($logic === self::LOGIC_OR) {
263 5
                        $entry = $this->or($field, $operator, $value);
264
                    } else {
265 7
                        $entry = $this->and($field, $operator, $value);
266
                    }
267 8
                } catch (Exception $exception) {
268 8
                    if ($this->strictMode) throw $exception;
269 4
                    continue;
270
                }
271
272 3
                $terms = array_merge_recursive($terms, $entry);
273
            }
274
        }
275
276 16
        return $terms ? ['bool' => $terms] : ['match_all' => new stdClass()];
277
    }
278
279
    /**
280
     * @throws InvalidArgumentException
281
     */
282 7
    private function and(string $field, string $operator, $value)
283
    {
284 7
        $handlers = [
285 7
            '=' => function ($field, $value) {
286 2
                if ($value === false || $value === null) {
287 1
                    return ['must_not' => [['exists' => ['field' => $field]]]];
288
                } else {
289 2
                    return ['must' => [[(is_array($value) ? 'terms' : 'term') => [$field => $value]]]];
290
                }
291 7
            },
292 7
            '!' => function ($field, $value) {
293 1
                if ($value === false || $value === null) {
294 1
                    return ['must' => [['exists' => ['field' => $field]]]];
295
                } else {
296 1
                    return ['must_not' => [[(is_array($value) ? 'terms' : 'term') => [$field => $value]]]];
297
                }
298 7
            },
299 7
            '%' => function ($field, $value) {
300 1
                return ['must' => [['wildcard' => [$field => "*$value*"]]]];
301 7
            },
302 7
            '>' => function ($field, $value) {
303 2
                return ['must' => [['range' => [$field => ['gt' => $value]]]]];
304 7
            },
305 7
            '>=' => function ($field, $value) {
306 1
                return ['must' => [['range' => [$field => ['gte' => $value]]]]];
307 7
            },
308 7
            '<' => function ($field, $value) {
309 1
                return ['must' => [['range' => [$field => ['lt' => $value]]]]];
310 7
            },
311 7
            '<=' => function ($field, $value) {
312 1
                return ['must' => [['range' => [$field => ['lte' => $value]]]]];
313 7
            },
314 7
            '><' => function ($field, $value) {
315 3
                if (!isset($value[0]) || !isset($value[1])) {
316 2
                    throw new InvalidArgumentException("Для фильтра ><$field должен быть указан массив значений.");
317
                }
318
319 1
                return ['must' => [['range' => [$field => ['gte' => min($value), 'lte' => max($value)]]]]];
320 7
            }
321 7
        ];
322
323 7
        if (!array_key_exists($operator, $handlers)) {
324 2
            throw new InvalidArgumentException("Невозможно отфильтровать $field по оператору $operator.");
325
        }
326
327 5
        return $handlers[$operator]($field, $value);
328
    }
329
330
    /**
331
     * @throws InvalidArgumentException
332
     */
333 5
    private function or($field, $operator, $value)
334
    {
335 5
        $handlers = [
336 5
            '=' => function ($field, $value) {
337 1
                if ($value === false || $value === null) {
338 1
                    return ['should' => [['bool' => ['must_not' => [['exists' => ['field' => $field]]]]]]];
339
                } else {
340 1
                    return ['should' => [[(is_array($value) ? 'terms' : 'term') => [$field => $value]]]];
341
                }
342 5
            },
343 5
            '!' => function ($field, $value) {
344 1
                if ($value === false || $value === null) {
345 1
                    return ['should' => [['exists' => ['field' => $field]]]];
346
                } else {
347 1
                    return ['should' => [['bool' => ['must_not' => [[(is_array($value) ? 'terms' : 'term') => [$field => $value]]]]]]];
348
                }
349 5
            },
350 5
            '%' => function ($field, $value) {
351 1
                return ['should' => [['wildcard' => [$field => "*$value*"]]]];
352 5
            },
353 5
            '>' => function ($field, $value) {
354 1
                return ['should' => [['range' => [$field => ['gt' => $value]]]]];
355 5
            },
356 5
            '>=' => function ($field, $value) {
357 1
                return ['should' => [['range' => [$field => ['gte' => $value]]]]];
358 5
            },
359 5
            '<' => function ($field, $value) {
360 1
                return ['should' => [['range' => [$field => ['lt' => $value]]]]];
361 5
            },
362 5
            '<=' => function ($field, $value) {
363 1
                return ['should' => [['range' => [$field => ['lte' => $value]]]]];
364 5
            },
365 5
            '><' => function ($field, $value) {
366 3
                if (!isset($value[0]) || !isset($value[1])) {
367 2
                    throw new InvalidArgumentException("Для фильтра ><$field должен быть указан массив значений.");
368
                }
369
370 1
                return ['should' => [['range' => [$field => ['gte' => min($value), 'lte' => max($value)]]]]];
371 5
            }
372 5
        ];
373
374 5
        if (!array_key_exists($operator, $handlers)) {
375 2
            throw new InvalidArgumentException("Невозможно отфильтровать $field по оператору $operator.");
376
        }
377
378 3
        return $handlers[$operator]($field, $value);
379
    }
380
}