LogicalFilter::__clone()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * LogicalFilter
4
 *
5
 * @package php-logical-filter
6
 * @author  Jean Claveau
7
 */
8
namespace JClaveau\LogicalFilter;
9
10
use JClaveau\LogicalFilter\Rule\AbstractRule;
11
use JClaveau\LogicalFilter\Rule\AbstractOperationRule;
12
use JClaveau\LogicalFilter\Rule\AndRule;
13
use JClaveau\LogicalFilter\Rule\OrRule;
14
use JClaveau\LogicalFilter\Rule\NotRule;
15
16
use JClaveau\LogicalFilter\Filterer\Filterer;
17
use JClaveau\LogicalFilter\Filterer\PhpFilterer;
18
use JClaveau\LogicalFilter\Filterer\CustomizableFilterer;
19
use JClaveau\LogicalFilter\Filterer\RuleFilterer;
20
21
/**
22
 * LogicalFilter describes a set of logical rules structured by
23
 * conjunctions and disjunctions (AND and OR).
24
 *
25
 * It's able to simplify them in order to find contractories branches
26
 * of the tree rule and check if there is at least one set rules having
27
 * possibilities.
28
 */
29
class LogicalFilter implements \JsonSerializable
30
{
31
    /** @var  AndRule $rules */
32
    protected $rules;
33
34
    /** @var  Filterer $default_filterer */
35
    protected $default_filterer;
36
37
    /** @var  array $options */
38
    protected $options = [];
39
40
    /** @var  array $default_options */
41
    protected static $default_options = [
42
        'in.normalization_threshold' => 0,
43
    ];
44
45
    /**
46
     * Creates a filter. You can provide a description of rules as in
47
     * addRules() as paramater.
48
     *
49
     * @param  array    $rules
50
     * @param  Filterer $default_filterer
0 ignored issues
show
Documentation introduced by
Should the type for parameter $default_filterer not be null|Filterer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
51
     *
52
     * @see self::addRules
53
     */
54 280
    public function __construct($rules=[], Filterer $default_filterer=null, array $options=[])
55
    {
56 280
        if ($rules instanceof AbstractRule) {
57 6
            $rules = $rules->copy();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $rules. This often makes code more readable.
Loading history...
58 6
        }
59 280
        elseif (! is_null($rules) && ! is_array($rules)) {
60
            throw new \InvalidArgumentException(
61
                "\$rules must be a rules description or an AbstractRule instead of"
62
                .var_export($rules, true)
63
            );
64
        }
65
66 280
        if ($default_filterer) {
67 37
            $this->default_filterer = $default_filterer;
68 37
        }
69
70 280
        if ($options) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
71 4
            $this->options = $options;
72 4
        }
73
74 280
        if ($rules) {
75 244
            $this->and_( $rules );
76 243
        }
77 279
    }
78
79
    /**
80
     */
81 30
    protected function getDefaultFilterer()
82
    {
83 30
        if (! $this->default_filterer) {
84 28
            $this->default_filterer = new PhpFilterer();
85 28
        }
86
87 30
        return $this->default_filterer;
88
    }
89
90
    /**
91
     */
92 1
    public static function setDefaultOptions(array $options)
93
    {
94 1
        foreach ($options as $name => $default_value) {
95 1
            self::$default_options[$name] = $default_value;
96 1
        }
97
98 1
        AbstractRule::flushStaticCache();
99 1
    }
100
101
    /**
102
     * @return array
103
     */
104 83
    public static function getDefaultOptions()
105
    {
106 83
        return self::$default_options;
107
    }
108
109
    /**
110
     * @return array
111
     */
112 276
    public function getOptions()
113
    {
114 276
        $options = self::$default_options;
115 276
        foreach ($this->options as $name => $value) {
116 4
            $options[$name] = $value;
117 276
        }
118
119 276
        return $options;
120
    }
121
122
    /**
123
     * This method parses different ways to define the rules of a LogicalFilter
124
     * and add them as a new And part of the filter.
125
     * + You can add N already instanciated Rules.
126
     * + You can provide 3 arguments: $field, $operator, $value
127
     * + You can provide a tree of rules:
128
     * ['or',
129
     *      ['and',
130
     *          ['field_5', 'above', 'a'],
131
     *          ['field_5', 'below', 'a'],
132
     *      ],
133
     *      ['field_6', 'equal', 'b'],
134
     *  ]
135
     *
136
     * @param  mixed The descriptions of the rules to add
137
     * @return $this
138
     *
139
     * @todo remove the _ for PHP 7
140
     */
