PluginProbe
Travelpayouts / trunk
Travelpayouts vtrunk
1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.20 1.1.21 1.1.22 1.1.23 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 All 56 releases
travelpayouts / src / components / arrayQuery / QueryProcessor.php

QueryProcessor.php in Travelpayouts trunk, at src/components/arrayQuery/QueryProcessor.php

493 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Travelpayouts\components\arrayQuery;
4
5 use Travelpayouts\components\BaseObject;
6 use Travelpayouts\components\exceptions\InvalidArgumentException;
7 use Travelpayouts\components\exceptions\InvalidConfigException;
8 use Travelpayouts\helpers\ArrayHelper;
9 use Travelpayouts\interfaces\Arrayable;
10
11 /**
12 * QueryProcessor processes [[Query]] instances adjusting data accordingly.
13 * It applies filter conditions, sorting and limit.
14 * @see Query
15 * @author Paul Klimov <klimov.paul@gmail.com>
16 * @author Igor Chepurnoy <igorzfort@gmail.com>
17 * @since 1.0
18 */
19 class QueryProcessor extends BaseObject
20 {
21 /**
22 * @var array
23 */
24 protected $conditionFilters = [
25 'NOT' => 'filterNotCondition',
26 'AND' => 'filterAndCondition',
27 'OR' => 'filterOrCondition',
28 'BETWEEN' => 'filterBetweenCondition',
29 'NOT BETWEEN' => 'filterBetweenCondition',
30 'IN' => 'filterInCondition',
31 'NOT IN' => 'filterInCondition',
32 'LIKE' => 'filterLikeCondition',
33 'NOT LIKE' => 'filterLikeCondition',
34 'OR LIKE' => 'filterLikeCondition',
35 'OR NOT LIKE' => 'filterLikeCondition',
36 'CALLBACK' => 'filterCallbackCondition',
37 ];
38
39 /**
40 * @var ArrayQuery
41 */
42 private $_query;
43
44 /**
45 * @param ArrayQuery $query
46 * @return array[]
47 */
48 public function process($query)
49 {
50 $this->_query = $query;
51
52 $data = $this->_query->from;
53 $this->checkInputArray($data);
54
55 $data = $this->applyWhere($data, $this->_query->where);
56 $data = $this->applyOrderBy($data, $this->_query->orderBy);
57 $data = $this->applyLimit($data, $this->_query->limit, $this->_query->offset);
58
59 return $data;
60 }
61
62 /**
63 * Проверяем в�
64 одящие данные
65 * @param array $data
66 * @return void
67 * @throws InvalidConfigException
68 */
69 protected function checkInputArray(array $data): void
70 {
71 foreach ($data as $item) {
72 if (is_object($item) && !$item instanceof \ArrayAccess) {
73 throw new InvalidConfigException('Filtered array item must implement ArrayAccess');
74 }
75 }
76 }
77
78 /**
79 * Applies sort for given data.
80 * @param array $data raw data
81 * @param array|null $orderBy order by
82 * @return array sorted data
83 */
84 protected function applyOrderBy(array $data, $orderBy)
85 {
86 if (!empty($orderBy)) {
87 ArrayHelper::multisort($data, array_keys($orderBy), array_values($orderBy));
88 }
89
90 return $data;
91 }
92
93 /**
94 * Applies limit and offset for given data.
95 * @param array $data raw data
96 * @param int|null $limit limit value
97 * @param int|null $offset offset value
98 * @return array data
99 */
100 protected function applyLimit(array $data, $limit, $offset)
101 {
102 if (empty($limit) && empty($offset)) {
103 return $data;
104 }
105
106 if (!ctype_digit((string)$limit)) {
107 $limit = null;
108 }
109
110 if (!ctype_digit((string)$offset)) {
111 $offset = 0;
112 }
113
114 return array_slice($data, $offset, $limit);
115 }
116
117 /**
118 * Applies where conditions.
119 * @param array $data raw data
120 * @param array|null $where where conditions
121 * @return array data
122 */
123 protected function applyWhere(array $data, $where)
124 {
125 return $this->filterCondition($data, $where);
126 }
127
128 /**
129 * Applies filter conditions.
130 * @param array $data data to be filtered
131 * @param array $condition filter condition
132 * @return array filtered data
133 * @throws InvalidArgumentException
134 */
135 public function filterCondition(array $data, $condition)
136 {
137 if (empty($condition)) {
138 return $data;
139 }
140
141 if (!is_array($condition)) {
142 throw new InvalidArgumentException('Condition must be an array');
143 }
144
145 if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
146 $operator = strtoupper($condition[0]);
147 if (isset($this->conditionFilters[$operator])) {
148 $method = $this->conditionFilters[$operator];
149 } else {
150 $method = 'filterSimpleCondition';
151 }
152
153 array_shift($condition);
154
155 return $this->$method($data, $operator, $condition);
156 } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
157 return $this->filterHashCondition($data, $condition);
158 }
159 }
160
161 /**
162 * Applies a condition based on column-value pairs.
163 * @param array $data data to be filtered
164 * @param array $condition the condition specification
165 * @return array filtered data
166 */
167 public function filterHashCondition(array $data, $condition)
168 {
169 foreach ($condition as $column => $value) {
170 if (is_array($value)) {
171 // IN condition
172 $data = $this->filterInCondition($data, 'IN', [$column, $value]);
173 } else {
174 $data = array_filter($data, function ($row) use ($column, $value) {
175 if ($value instanceof \Closure) {
176 return call_user_func($value, $row[$column]);
177 }
178
179 return $row[$column] == $value;
180 });
181 }
182 }
183
184 return $data;
185 }
186
187 /**
188 * Applies 2 or more conditions using 'AND' logic.
189 * @param array $data data to be filtered
190 * @param string $operator operator
191 * @param array $operands conditions to be united
192 * @return array filtered data
193 */
194 protected function filterAndCondition(array $data, $operator, $operands)
195 {
196 foreach ($operands as $operand) {
197 if (is_array($operand)) {
198 $data = $this->filterCondition($data, $operand);
199 }
200 }
201
202 return $data;
203 }
204
205 /**
206 * Applies 2 or more conditions using 'OR' logic.
207 * @param array $data data to be filtered
208 * @param string $operator operator
209 * @param array $operands conditions to be united
210 * @return array filtered data
211 */
212 protected function filterOrCondition(array $data, $operator, $operands)
213 {
214 $parts = [];
215 foreach ($operands as $operand) {
216 if (is_array($operand)) {
217 $parts[] = $this->filterCondition($data, $operand);
218 }
219 }
220
221 if (empty($parts)) {
222 return $data;
223 }
224
225 $data = [];
226 foreach ($parts as $part) {
227 foreach ($part as $row) {
228 $pk = $row[$this->_query->primaryKeyName];
229 $data[$pk] = $row;
230 }
231 }
232
233 return $data;
234 }
235
236 /**
237 * Inverts a filter condition.
238 * @param array $data data to be filtered
239 * @param string $operator operator
240 * @param array $operands operands to be inverted
241 * @return array filtered data
242 * @throws InvalidArgumentException if wrong number of operands have been given
243 */
244 protected function filterNotCondition(array $data, $operator, $operands)
245 {
246 if (count($operands) != 1) {
247 throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
248 }
249
250 $operand = reset($operands);
251 $filteredData = $this->filterCondition($data, $operand);
252 if (empty($filteredData)) {
253 return $data;
254 }
255
256 $pkName = $this->_query->primaryKeyName;
257 foreach ($data as $key => $row) {
258 foreach ($filteredData as $filteredRowKey => $filteredRow) {
259 if ($row[$pkName] === $filteredRow[$pkName]) {
260 unset($data[$key]);
261 unset($filteredData[$filteredRowKey]);
262 break;
263 }
264 }
265 }
266
267 return $data;
268 }
269
270 /**
271 * Applies `BETWEEN` condition.
272 * @param array $data data to be filtered
273 * @param string $operator operator
274 * @param array $operands the first operand is the column name. The second and third operands
275 * describe the interval that column value should be in
276 * @return array filtered data
277 * @throws InvalidArgumentException if wrong number of operands have been given
278 */
279 protected function filterBetweenCondition(array $data, $operator, $operands)
280 {
281 if (!isset($operands[0], $operands[1], $operands[2])) {
282 throw new InvalidArgumentException("Operator '$operator' requires three operands.");
283 }
284
285 [$column, $value1, $value2] = $operands;
286
287 if (strncmp('NOT', $operator, 3) === 0) {
288 return array_filter($data, function ($row) use ($column, $value1, $value2) {
289 return $row[$column] < $value1 || $row[$column] > $value2;
290 });
291 }
292
293 return array_filter($data, function ($row) use ($column, $value1, $value2) {
294 return $row[$column] >= $value1 && $row[$column] <= $value2;
295 });
296 }
297
298 /**
299 * Applies 'IN' condition.
300 * @param array $data data to be filtered
301 * @param string $operator operator
302 * @param array $operands the first operand is the column name.
303 * The second operand is an array of values that column value should be among
304 * @return array filtered data
305 * @throws InvalidArgumentException if wrong number of operands have been given
306 */
307 protected function filterInCondition(array $data, $operator, $operands)
308 {
309 if (!isset($operands[0], $operands[1])) {
310 throw new InvalidArgumentException("Operator '$operator' requires two operands.");
311 }
312
313 [$column, $values] = $operands;
314
315 if ($values === [] || $column === []) {
316 return $operator === 'IN' ? [] : $data;
317 }
318
319 $values = (array)$values;
320
321 if (count((array)$column) > 1) {
322 throw new InvalidArgumentException("Operator '$operator' allows only a single column.");
323 }
324
325 if (is_array($column)) {
326 $column = reset($column);
327 }
328
329 foreach ($values as $i => $value) {
330 if (is_array($value)) {
331 $values[$i] = isset($value[$column]) ? $value[$column] : null;
332 }
333 }
334
335 if (strncmp('NOT', $operator, 3) === 0) {
336 return array_filter($data, function ($row) use ($column, $values) {
337 return !in_array($row[$column], $values);
338 });
339 }
340
341 return array_filter($data, function ($row) use ($column, $values) {
342 return in_array($row[$column], $values);
343 });
344 }
345
346 /**
347 * Applies 'LIKE' condition.
348 * @param array $data data to be filtered
349 * @param string $operator operator
350 * @param array $operands the first operand is the column name. The second operand is a single value
351 * or an array of values that column value should be compared with
352 * @return array filtered data
353 * @throws InvalidArgumentException if wrong number of operands have been given
354 */
355 protected function filterLikeCondition(array $data, $operator, $operands)
356 {
357 if (!isset($operands[0], $operands[1])) {
358 throw new InvalidArgumentException("Operator '$operator' requires two operands.");
359 }
360
361 [$column, $values] = $operands;
362
363 if (!is_array($values)) {
364 $values = [$values];
365 }
366
367 $not = (stripos($operator, 'NOT ') !== false);
368 $or = (stripos($operator, 'OR ') !== false);
369
370 if ($not) {
371 if (empty($values)) {
372 return $data;
373 }
374
375 if ($or) {
376 return array_filter($data, function ($row) use ($column, $values) {
377 foreach ($values as $value) {
378 if (stripos($row[$column], $value) === false) {
379 return true;
380 }
381 }
382
383 return false;
384 });
385 }
386
387 return array_filter($data, function ($row) use ($column, $values) {
388 foreach ($values as $value) {
389 if (stripos($row[$column], $value) !== false) {
390 return false;
391 }
392 }
393
394 return true;
395 });
396 }
397
398 if (empty($values)) {
399 return [];
400 }
401
402 if ($or) {
403 return array_filter($data, function ($row) use ($column, $values) {
404 foreach ($values as $value) {
405 if (stripos($row[$column], $value) !== false) {
406 return true;
407 }
408 }
409
410 return false;
411 });
412 }
413
414 return array_filter($data, function ($row) use ($column, $values) {
415 foreach ($values as $value) {
416 if (stripos($row[$column], $value) === false) {
417 return false;
418 }
419 }
420
421 return true;
422 });
423 }
424
425 /**
426 * Applies 'CALLBACK' condition.
427 * @param array $data data to be filtered
428 * @param string $operator operator
429 * @param array $operands the only one operand is the PHP callback, which should be compatible with
430 * `array_filter()` PHP function, e.g.:
431 * ```php
432 * function ($row) {
433 * //return bool whether row matches condition or not
434 * }
435 * ```
436 * @return array filtered data
437 * @throws InvalidArgumentException if wrong number of operands have been given
438 * @since 1.0.3
439 */
440 public function filterCallbackCondition(array $data, $operator, $operands)
441 {
442 if (count($operands) != 1) {
443 throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
444 }
445
446 $callback = reset($operands);
447
448 return array_filter($data, $callback);
449 }
450
451 /**
452 * Applies comparison condition, e.g. `column operator value`.
453 * @param array $data data to be filtered
454 * @param string $operator operator
455 * @param array $operands
456 * @return array filtered data
457 * @throws InvalidArgumentException if wrong number of operands have been given or operator is not supported
458 * @since 1.0.4
459 */
460 public function filterSimpleCondition(array $data, $operator, $operands)
461 {
462 if (count($operands) !== 2) {
463 throw new InvalidArgumentException("Operator '$operator' requires two operands.");
464 }
465 [$column, $value] = $operands;
466
467 return array_filter($data, function ($row) use ($operator, $column, $value) {
468 switch ($operator) {
469 case '=':
470 case '==':
471 return $row[$column] == $value;
472 case '===':
473 return $row[$column] === $value;
474 case '!=':
475 case '<>':
476 return $row[$column] != $value;
477 case '!==':
478 return $row[$column] !== $value;
479 case '>':
480 return $row[$column] > $value;
481 case '<':
482 return $row[$column] < $value;
483 case '>=':
484 return $row[$column] >= $value;
485 case '<=':
486 return $row[$column] <= $value;
487 default:
488 throw new InvalidArgumentException("Operator '$operator' is not supported.");
489 }
490 });
491 }
492 }
493