HasFilter   F
last analyzed

Complexity

Total Complexity 91

Size/Duplication

Total Lines 879
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 91
eloc 182
c 4
b 0
f 0
dl 0
loc 879
ccs 217
cts 217
cp 1
rs 2

34 Methods

Rating   Name   Duplication   Size   Complexity  
F where() 0 129 33
A orWhereExpression() 0 3 1
A useClosureAsFilter() 0 16 2
A useFilter() 0 5 2
A whereNull() 0 7 2
A orWhereFlags() 0 3 1
A orWhereColumn() 0 3 1
A whereSpatialPolygon() 0 11 2
A columnCompareDimension() 0 15 3
A orWhereArrayContains() 0 3 1
A whereColumn() 0 8 1
A orWhere() 0 6 1
A orWhereBetween() 0 7 1
A whereNot() 0 16 2
A orWhereSpatialPolygon() 0 3 1
A orWhereInterval() 0 3 1
A whereExpression() 0 7 2
A whereInterval() 0 8 1
A whereBetween() 0 10 1
A orWhereSpatialRectangular() 0 3 1
A orWhereNot() 0 3 1
A orWhereNull() 0 3 1
A whereFlags() 0 23 2
A orWhereSpatialRadius() 0 3 1
A whereArrayContains() 0 5 1
A whereSpatialRadius() 0 7 2
A orWhereIn() 0 3 1
A whereIn() 0 5 1
A whereSpatialRectangular() 0 11 2
A addOrFilter() 0 15 3
A addAndFilter() 0 15 3
B normalizeIntervals() 0 35 9
A getFilter() 0 3 1
A isDruidInterval() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like HasFilter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasFilter, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace Level23\Druid\Concerns;
5
6
use Closure;
7
use InvalidArgumentException;
8
use Level23\Druid\Types\DataType;
9
use Level23\Druid\Filters\InFilter;
10
use Level23\Druid\Filters\OrFilter;
11
use Level23\Druid\Filters\AndFilter;
12
use Level23\Druid\Filters\NotFilter;
13
use Level23\Druid\Interval\Interval;
14
use Level23\Druid\Filters\LikeFilter;
15
use Level23\Druid\Filters\NullFilter;
16
use Level23\Druid\Filters\RegexFilter;
17
use Level23\Druid\Filters\RangeFilter;
18
use Level23\Druid\Filters\SearchFilter;
19
use Level23\Druid\Dimensions\Dimension;
20
use Level23\Druid\Queries\QueryBuilder;
21
use Level23\Druid\Filters\FilterBuilder;
22
use Level23\Druid\Filters\BetweenFilter;
23
use Level23\Druid\Filters\IntervalFilter;
24
use Level23\Druid\Filters\EqualityFilter;
25
use Level23\Druid\Filters\FilterInterface;
26
use Level23\Druid\Filters\JavascriptFilter;
27
use Level23\Druid\Filters\ExpressionFilter;
28
use Level23\Druid\Interval\IntervalInterface;
29
use Level23\Druid\Dimensions\DimensionBuilder;
30
use Level23\Druid\Filters\SpatialRadiusFilter;
31
use Level23\Druid\Filters\ArrayContainsFilter;
32
use Level23\Druid\Filters\SpatialPolygonFilter;
33
use Level23\Druid\Dimensions\DimensionInterface;
34
use Level23\Druid\Filters\ColumnComparisonFilter;
35
use Level23\Druid\Filters\SpatialRectangularFilter;
36
37
trait HasFilter
38
{
39
    protected ?QueryBuilder $query = null;
40
41
    protected ?FilterInterface $filter = null;
42
43
    /**
44
     * Filter our results where the given dimension matches the value based on the operator.
45
     * The operator can be '=', '>', '>=', '<', '<=', '<>', '!=', 'like', 'not like', 'regex', 'not regex',
46
     * 'javascript', 'not javascript', 'search' and 'not search'
47
     *
48
     * @param \Closure|string|FilterInterface     $filterOrDimensionOrClosure The dimension which you want to filter.
49
     * @param int|string|float|bool|null          $operator                   The operator which you want to use to
50
     *                                                                        filter. See below for a complete list of
51
     *                                                                        supported operators.
52
     * @param int|string|string[]|null|float|bool $value                      The value which you want to use in your
53
     *                                                                        filter comparison
54
     * @param string                              $boolean                    This influences how this filter will be
55
     *                                                                        joined with previous added filters.
56
     *                                                                        Should
57
     *                                                                        both filters apply ("and") or one or the
58
     *                                                                        other ("or") ? Default is "and".
59
     *
60
     * @return $this
61
     */
62 61
    public function where(
63
        Closure|string|FilterInterface $filterOrDimensionOrClosure,
64
        int|string|float|bool|null $operator = null,
65
        array|int|string|float|bool|null $value = null,
66
        string $boolean = 'and'
67
    ): self {
68
69 61
        if ($filterOrDimensionOrClosure instanceof FilterInterface) {
70 9
            return $this->useFilter($filterOrDimensionOrClosure, $boolean);
71
        }
72
73 53
        if ($filterOrDimensionOrClosure instanceof Closure) {
74 4
            return $this->useClosureAsFilter($filterOrDimensionOrClosure, $boolean);
75
        }
76
77 51
        if ($operator === null && $value !== null) {
78 1
            throw new InvalidArgumentException('You have to supply an operator when you supply a dimension as string');
79
        }
80
81
        // Allow shorthand method where the operator is left out, just like laravel does.
82 50
        if ($value === null && $operator !== null && !in_array($operator, ['=', '!=', '<>'])) {
83 1
            $value    = $operator;
84 1
            $operator = '=';
85
        }
86
87 50
        if ($operator === null || $value === null) {
88 6
            $operator = '=';
89
        }
90
91 50
        if (is_bool($value)) {
92 1
            $value = $value ? 1 : 0;
93
        }
94
95 50
        $operator = strtolower((string)$operator);
96
97 50
        if ($operator == 'search') {
98 4
            if (is_int($value) || is_float($value)) {
99 1
                $value = (string)$value;
100
            }
101
102 4
            return $this->useFilter(new SearchFilter(
103 4
                $filterOrDimensionOrClosure, $value ?? '', false
0 ignored issues
show
Bug introduced by
It seems like $value ?? '' can also be of type boolean and null; however, parameter $valueOrValues of Level23\Druid\Filters\SearchFilter::__construct() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

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

103
                $filterOrDimensionOrClosure, /** @scrutinizer ignore-type */ $value ?? '', false
Loading history...
104 4
            ), $boolean);
105
        }
106
107 46
        if ($operator == 'not search') {
108 4
            if (is_int($value) || is_float($value)) {
109 1
                $value = (string)$value;
110
            }
111
112 4
            return $this->useFilter(new NotFilter(new SearchFilter(
113 4
                $filterOrDimensionOrClosure, $value ?? '', false
114 4
            )), $boolean);
115
        }
116
117
        // -- Anything below does not accept an array as value --
118
119 42
        if (is_array($value)) {
120 2
            throw new InvalidArgumentException('Given $value is invalid in combination with operator ' . $operator);
121
        }
122
123
        /** @var int|string|float|null $value */
124
125 40
        if ($operator == '=') {
126 21
            return $this->useFilter(new EqualityFilter(
127 21
                $filterOrDimensionOrClosure,
128 21
                is_null($value) ? '' : $value,
129 21
                null # Auto-detect type...
130 21
            ), $boolean);
131
        }
132
133 19
        if ($operator == '<>' || $operator == '!=') {
134 5
            $filter = new EqualityFilter(
135 5
                $filterOrDimensionOrClosure,
136 5
                is_null($value) ? '' : $value,
137 5
                null # Auto-detect type...
138 5
            );
139
140 5
            return $this->useFilter(new NotFilter($filter), $boolean);
141
        }
142
143 14
        if (in_array($operator, ['>', '>=', '<', '<='])) {
144 5
            return $this->useFilter(new RangeFilter(
145 5
                $filterOrDimensionOrClosure,
146 5
                $operator,
147 5
                $value ?? '',
148 5
                null
149 5
            ), $boolean);
150
        }
151
152 9
        if ($operator == 'like') {
153 1
            return $this->useFilter(new LikeFilter(
154 1
                $filterOrDimensionOrClosure, (string)$value, '\\'
155 1
            ), $boolean);
156
        }
157
158 8
        if ($operator == 'not like') {
159 1
            return $this->useFilter(new NotFilter(
160 1
                new LikeFilter($filterOrDimensionOrClosure, (string)$value, '\\')
161 1
            ), $boolean);
162
        }
163
164 7
        if ($operator == 'javascript') {
165 1
            return $this->useFilter(new JavascriptFilter(
166 1
                $filterOrDimensionOrClosure,
167 1
                (string)$value
168 1
            ), $boolean);
169
        }
170
171 6
        if ($operator == 'not javascript') {
172 1
            return $this->useFilter(new NotFilter(
173 1
                new JavascriptFilter($filterOrDimensionOrClosure, (string)$value)
174 1
            ), $boolean);
175
        }
176
177 5
        if ($operator == 'regex' || $operator == 'regexp') {
178 2
            return $this->useFilter(new RegexFilter(
179 2
                $filterOrDimensionOrClosure,
180 2
                (string)$value,
181 2
            ), $boolean);
182
        }
183
184 3
        if ($operator == 'not regex' || $operator == 'not regexp') {
185 2
            return $this->useFilter(new NotFilter(
186 2
                new RegexFilter($filterOrDimensionOrClosure, (string)$value)
187 2
            ), $boolean);
188
        }
189
190 1
        throw new InvalidArgumentException('The arguments which you have supplied cannot be parsed.');
191
    }
192
193
    /**
194
     * Add a filter to our known filters in the given boolean mode (and / or)
195
     *
196
     * @param \Level23\Druid\Filters\FilterInterface $filter
197
     * @param string                                 $boolean
198
     *
199
     * @return $this
200
     */
201 57
    private function useFilter(FilterInterface $filter, string $boolean): self
202
    {
203 57
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
204
205 57
        return $this;
206
    }
207
208
    /**
209
     * Use a closure as a filter. The closure will receive a FilterBuilder as parameter.
210
     *
211
     * @param \Closure $closure
212
     * @param string   $boolean
213
     *
214
     * @return $this
215
     */
216 4
    private function useClosureAsFilter(Closure $closure, string $boolean): self
217
    {
218
        // let's create a new builder object where the user can mess around with
219 4
        $builder = new FilterBuilder($this->query);
220
221
        // call the user function
222 4
        call_user_func($closure, $builder);
223
224 4
        $filter = $builder->getFilter();
225
226 4
        if (!$filter) {
227 1
            throw new InvalidArgumentException('The arguments which you have supplied cannot be parsed.');
228
        }
229
230
        // Now retrieve the filter which was created and add it to our current filter set.
231 3
        return $this->useFilter($filter, $boolean);
232
    }
233
234
    /**
235
     * Check if an ARRAY contains a specific element but can also match against any type of column.
236
     * When matching against scalar columns, scalar columns are treated as single-element arrays.
237
     *
238
     * @param string                $column Input column or virtual column name to filter on.
239
     * @param int|float|string|null $value  Array element value to match. This value can be null.
240
     * @param string                $boolean
241
     *
242
     * @return $this
243
     * @see https://druid.apache.org/docs/latest/querying/filters/#array-contains-element-filter
244
     *
245
     */
246 1
    public function whereArrayContains(string $column, int|float|string|null $value, string $boolean = 'and'): self
247
    {
248 1
        $filter = new ArrayContainsFilter($column, $value);
249
250 1
        return $this->useFilter($filter, $boolean);
251
    }
252
253
    /**
254
     * Check if an ARRAY contains a specific element but can also match against any type of column.
255
     * When matching against scalar columns, scalar columns are treated as single-element arrays.
256
     *
257
     * @param string                $column Input column or virtual column name to filter on.
258
     * @param int|float|string|null $value  Array element value to match. This value can be null.
259
     *
260
     * @return $this
261
     */
262 1
    public function orWhereArrayContains(string $column, int|float|string|null $value): self
263
    {
264 1
        return $this->whereArrayContains($column, $value, 'or');
265
    }
266
267
    /**
268
     * Build a where selection which is inverted
269
     *
270
     * @param \Closure $filterBuilder A closure which will receive a FilterBuilder instance.
271
     * @param string   $boolean       This influences how this filter will be joined with previous added filters.
272
     *                                Should both filters apply ("and") or one or the other ("or") ? Default is "and".
273
     *
274
     * @return $this
275
     */
276 2
    public function whereNot(Closure $filterBuilder, string $boolean = 'and'): self
277
    {
278
        // let's create a bew builder object where the user can mess around with
279 2
        $builder = new FilterBuilder($this->query);
280
281
        // call the user function
282 2
        call_user_func($filterBuilder, $builder);
283
284
        // Now retrieve the filter which was created and add it to our current filter set.
285 2
        $filter = $builder->getFilter();
286 2
        if ($filter) {
287 1
            return $this->where(new NotFilter($filter), null, null, $boolean);
288
        }
289
290
        // Whe no filter was given, just return.
291 1
        return $this;
292
    }
293
294
    /**
295
     * Build a where selection which is inverted
296
     *
297
     * @param \Closure $filterBuilder A closure which will receive a FilterBuilder instance.
298
     *
299
     * @return $this
300
     */
301 1
    public function orWhereNot(Closure $filterBuilder): self
302
    {
303 1
        return $this->whereNot($filterBuilder, 'or');
304
    }
305
306
    /**
307
     * This applies a filter, only it will join previous added filters with an "or" instead of an "and".
308
     * See the documentation of the "where" method for more information
309
     *
310
     * @param string|FilterInterface|Closure      $filterOrDimension
311
     * @param string|int|float|bool|null          $operator
312
     * @param int|string|float|bool|string[]|null $value
313
     *
314
     * @return $this
315
     * @see \Level23\Druid\Concerns\HasFilter::where()
316
     */
317 3
    public function orWhere(
318
        string|FilterInterface|Closure $filterOrDimension,
319
        string|int|float|bool|null $operator = null,
320
        array|int|float|string|bool|null $value = null
321
    ): self {
322 3
        return $this->where($filterOrDimension, $operator, $value, 'or');
323
    }
324
325
    /**
326
     * Filter records where the given dimension exists in the given list of items
327
     *
328
     * @param string         $dimension  The dimension which you want to filter
329
     * @param string[]|int[] $items      A list of values. We will return records where the dimension is in this list.
330
     * @param string         $boolean    This influences how this filter will be joined with previous added filters.
331
     *                                   Should both filters apply ("and") or one or the other ("or") ? Default is
332
     *                                   "and".
333
     *
334
     * @return $this
335
     */
336 2
    public function whereIn(string $dimension, array $items, string $boolean = 'and'): self
337
    {
338 2
        $filter = new InFilter($dimension, $items);
339
340 2
        return $this->where($filter, null, null, $boolean);
341
    }
342
343
    /**
344
     * Filter on (virtual) columns with a value which is equal to NULL. This is especially useful when
345
     * `druid.generic.useDefaultValueForNull=false` was configured.
346
     *
347
     * @param string $column
348
     * @param string $boolean
349
     *
350
     * @return $this
351
     */
352 1
    public function whereNull(string $column, string $boolean = 'and'): self
353
    {
354 1
        $filter = new NullFilter($column);
355
356 1
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
357
358 1
        return $this;
359
    }
360
361
    /**
362
     * Filter on (virtual) columns with a value which is equal to NULL. This is especially useful when
363
     * `druid.generic.useDefaultValueForNull=false` was configured.
364
     *
365
     * @param string $column
366
     *
367
     * @return $this
368
     */
369 1
    public function orWhereNull(string $column): self
370
    {
371 1
        return $this->whereNull($column, 'or');
372
    }
373
374
    /**
375
     * The expression filter allows for the implementation of arbitrary conditions, leveraging the Druid expression
376
     * system.
377
     *
378
     * This filter allows for more flexibility, but it might be less performant than a combination of the other filters
379
     * on this page due to the fact that not all filter optimizations are in place yet.
380
     *
381
     * @param string $expression        The expression to filter on
382
     * @param string $boolean           This influences how this filter will be joined with previous added filters.
383
     *                                  Should both filters apply ("and") or one or the other ("or") ? Default is
384
     *                                  "and".
385
     *
386
     * @return $this
387
     * @see https://druid.apache.org/docs/latest/misc/math-expr.html
388
     */
389 3
    public function whereExpression(string $expression, string $boolean = 'and'): self
390
    {
391 3
        $filter = new ExpressionFilter($expression);
392
393 3
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
394
395 3
        return $this;
396
    }
397
398
    /**
399
     * The expression filter allows for the implementation of arbitrary conditions, leveraging the Druid expression
400
     * system.
401
     *
402
     * This filter allows for more flexibility, but it might be less performant than a combination of the other filters
403
     * on this page due to the fact that not all filter optimizations are in place yet.
404
     *
405
     * @param string $expression
406
     *
407
     * @return $this
408
     */
409 1
    public function orWhereExpression(string $expression): self
410
    {
411 1
        return $this->whereExpression($expression, 'or');
412
    }
413
414
    /**
415
     * Filter records where the given dimension exists in the given list of items.
416
     *
417
     * If there are previously defined filters, this filter will be joined with an "or".
418
     *
419
     * @param string         $dimension The dimension which you want to filter
420
     * @param string[]|int[] $items     A list of values. We will return records where the dimension is in this list.
421
     *
422
     * @return $this
423
     */
424 1
    public function orWhereIn(string $dimension, array $items): self
425
    {
426 1
        return $this->whereIn($dimension, $items, 'or');
427
    }
428
429
    /**
430
     * Filter records where dimensionA is equal to dimensionB.
431
     *
432
     * @param Closure|string $dimensionA The dimension which you want to compare, or a Closure which will receive a
433
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
434
     * @param Closure|string $dimensionB The dimension which you want to compare, or a Closure which will receive a
435
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
436
     * @param string         $boolean    This influences how this filter will be joined with previous added filters.
437
     *                                   Should both filters apply ("and") or one or the other ("or") ? Default is
438
     *                                   "and".
439
     *
440
     * @return $this
441
     */
442 1
    public function whereColumn(Closure|string $dimensionA, Closure|string $dimensionB, string $boolean = 'and'): self
443
    {
444 1
        $filter = new ColumnComparisonFilter(
445 1
            $this->columnCompareDimension($dimensionA),
446 1
            $this->columnCompareDimension($dimensionB)
447 1
        );
448
449 1
        return $this->where($filter, null, null, $boolean);
450
    }
451
452
    /**
453
     * Filter records where dimensionA is equal to dimensionB.
454
     * You can either supply a string or a Closure. The Closure will receive a DimensionBuilder object, which allows
455
     * you to select a dimension.
456
     *
457
     * Example:
458
     * ```php
459
     * $builder->orWhereColumn('initials', function(DimensionBuilder $dimensionBuilder) {
460
     *   $dimensionBuilder->select('first_name');
461
     * });
462
     * ```
463
     *
464
     * @param Closure|string $dimensionA The dimension which you want to compare, or a Closure which will receive a
465
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
466
     * @param Closure|string $dimensionB The dimension which you want to compare, or a Closure which will receive a
467
     *                                   DimensionBuilder which allows you to select a dimension in a more advance way.
468
     *
469
     * @return $this
470
     */
471 1
    public function orWhereColumn(Closure|string $dimensionA, Closure|string $dimensionB): self
472
    {
473 1
        return $this->whereColumn($dimensionA, $dimensionB, 'or');
474
    }
475
476
    /**
477
     * This filter will select records where the given dimension is greater than or equal to the given minValue, and
478
     * less than or equal to the given $maxValue.
479
     *
480
     * So in SQL syntax, this would be:
481
     * ```
482
     * WHERE dimension => $minValue AND dimension <= $maxValue
483
     * ```
484
     *
485
     * @param string           $dimension The dimension which you want to filter
486
     * @param int|float|string $minValue  The minimum value where the dimension should match. It should be equal or
487
     *                                    greater than this value.
488
     * @param int|float|string $maxValue  The maximum value where the dimension should match. It should be less than
489
     *                                    this value.
490
     * @param DataType|null    $valueType String specifying the type of bounds to match. The valueType determines how
491
     *                                    Druid interprets the matchValue to assist in converting to the type of the
492
     *                                    matched column and also defines the type of comparison used when matching
493
     *                                    values.
494
     * @param string           $boolean   This influences how this filter will be joined with previous added filters.
495
     *                                    Should both filters apply ("and") or one or the other ("or") ? Default is
496
     *                                    "and".
497
     *
498
     * @return $this
499
     */
500 1
    public function whereBetween(
501
        string $dimension,
502
        int|float|string $minValue,
503
        int|float|string $maxValue,
504
        ?DataType $valueType = null,
505
        string $boolean = 'and'
506
    ): self {
507 1
        $filter = new BetweenFilter($dimension, $minValue, $maxValue, $valueType);
508
509 1
        return $this->where($filter, null, null, $boolean);
510
    }
511
512
    /**
513
     * This filter will select records where the given dimension is greater than or equal to the given minValue, and
514
     * less than or equal to the given $maxValue.
515
     *
516
     * This method will join previous added filters with an "or" instead of an "and".
517
     *
518
     * So in SQL syntax, this would be:
519
     * ```
520
     * WHERE (dimension => $minValue AND dimension <= $maxValue) or .... (other filters here)
521
     * ```
522
     *
523
     * @param string           $dimension The dimension which you want to filter
524
     * @param int|float|string $minValue  The minimum value where the dimension should match. It should be equal or
525
     *                                    greater than this value.
526
     * @param int|float|string $maxValue  The maximum value where the dimension should match. It should be less than
527
     *                                    this value.
528
     * @param DataType|null    $valueType String specifying the type of bounds to match. The valueType determines how
529
     *                                    Druid interprets the matchValue to assist in converting to the type of the
530
     *                                    matched column and also defines the type of comparison used when matching
531
     *                                    values.
532
     *
533
     * @return $this
534
     */
535 1
    public function orWhereBetween(
536
        string $dimension,
537
        int|float|string $minValue,
538
        int|float|string $maxValue,
539
        ?DataType $valueType = null
540
    ): self {
541 1
        return $this->whereBetween($dimension, $minValue, $maxValue, $valueType, 'or');
542
    }
543
544
    /**
545
     * Filter on records which match using a bitwise AND comparison.
546
     *
547
     * Only records will match where the dimension contains ALL bits which are also enabled in the given $flags
548
     * argument. Support for 64-bit integers are supported.
549
     *
550
     * @param string $dimension     The dimension which contains int values where you want to do a bitwise AND check
551
     *                              against.
552
     * @param int    $flags         The bits which you want to check if they are enabled in the given dimension.
553
     *
554
     * @return $this
555
     */
556 1
    public function orWhereFlags(string $dimension, int $flags): self
557
    {
558 1
        return $this->whereFlags($dimension, $flags, 'or');
559
    }
560
561
    /**
562
     * Filter on records which match using a bitwise AND comparison.
563
     *
564
     * Only records will match where the dimension contains ALL bits which are also enabled in the given $flags
565
     * argument. Support for 64-bit integers are supported.
566
     *
567
     * @param string $dimension     The dimension which contains int values where you want to do a bitwise AND check
568
     *                              against.
569
     * @param int    $flags         The bits which you want to check if they are enabled in the given dimension.
570
     * @param string $boolean       This influences how this filter will be joined with previous added filters. Should
571
     *                              both filters apply ("and") or one or the other ("or") ? Default is "and".
572
     *
573
     * @return $this
574
     * @throws \BadFunctionCallException
575
     */
576 4
    public function whereFlags(
577
        string $dimension,
578
        int $flags,
579
        string $boolean = 'and'
580
    ): self {
581
        // If we do not have access to a query builder object, we cannot select our
582
        // flags value as a virtual column. This situation can happen for example when
583
        // we are in a task-builder. In that case, we will use the expression filter.
584
        // This has not our preference as it is slower.
585 4
        if (!$this->query instanceof QueryBuilder) {
586 2
            return $this->whereExpression('bitwiseAnd("' . $dimension . '", ' . $flags . ') == ' . $flags);
587
        }
588
589 2
        $placeholder                 = 'v' . count($this->query->placeholders);
590 2
        $this->query->placeholders[] = $placeholder;
591
592 2
        $this->query->virtualColumn(
593 2
            'bitwiseAnd("' . $dimension . '", ' . $flags . ')',
594 2
            $placeholder,
595 2
            DataType::LONG
596 2
        );
597
598 2
        return $this->where($placeholder, '=', $flags, $boolean);
599
    }
600
601
    /**
602
     * Filter on a dimension where the value exists in the given intervals array.
603
     *
604
     * The intervals array can contain the following:
605
     * - an Interval object
606
     * - a raw interval string as used in druid. For example: 2019-04-15T08:00:00.000Z/2019-04-15T09:00:00.000Z
607
     * - an array which contains 2 elements, a start and stop date. These can be an DateTime object, a unix timestamp
608
     *   or anything which can be parsed by DateTime::__construct
609
     *
610
     * @param string                                                               $dimension  The dimension which you
611
     *                                                                                         want to filter
612
     * @param array<string|IntervalInterface|array<string|\DateTimeInterface|int>> $intervals  The interval which you
613
     *                                                                                         want to match. See above
614
     *                                                                                         for more info.
615
     * @param string                                                               $boolean    This influences how this
616
     *                                                                                         filter will be joined
617
     *                                                                                         with previous added
618
     *                                                                                         filters. Should both
619
     *                                                                                         filters apply
620
     *                                                                                         ("and") or one or the
621
     *                                                                                         other ("or") ? Default
622
     *                                                                                         is
623
     *                                                                                         "and".
624
     *
625
     * @return $this
626
     * @throws \Exception
627
     */
628 1
    public function whereInterval(
629
        string $dimension,
630
        array $intervals,
631
        string $boolean = 'and'
632
    ): self {
633 1
        $filter = new IntervalFilter($dimension, $this->normalizeIntervals($intervals));
634
635 1
        return $this->where($filter, null, null, $boolean);
636
    }
637
638
    /**
639
     * Filter on a dimension where the value exists in the given intervals array.
640
     *
641
     * The intervals array can contain the following:
642
     * - an Interval object
643
     * - a raw interval string as used in druid. For example: 2019-04-15T08:00:00.000Z/2019-04-15T09:00:00.000Z
644
     * - an array which contains 2 elements, a start and stop date. These can be an DateTime object, a unix timestamp
645
     *   or anything which can be parsed by DateTime::__construct
646
     *
647
     * @param string                                                               $dimension  The dimension which you
648
     *                                                                                         want to filter
649
     * @param array<string|IntervalInterface|array<string|\DateTimeInterface|int>> $intervals  The interval which you
650
     *                                                                                         want to match. See above
651
     *                                                                                         for more info.
652
     *
653
     * @return $this
654
     * @throws \Exception
655
     */
656 1
    public function orWhereInterval(string $dimension, array $intervals): self
657
    {
658 1
        return $this->whereInterval($dimension, $intervals, 'or');
659
    }
660
661
    /**
662
     * Filter on a spatial dimension where the spatial dimension value (x,y coordinates) are between the
663
     * given min and max coordinates.
664
     *
665
     * @param string  $dimension The name of the spatial dimension.
666
     * @param float[] $minCoords List of minimum dimension coordinates for coordinates [x, y, z, …]
667
     * @param float[] $maxCoords List of maximum dimension coordinates for coordinates [x, y, z, …]
668
     * @param string  $boolean   This influences how this filter will be joined with previous added filters.
669
     *                           Should both filters apply ("and") or one or the other ("or") ? Default is
670
     *                           "and".
671
     *
672
     * @return $this
673
     */
674 2
    public function whereSpatialRectangular(
675
        string $dimension,
676
        array $minCoords,
677
        array $maxCoords,
678
        string $boolean = 'and'
679
    ): self {
680 2
        $filter = new SpatialRectangularFilter($dimension, $minCoords, $maxCoords);
681
682 2
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
683
684 2
        return $this;
685
    }
686
687
    /**
688
     * Select all records where the spatial dimension is within the given radios.
689
     * You can specify an x,y, z coordinate and the radius. All the records where the spatial dimension
690
     * is within the given area will be returned.
691
     *
692
     * @param string  $dimension The name of the spatial dimension.
693
     * @param float[] $coords    Origin coordinates in the form [x, y, z, …]
694
     * @param float   $radius    The float radius value
695
     * @param string  $boolean   This influences how this filter will be joined with previous added filters.
696
     *                           Should both filters apply ("and") or one or the other ("or") ? Default is
697
     *                           "and".
698
     *
699
     * @return $this
700
     */
701 2
    public function whereSpatialRadius(string $dimension, array $coords, float $radius, string $boolean = 'and'): self
702
    {
703 2
        $filter = new SpatialRadiusFilter($dimension, $coords, $radius);
704
705 2
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
706
707 2
        return $this;
708
    }
709
710
    /**
711
     * Return the records where the spatial dimension is within the area of the defined polygon.
712
     *
713
     * @param string  $dimension The name of the spatial dimension.
714
     * @param float[] $abscissa  (The x-axis) Horizontal coordinate for corners of the polygon
715
     * @param float[] $ordinate  (The y-axis) Vertical coordinate for corners of the polygon
716
     * @param string  $boolean   This influences how this filter will be joined with previous added filters.
717
     *                           Should both filters apply ("and") or one or the other ("or") ? Default is
718
     *                           "and".
719
     *
720
     * @return $this
721
     */
722 2
    public function whereSpatialPolygon(
723
        string $dimension,
724
        array $abscissa,
725
        array $ordinate,
726
        string $boolean = 'and'
727
    ): self {
728 2
        $filter = new SpatialPolygonFilter($dimension, $abscissa, $ordinate);
729
730 2
        strtolower($boolean) == 'and' ? $this->addAndFilter($filter) : $this->addOrFilter($filter);
731
732 2
        return $this;
733
    }
734
735
    /**
736
     * Filter on a spatial dimension where the spatial dimension value (x,y coordinates) are between the
737
     * given min and max coordinates.
738
     *
739
     * @param string  $dimension The name of the spatial dimension.
740
     * @param float[] $minCoords List of minimum dimension coordinates for coordinates [x, y, z, …]
741
     * @param float[] $maxCoords List of maximum dimension coordinates for coordinates [x, y, z, …]
742
     *
743
     * @return $this
744
     */
745 1
    public function orWhereSpatialRectangular(string $dimension, array $minCoords, array $maxCoords): self
746
    {
747 1
        return $this->whereSpatialRectangular($dimension, $minCoords, $maxCoords, 'or');
748
    }
749
750
    /**
751
     * Select all records where the spatial dimension is within the given radios.
752
     * You can specify an x,y, z coordinate and the radius. All the records where the spatial dimension
753
     * is within the given area will be returned.
754
     *
755
     * @param string  $dimension The name of the spatial dimension.
756
     * @param float[] $coords    Origin coordinates in the form [x, y, z, …]
757
     * @param float   $radius    The float radius value
758
     *
759
     * @return $this
760
     */
761 1
    public function orWhereSpatialRadius(string $dimension, array $coords, float $radius): self
762
    {
763 1
        return $this->whereSpatialRadius($dimension, $coords, $radius, 'or');
764
    }
765
766
    /**
767
     * Return the records where the spatial dimension is within the area of the defined polygon.
768
     *
769
     * @param string  $dimension The name of the spatial dimension.
770
     * @param float[] $abscissa  (The x-axis) Horizontal coordinate for corners of the polygon
771
     * @param float[] $ordinate  (The y-axis) Vertical coordinate for corners of the polygon
772
     *
773
     * @return $this
774
     */
775 1
    public function orWhereSpatialPolygon(string $dimension, array $abscissa, array $ordinate): self
776
    {
777 1
        return $this->whereSpatialPolygon($dimension, $abscissa, $ordinate, 'or');
778
    }
779
780
    /**
781
     * Normalize the given dimension to a DimensionInterface object.
782
     *
783
     * @param Closure|string $dimension
784
     *
785
     * @return \Level23\Druid\Dimensions\DimensionInterface
786
     * @throws InvalidArgumentException
787
     */
788 4
    protected function columnCompareDimension(Closure|string $dimension): DimensionInterface
789
    {
790 4
        if ($dimension instanceof Closure) {
791 2
            $builder = new DimensionBuilder();
792 2
            call_user_func($dimension, $builder);
793 2
            $dimensions = $builder->getDimensions();
794
795 2
            if (count($dimensions) != 1) {
796 1
                throw new InvalidArgumentException('Your dimension builder should select 1 dimension');
797
            }
798
799 1
            return $dimensions[0];
800
        }
801
802 2
        return new Dimension($dimension);
803
    }
804
805
    /**
806
     * Normalize the given intervals into Interval objects.
807
     *
808
     * @param array<int,string|IntervalInterface|array<string|\DateTimeInterface|int>|mixed> $intervals
809
     *
810
     * @return array<IntervalInterface>
811
     * @throws \Exception
812
     */
813 13
    protected function normalizeIntervals(array $intervals): array
814
    {
815 13
        if (sizeof($intervals) == 0) {
816 1
            return [];
817
        }
818
819 12
        $first = reset($intervals);
820
821
        // If first is an array or already a druid interval string or object we do not wrap it in an array
822 12
        if (!is_array($first) && !$this->isDruidInterval($first)) {
823 8
            $intervals = [$intervals];
824
        }
825
826 12
        return array_map(function ($interval) {
827
828 12
            if ($interval instanceof IntervalInterface) {
829 1
                return $interval;
830
            }
831
832
            // If it is a string we explode it into to elements
833 11
            if (is_string($interval)) {
834 2
                $interval = explode('/', $interval);
835
            }
836
837
            // If the value is an array and is not empty and has either one or 2 values it's an interval array
838 11
            if (is_array($interval) && !empty(array_filter($interval)) && count($interval) < 3) {
839
                /** @scrutinizer ignore-type */
840 6
                return new Interval(...$interval);
841
            }
842
843 5
            throw new InvalidArgumentException(
844 5
                'Invalid type given in the interval array. We cannot process ' .
845 5
                var_export($interval, true)
846 5
            );
847 12
        }, $intervals);
848
    }
849
850
    /**
851
     * Returns true if the argument provided is a druid interval string or interface
852
     *
853
     * @param mixed $interval
854
     *
855
     * @return bool
856
     */
857 11
    protected function isDruidInterval(mixed $interval): bool
858
    {
859 11
        if ($interval instanceof IntervalInterface) {
860 1
            return true;
861
        }
862
863 10
        return is_string($interval) && str_contains($interval, '/');
864
    }
865
866
    /**
867
     * Helper method to add an OR filter
868
     *
869
     * @param FilterInterface $filter
870
     */
871 6
    protected function addOrFilter(FilterInterface $filter): void
872
    {
873 6
        if (!$this->filter instanceof FilterInterface) {
874 5
            $this->filter = $filter;
875
876 5
            return;
877
        }
878
879 6
        if ($this->filter instanceof OrFilter) {
880 1
            $this->filter->addFilter($filter);
0 ignored issues
show
Bug introduced by
The method addFilter() does not exist on Level23\Druid\Filters\FilterInterface. It seems like you code against a sub-type of Level23\Druid\Filters\FilterInterface such as Level23\Druid\Filters\OrFilter or Level23\Druid\Filters\AndFilter. ( Ignorable by Annotation )

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

880
            $this->filter->/** @scrutinizer ignore-call */ 
881
                           addFilter($filter);
Loading history...
881
882 1
            return;
883
        }
884
885 6
        $this->filter = new OrFilter([$this->filter, $filter]);
886
    }
887
888
    /**
889
     * Helper method to add an AND filter
890
     *
891
     * @param FilterInterface $filter
892
     */
893 55
    protected function addAndFilter(FilterInterface $filter): void
894
    {
895 55
        if (!$this->filter instanceof FilterInterface) {
896 55
            $this->filter = $filter;
897
898 55
            return;
899
        }
900
901 29
        if ($this->filter instanceof AndFilter) {
902 2
            $this->filter->addFilter($filter);
903
904 2
            return;
905
        }
906
907 29
        $this->filter = new AndFilter([$this->filter, $filter]);
908
    }
909
910
    /**
911
     * @return \Level23\Druid\Filters\FilterInterface|null
912
     */
913 41
    public function getFilter(): ?FilterInterface
914
    {
915 41
        return $this->filter;
916
    }
917
}