141 273
    public function and_()
142
    {
143 273
        $this->rules = RuleDescriptionParser::updateRuleTreeFromDescription(
0 ignored issues
show
Documentation Bug introduced by
It seems like \JClaveau\LogicalFilter\...s, $this->getOptions()) can also be of type object<JClaveau\LogicalFilter\Rule\AbstractRule>. However, the property $rules is declared as type object<JClaveau\LogicalFilter\Rule\AndRule>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
144 273
            AndRule::operator,
145 273
            func_get_args(),
146 273
            $this->rules,
147 273
            $this->getOptions()
148 273
        );
149 269
        return $this;
150
    }
151
152
    /**
153
     * This method parses different ways to define the rules of a LogicalFilter
154
     * and add them as a new Or part of the filter.
155
     * + You can add N already instanciated Rules.
156
     * + You can provide 3 arguments: $field, $operator, $value
157
     * + You can provide a tree of rules:
158
     * ['or',
159
     *      ['and',
160
     *          ['field_5', 'above', 'a'],
161
     *          ['field_5', 'below', 'a'],
162
     *      ],
163
     *      ['field_6', 'equal', 'b'],
164
     *  ]
165
     *
166
     * @param  mixed The descriptions of the rules to add
167
     * @return $this
168
     *
169
     * @todo
170
     * @todo remove the _ for PHP 7
171
     */
172 24
    public function or_()
173
    {
174 24
        $this->rules = RuleDescriptionParser::updateRuleTreeFromDescription(
0 ignored issues
show
Documentation Bug introduced by
It seems like \JClaveau\LogicalFilter\...s, $this->getOptions()) can also be of type object<JClaveau\LogicalFilter\Rule\AbstractRule>. However, the property $rules is declared as type object<JClaveau\LogicalFilter\Rule\AndRule>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
175 24
            OrRule::operator,
176 24
            func_get_args(),
177 24
            $this->rules,
178 24
            $this->getOptions()
179 24
        );
180 24
        return $this;
181
    }
182
183
    /**
184
     * @deprecated
185
     */
186 1
    public function matches($rules_to_match)
187
    {
188 1
        return $this->hasSolutionIf($rules_to_match);
189
    }
190
191
    /**
192
     * Checks that a filter matches another one.
193
     *
194
     * @param array|AbstractRule $rules_to_match
195
     *
196
     * @return bool Whether or not this combination of filters has
197
     *              potential solutions
198
     */
199 1
    public function hasSolutionIf($rules_to_match)
200
    {
201 1
        return $this
202 1
            ->copy()
203 1
            ->and_($rules_to_match)
204 1
            ->hasSolution()
205 1
            ;
206
    }
207
208
    /**
209
     * Retrieve all the rules.
210
     *
211
     * @param  bool $copy By default copy the rule tree to avoid side effects.
212
     *
213
     * @return AbstractRule The tree of rules
214
     */
215 133
    public function getRules($copy = true)
216
    {
217 133
        return $copy && $this->rules ? $this->rules->copy() : $this->rules;
218
    }
219
220
    /**
221
     * Remove any constraint being a duplicate of another one.
222
     *
223
     * @param  array $options stop_after | stop_before |
224
     * @return $this
225
     */
226 142
    public function simplify($options=[])
227
    {
228 142
        if ($this->rules) {
229
            // AndRule added to make all Operation methods available
230 140
            $this->rules = (new AndRule([$this->rules]))
231 140
                ->simplify( $options )
232
                // ->dump(true, false)
233
                ;
234 140
        }
235
236 142
        return $this;
237
    }
238
239
240
    /**
241
     * Forces the two firsts levels of the tree to be an OrRule having
242
     * only AndRules as operands:
243
     * ['field', '=', '1'] <=> ['or', ['and', ['field', '=', '1']]]
244
     * As a simplified ruleTree will alwways be reduced to this structure
245
     * with no suboperands others than atomic ones or a simpler one like:
246
     * ['or', ['field', '=', '1'], ['field2', '>', '3']]
247
     *
248
     * This helpes to ease the result of simplify()
249
     *
250
     * @return OrRule
0 ignored issues
show
Documentation introduced by
Should the return type not be LogicalFilter?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
251
     */
252 74
    public function addMinimalCase()
