Passed
Pull Request — master (#51)
by
unknown
15:04
created

HasFilter   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 925
Duplicated Lines 0 %

Test Coverage

Coverage 94.67%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 85
eloc 180
c 5
b 0
f 0
dl 0
loc 925
ccs 213
cts 225
cp 0.9467
rs 2

32 Methods

Rating   Name   Duplication   Size   Complexity  
D where() 0 101 28
A orWhereExpression() 0 3 1
A orWhereFlags() 0 3 1
A addOrFilter() 0 15 3
A orWhereColumn() 0 3 1
A whereSpatialPolygon() 0 11 2
A columnCompareDimension() 0 15 3
A whereColumn() 0 8 1
A orWhere() 0 7 1
A orWhereBetween() 0 8 1
A whereNot() 0 16 2
A orWhereSpatialPolygon() 0 3 1
A orWhereNotColumn() 0 5 1
A orWhereInterval() 0 3 1
A whereExpression() 0 7 2
A whereBetween() 0 11 1
A whereInterval() 0 13 1
A whereIsNull() 0 11 2
A orWhereSpatialRectangular() 0 3 1
A orWhereNot() 0 3 1
A whereIsNotNull() 0 11 2
A addAndFilter() 0 15 3
A whereFlags() 0 46 3
A orWhereSpatialRadius() 0 3 1
A getExtraction() 0 10 2
B normalizeIntervals() 0 36 9
A whereSpatialRadius() 0 7 2
A getFilter() 0 3 1
A orWhereIn() 0 3 1
A isDruidInterval() 0 7 3
A whereIn() 0 5 1
A whereSpatialRectangular() 0 11 2

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

912
            $this->filter->/** @scrutinizer ignore-call */ 
913
                           addFilter($filter);
Loading history...
913
914 1
            return;
915
        }
916
917 7
        $this->filter = new OrFilter([$this->filter, $filter]);
918
    }
919
920
    /**
921
     * Helper method to add an AND filter
922
     *
923
     * @param FilterInterface $filter
924
     */
925 51
    protected function addAndFilter(FilterInterface $filter): void
926
    {
927 51
        if (!$this->filter instanceof FilterInterface) {
928 51
            $this->filter = $filter;
929
930 51
            return;
931
        }
932
933 25
        if ($this->filter instanceof AndFilter) {
934 2
            $this->filter->addFilter($filter);
935
936 2
            return;
937
        }
938
939 25
        $this->filter = new AndFilter([$this->filter, $filter]);
940
    }
941
942
    /**
943
     * @return \Level23\Druid\Filters\FilterInterface|null
944
     */
945 37
    public function getFilter(): ?FilterInterface
946
    {
947 37
        return $this->filter;
948
    }
949
950
    /**
951
     * @param \Closure|null $extraction
952
     *
953
     * @return \Level23\Druid\Extractions\ExtractionInterface|null
954
     */
955 49
    private function getExtraction(?Closure $extraction): ?ExtractionInterface
956
    {
957 49
        if (empty($extraction)) {
958 48
            return null;
959
        }
960
961 1
        $builder = new ExtractionBuilder();
962 1
        call_user_func($extraction, $builder);
963
964 1
        return $builder->getExtraction();
965
    }
966
}
967