253
    {
254 74
        if ($this->rules) {
255 74
            $this->rules = $this->rules->addMinimalCase();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->rules->addMinimalCase() of type object<JClaveau\LogicalFilter\Rule\OrRule> is incompatible with the declared type object<JClaveau\LogicalFilter\Rule\AndRule> of property $rules.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
256 74
        }
257
258 74
        return $this;
259
    }
260
261
    /**
262
     * Checks if there is at least on set of conditions which is not
263
     * contradictory.
264
     *
265
     * Checking if a filter has solutions require to simplify it first.
266
     * To let the control on the balance between readability and
267
     * performances, the required simplification can be saved or not
268
     * depending on the $save_simplification parameter.
269
     *
270
     * @param  $save_simplification
271
     *
272
     * @return bool
273
     */
274 82
    public function hasSolution($save_simplification=true)
275
    {
276 82
        if (! $this->rules) {
277 2
            return true;
278
        }
279
280 80
        if ($save_simplification) {
281 79
            $this->simplify();
282 79
            return $this->rules->hasSolution();
283
        }
284
285 2
        return $this->copy()->simplify()->rules->hasSolution();
286
    }
287
288
    /**
289
     * Returns an array describing the rule tree of the Filter.
290
     *
291
     * @param array $options
292
     *
293
     * @return array A description of the rules.
294
     */
295 188
    public function toArray(array $options=[])
296
    {
297 188
        return $this->rules ? $this->rules->toArray($options) : $this->rules;
298
    }
299
300
    /**
301
     * Returns an array describing the rule tree of the Filter.
302
     *
303
     * @param $debug Provides a source oriented dump.
304
     *
305
     * @return array A description of the rules.
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
306
     */
307 4
    public function toString(array $options=[])
308
    {
309 4
        return $this->rules ? $this->rules->toString($options) : $this->rules;
310
    }
311
312
    /**
313
     * Returns a unique id corresponding to the set of rules of the filter
314
     *
315
     * @return string The unique semantic id
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
316
     */
317 1
    public function getSemanticId()
318
    {
319 1
        return $this->rules ? $this->rules->getSemanticId() : null;
320
    }
321
322
    /**
323
     * For implementing JsonSerializable interface.
324
     *
325
     * @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php
326
     */
327 1
    public function jsonSerialize()
328
    {
329 1
        return $this->toArray();
330
    }
331
332
    /**
333
     * @return string
334
     */
335 4
    public function __toString()
336
    {
337 4
        return $this->toString();
338
    }
339
340
    /**
341
     * @see    https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke
342
     * @param  mixed $row
343
     * @return bool
344
     */
345 3
    public function __invoke($row, $key=null)
346
    {
347 3
        return $this->validates($row, $key);
348
    }
349
350
    /**
351
     * Removes all the defined rules.
352
     *
353
     * @return $this
354
     */
355 2
    public function flushRules()
356
    {
357 2
        $this->rules = null;
358 2
        return $this;
359
    }
360
361
    /**
362
     * @param  array|callable Associative array of renamings or callable
363
     *                        that would rename the fields.
364
     *
365
     * @return LogicalFilter  $this
366
     */
367 1
    public function renameFields($renamings)
368
    {
369 1
        if ($this->rules) {
370 1
            $this->rules->renameFields($renamings);
371 1
        }
372
373 1
        return $this;
374
    }
375
376
    /**
377
     * @param  array|callable Associative array of renamings or callable
378
     *                        that would rename the fields.
379
     *
380
     * @return string $this
0 ignored issues
show
Documentation introduced by
Should the return type not be LogicalFilter?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
381
     */
382 10
    public function removeRules($filter)
383
    {
384 10
        $cache_flush_required = false;
385
386 10
        $this->rules = (new RuleFilterer)->apply(
387 10
            new LogicalFilter($filter),
388 10
            $this->rules,
389
            [
390
                Filterer::on_row_matches => function($rule, $key, &$rows, $matching_case) use (&$cache_flush_required) {
0 ignored issues
show
Unused Code introduced by
The parameter $matching_case is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
391
                    // $rule->dump();
392 8
                    unset( $rows[$key] );
393 8
                    if (! $rows ) {
394 1
                        throw new \Exception(
395 1
                             "Removing the only rule $rule from the filter $this "
396
                            ."produces a case which has no possible solution due to missing "
397 1
                            ."implementation of TrueRule.\n"
398 1
                            ."Please see: https://github.com/jclaveau/php-logical-filter/issues/59"
399 1
                        );
400
                    }
401
402
                    // $matching_case->dump(true);
403 7
                    $cache_flush_required = true;
404 10
                },
405
                // Filterer::on_row_mismatches => function($rule, $key, &$rows, $matching_case) {
406
                    // $rule->dump();
407
                    // $matching_case && $matching_case->dump(true);
408
                // }
409
            ]
410 10
        );
411
412 7
        if ($cache_flush_required) {
413 7
            $this->rules->flushCache();
414 7
        }
415
416 7
        return $this;
417
    }
418
419
    /**
420
     * Apply a "RuleFilter" on the rules of the current instance.
421
     *
422
     * @param  array|LogicalFilter|AbstractRule $rules
423
     * @param  array|callable                   $options
424
     *
425
     * @return array The rules matching the filter
0 ignored issues
show
Documentation introduced by
Should the return type not be LogicalFilter?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
426
     */
427 21
    public function filterRules($rules=[], array $options=[])
428
    {
429 21
        if ($rules instanceof LogicalFilter) {
430
            $rules = $rules->getRules();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $rules. This often makes code more readable.
Loading history...
431
        }
432
433 21
        $filter = (new LogicalFilter($rules, new RuleFilterer))
0 ignored issues
show
Bug introduced by
It seems like $rules can also be of type object<JClaveau\LogicalFilter\Rule\AbstractRule>; however, JClaveau\LogicalFilter\L...alFilter::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
434
        // ->dump()
435 21
        ;
436
437 21
        $this->rules = (new RuleFilterer)->apply($filter, $this->rules, $options);
438
        // $this->rules->dump(true);
439
440
        // TODO replace it by a FalseRule
441 19
        if (false === $this->rules) {
442 1
            $this->rules = new AndRule;
443 1
        }
444
445 19
        return $this;
446
    }
447
448
    /**
449
     * @param  array|callable Associative array of renamings or callable
450
     *                        that would rename the fields.
451
     *
452
     * @return array The rules matching the filter
453
     * @return array $options debug | leaves_only | clean_empty_branches
0 ignored issues
show
Documentation introduced by
Should the return type not be LogicalFilter?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
454
     */
455 4
    public function keepLeafRulesMatching($filter=[], array $options=[])
456
    {
457 4
        $clean_empty_branches = ! isset($options['clean_empty_branches']) || $options['clean_empty_branches'];
458
459 4
        $filter = (new LogicalFilter($filter, new RuleFilterer))
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filter. This often makes code more readable.
Loading history...
460
        // ->dump()
461 4
        ;
462
463 4
        $options[ Filterer::leaves_only ] = true;
464
465 4
        $this->rules = (new RuleFilterer)->apply($filter, $this->rules, $options);
466
        // $this->rules->dump(true);
467
468
        // clean the remaining branches
469 4
        if ($clean_empty_branches) {
470 4
            $this->rules = (new RuleFilterer)->apply(
471 4
                new LogicalFilter(['and',
472 4
                    ['operator', 'in', ['or', 'and', 'not', '!in']],
473 4
                    ['children', '=', 0],
474 4
                ]),
475 4
                $this->rules,
476
                [
477
                    Filterer::on_row_matches => function($rule, $key, &$rows) {
478 2
                        unset($rows[$key]);
479 4
                    },
480
                    Filterer::on_row_mismatches => function($rule, $key, &$rows) {
0 ignored issues
show
Unused Code introduced by
The parameter $rule is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $rows is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
481 4
                    },
482
                ]
483 4
            );
484
485
            // TODO replace it by a FalseRule
486 4
            if (false === $this->rules) {
487 1
                $this->rules = new AndRule;
488 1
            }
489 4
        }
490
491 4
        return $this;
492
    }
493
494
    /**
495
     * @param  array|callable Associative array of renamings or callable
496
     *                        that would rename the fields.
497
     *
498
     * @return array The rules matching the filter
499
     *
500
     *
501
     * @todo Merge with rules
502
     */
503 3
    public function listLeafRulesMatching($filter=[])
504
    {
505 3
        $filter = (new LogicalFilter($filter, new RuleFilterer))
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filter. This often makes code more readable.
Loading history...
506
        // ->dump()
507 3
        ;
508
509 3
        if (! $this->rules) {
510 1
            return [];
511
        }
512
513 2
        $out = [];
514 2
        (new RuleFilterer)->apply(
515 2
            $filter,
516 2
            $this->rules,
517
            [
518
                Filterer::on_row_matches => function(
519
                    AbstractRule $matching_rule,
520
                    $key,
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
521
                    array $siblings
0 ignored issues
show
Unused Code introduced by
The parameter $siblings is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
522
                ) use (&$out) {
523
                    if (   ! $matching_rule instanceof AndRule
524 2
                        && ! $matching_rule instanceof OrRule
525 2
                        && ! $matching_rule instanceof NotRule
526 2
                    ) {
527 2
                        $out[] = $matching_rule;
528 2
                    }
529 2
                },
530 2
                Filterer::leaves_only => true,
531
            ]
532 2
        );
533
534 2
        return $out;
535
    }
536
537
    /**
538
     * $filter->onEachRule(
539
     *      ['field', 'in', [...]],
540
     *      function ($rule, $key, array &$rules) {
541
     *          // ...
542
     * })
543
     *
544
     * $filter->onEachRule(
545
     *      ['field', 'in', [...]],
546
     *      [
547
     *          Filterer::on_row_matches => function ($rule, $key, array &$rules) {
548
     *              // ...
549
     *          },
550
     *          Filterer::on_row_mismatches => function ($rule, $key, array &$rules) {
551
     *              // ...
552
     *          },
553
     *      ]
554
     * )
555
     *
556
     * @todo Make it available on AbstractRule also
557
     *
558
     * @param  array|LogicalFilter
559
     * @param  array|callable Associative array of renamings or callable
560
     *                        that would rename the fields.
561
     *
562
     * @return array          The rules matching the filter
0 ignored issues
show
Documentation introduced by
Should the return type not be array|LogicalFilter?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
563
     */
564 93
    public function onEachRule($filter=[], $options)
565
    {
566 7
        $filter = (new LogicalFilter($filter, new RuleFilterer))
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filter. This often makes code more readable.
Loading history...
567
        // ->dump()
568 7
        ;
569
570 7
        if (! $this->rules) {
571 93
            return [];
572
        }
573
574 7
        if (is_callable($options)) {
575
            $options = [
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $options. This often makes code more readable.
Loading history...
576 7
                Filterer::on_row_matches => $options,
577 7
            ];
578 7
        }
579
580 7
        (new RuleFilterer)->apply(
581 7
            $filter,
582 7
            $this->rules,
583
            $options
584 7
        );
585
586 7
        return $this;
587
    }
588
589
    /**
590
     * $filter->onEachCase(function (AndRule $case, $key, array &$caseRules) {
591
     *      // do whatever you want on the current case...
592
     * })
593
     *
594
     * @param  array|callable $action Callback to apply on each case.
595
     * @return LogicalFilter  $this
596
     *
597
     * @todo Make it available on AbstractRule also
598
     */
599 6
    public function onEachCase(callable $action)
600
    {
601 6
        $this->simplify()->addMinimalCase();
602
603 6
        if (! $this->rules) {
604
            return $this;
605
        }
606
607 6
        $operands = $this->rules->getOperands();
608
609 6
        foreach ($operands as $i => &$and_case) {
610
            $arguments = [
611 6
                &$and_case,
612 6
            ];
613 6
            call_user_func_array($action, $arguments);
614 6
        }
615
616 6
        $this->rules = new OrRule($operands);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \JClaveau\LogicalFilter\Rule\OrRule($operands) of type object<JClaveau\LogicalFilter\Rule\OrRule> is incompatible with the declared type object<JClaveau\LogicalFilter\Rule\AndRule> of property $rules.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
617
618 6
        return $this;
619
    }
620
621
    /**
622
     * Retrieves the minimum possibility and the maximum possibility for
623
     * each field of the rules matching the filter.
624
     *
625
     * @param  array|LogicalFilter|AbstractRule $ruleFilter
0 ignored issues
show
Documentation introduced by
Should the type for parameter $ruleFilter not be array|LogicalFilter|AbstractRule|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
626
     *
627
     * @return array The bounds of the range and a nullable property for each field
628
     */
629 5
    public function getRanges($ruleFilter=null)
630
    {
631 5
        $ranges = [];
632
633
        $this->onEachCase(function (AndRule $and_rule) use (&$ranges, $ruleFilter) {
634 5
            (new self($and_rule))->onEachRule(
0 ignored issues
show
Documentation introduced by
$and_rule is of type object<JClaveau\LogicalFilter\Rule\AndRule>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
635 5
                ['and',
636 5
                    $ruleFilter,
637 5
                    ['operator', 'in', [
638 5
                        '=', '>', '<', '>=', '<=',
639 5
                        '><', '><=', '=><=', '=><',
640 5
                    ]],
641 5
                ],
642 4
                function ($rule) use (&$ranges) {
643
644 4
                    $field = $rule->getField();
645
646 4
                    $range = isset($ranges[ $field ])
647 4
                           ? $ranges[ $field ]
648 4
                           : ['min' => [], 'max' => [], 'nullable' => false];
649
650 4
                    if ($rule::operator == '=') {
651 3
                        if (null === $rule->getValues()) {
652 1
                            $range['nullable'] = true;
653 1
                        }
654
                        else {
655 2
                            $range['min'][] = $rule->getValues();
656 2
                            $range['max'][] = $rule->getValues();
657
                        }
658 3
                    }
659 4 View Code Duplication
                    elseif (in_array($rule::operator, ['<', '<='])) {
660 4
                        $range['max'][] = $rule->getValues();
661 4
                    }
662 4 View Code Duplication
                    elseif (in_array($rule::operator, ['>', '>='])) {
663 4
                        $range['min'][] = $rule->getValues();
664 4
                    }
665
                    elseif (in_array($rule::operator, ['><', '><=', '=><=', '=><'])) {
666
                        $range['min'][] = $rule->getValues()[0];
667
                        $range['max'][] = $rule->getValues()[1];
668
                    }
669
                    else {
670
                        throw new \LogicException(
671
                            "Buggy case: ".$rule::operator
672
                        );
673
                    }
674
675 4
                    $ranges[ $field ] = $range;
676 4
                }
677 5
            );
678 5
        });
679
680 5
        foreach ($ranges as &$range) {
681 4
            $range['min'] = min($range['min']);
682 4
            $range['max'] = max($range['max']);
683 5
        }
684
685 5
        return $ranges;
686
    }
687
688
    /**
689
     * Retrieves the minimum possibility and the maximum possibility for
690
     * the given field.
691
     *
692
     * @param  mixed $field
693
     * @return array The bounds of the range and a nullable property for the given field
694
     */
695 5
    public function getFieldRange($field)
696
    {
697 5
        $range = $this->getRanges(['field', '=', $field]);
698 5
        return isset($range[$field])
699 5
            ? $range[$field]
700 5
            : ['min' => null, 'max' => null, 'nullable' => false];
701
    }
702
703
    /**
704
     * Clone the current object and its rules.
705
     *
706
     * @return LogicalFilter A copy of the current instance with a copied ruletree
707
     */
708 7
    public function copy()
709
    {
710 7
        return clone $this;
711
    }
712
713
    /**
714
     * Make a deep copy of the rules
715
     */
716 7
    public function __clone()
717
    {
718 7
        if ($this->rules) {
719 7
            $this->rules = $this->rules->copy();
720 7
        }
721 7
    }
722
723
    /**
724
     * Copy the current instance into the variable given as parameter
725
     * and returns the copy.
726
     *
727
     * @return LogicalFilter
728
     */
729 3
    public function saveAs( &$variable)
730
    {
731 3
        return $variable = $this;
732
    }
733
734
    /**
735
     * Copy the current instance into the variable given as parameter
736
     * and returns the copied instance.
737
     *
738
     * @return LogicalFilter
739
     */
740 1
    public function saveCopyAs( &$copied_variable)
741
    {
742 1
        $copied_variable = $this->copy();
743 1
        return $this;
744
    }
745
746
    /**
747
     * @param bool  $exit=false
0 ignored issues
show
Bug introduced by
There is no parameter named $exit=false. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
748
     * @param array $options    + callstack_depth=2 The level of the caller to dump
749
     *                          + mode='string' in 'export' | 'dump' | 'string'
750
     *
751
     * @return $this
0 ignored issues
show
Documentation introduced by
Should the return type not be LogicalFilter|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
752
     */
753 4
    public function dump($exit=false, array $options=[])
754
    {
755
        $default_options = [
756 4
            'callstack_depth' => 3,
757 4
            'mode'            => 'string',
758 4
        ];
759 4
        foreach ($default_options as $default_option => &$default_value) {
760 4
            if (! isset($options[ $default_option ])) {
761 4
                $options[ $default_option ] = $default_value;
762 4
            }
763 4
        }
764 4
        extract($options);
765
766 4
        if ($this->rules) {
767 4
            $this->rules->dump($exit, $options);
768 4
        }
769
        else {
770
            // TODO dump a TrueRule
771
            $bt     = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $callstack_depth);
772
            $caller = $bt[ $callstack_depth - 2 ];
773
774
            // get line and file from the previous level of the caller
775
            // TODO go deeper if this case exist?
776 View Code Duplication
            if (! isset($caller['file'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
777
                $caller['file'] = $bt[ $callstack_depth - 3 ]['file'];
778
            }
779
780 View Code Duplication
            if (! isset($caller['line'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
781
                $caller['line'] = $bt[ $callstack_depth - 3 ]['line'];
782
            }
783
784
            try {
785
                echo "\n" . $caller['file'] . ':' . $caller['line'] . "\n";
786
                var_export($this->toArray($options));
787
            }
788
            catch (\Exception $e) {
789
                echo "\nError while dumping: " . $e->getMessage() . "\n";
790
                var_export($caller);
791
                echo "\n\n";
792
                var_export($bt);
793
                echo "\n\n";
794
                var_export($this->toArray($options));
795
            }
796
            echo "\n\n";
797
798
            if ($exit) {
799
                exit;
800
            }
801
        }
802
803 4
        return $this;
804
    }
805
806
    /**
807
     * Applies the current instance to a set of data.
808
     *
809
     * @param  mixed                  $data_to_filter
810
     * @param  Filterer|callable|null $filterer
811
     *
812
     * @return mixed The filtered data
813
     */
814 26
    public function applyOn($data_to_filter, $action_on_matches=null, $filterer=null)
0 ignored issues
show
Unused Code introduced by
The parameter $action_on_matches is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
815
    {
816 26 View Code Duplication
        if (! $filterer) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
817 26
            $filterer = $this->getDefaultFilterer();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filterer. This often makes code more readable.
Loading history...
818 26
        }
819
        elseif (is_callable($filterer)) {
820
            $filterer = new CustomizableFilterer($filterer);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filterer. This often makes code more readable.
Loading history...
821
        }
822
        elseif (! $filterer instanceof Filterer) {
823
            throw new \InvalidArgumentException(
824
                 "The given \$filterer must be null or a callable or a instance "
825
                ."of Filterer instead of: ".var_export($filterer, true)
826
            );
827
        }
828
829 26
        if ($data_to_filter instanceof LogicalFilter) {
830 2
            $filtered_rules = $filterer->apply($this, $data_to_filter->getRules());
0 ignored issues
show
Documentation introduced by
$data_to_filter->getRules() is of type object<JClaveau\LogicalF...\AbstractOperationRule>, but the function expects a object<JClaveau\LogicalFilter\Filterer\Iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
831 2
            return $data_to_filter->flushRules()->and_($filtered_rules);
832
        }
833
        else {
834 24
            return $filterer->apply($this, $data_to_filter);
835
        }
836
    }
837
838
    /**
839
     * Applies the current instance to a value (and its index optionnally).
840
     *
841
     * @param  mixed                  $value_to_check
842
     * @param  scalar                 $index
0 ignored issues
show
Bug introduced by
There is no parameter named $index. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
843
     * @param  Filterer|callable|null $filterer
844
     *
845
     * @return AbstractRule|false|true + False if the filter doesn't validates
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
846
     *                                 + Null if the target has no sens (operation filtered by field for example)
847
     *                                 + A rule tree containing the first matching case if there is one.
848
     */
849 4
    public function validates($value_to_check, $key_to_check=null, $filterer=null)
850
    {
851 4 View Code Duplication
        if (! $filterer) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
852 4
            $filterer = $this->getDefaultFilterer();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filterer. This often makes code more readable.
Loading history...
853 4
        }
854
        elseif (is_callable($filterer)) {
855
            $filterer = new CustomizableFilterer($filterer);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $filterer. This often makes code more readable.
Loading history...
856
        }
857
        elseif (! $filterer instanceof Filterer) {
858
            throw new \InvalidArgumentException(
859
                 "The given \$filterer must be null or a callable or a instance "
860
                ."of Filterer instead of: ".var_export($filterer, true)
861
            );
862
        }
863
864 4
        return $filterer->hasMatchingCase($this, $value_to_check, $key_to_check);
865
    }
866
867
    /**/
868
}
869