Passed
Branch master (2a417c)
by Jean
05:00
created

LogicalFilter::keepLeafRulesMatching()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 6
nop 2
dl 0
loc 39
ccs 22
cts 22
cp 1
crap 4
rs 9.296
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
51
     *
52
     * @see self::addRules
53
     */
54 145
    public function __construct($rules=[], Filterer $default_filterer=null, array $options=[])
55
    {
56 145
        if ($rules instanceof AbstractRule) {
57 1
            $rules = $rules->copy();
58 1
        }
59 145
        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 145
        if ($default_filterer) {
67 11
            $this->default_filterer = $default_filterer;
68 11
        }
69
70 145
        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 145
        if ($rules) {
75 125
            $this->and_( $rules );
76 124
        }
77 144
    }
78
79
    /**
80
     */
81 9
    protected function getDefaultFilterer()
82
    {
83 9
        if ( ! $this->default_filterer) {
84 7
            $this->default_filterer = new PhpFilterer();
85 7
        }
86
87 9
        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 51
    public static function getDefaultOptions()
105
    {
106 51
        return self::$default_options;
107
    }
108
109
    /**
110
     * @return array
111
     */
112 135
    public function getOptions()
113
    {
114 135
        $options = self::$default_options;
115 135
        foreach ($this->options as $name => $value) {
116 4
            $options[$name] = $value;
117 135
        }
118
119 135
        return $options;
120
    }
121
122
    /**
123
     * This method parses different ways to define the rules of a LogicalFilter.
124
     * + You can add N already instanciated Rules.
125
     * + You can provide 3 arguments: $field, $operator, $value
126
     * + You can provide a tree of rules:
127
     * [
128
     *      'or',
129
     *      [
130
     *          'and',
131
     *          ['field_5', 'above', 'a'],
132
     *          ['field_5', 'below', 'a'],
133
     *      ],
134
     *      ['field_6', 'equal', 'b'],
135
     *  ]
136
     *
137
     * @param  string        $operation         and | or
138
     * @param  array         $rules_description Rules description
139
     * @return LogicalFilter $this
140
     */
141 141
    protected function addRules( $operation, array $rules_description )
142
    {
143 141
        if ($rules_description == [null]) {
144
            // TODO this is due to the bad design of using "Null" instead of
145
            // TrueRule when a Filter "has no rule". So it's the equivalent of
146
            // "and true" or "or true".
147
            // Remove it while fixing https://github.com/jclaveau/php-logical-filter/issues/59
148 3
            if (AndRule::operator == $operation) {
149
                // A && True <=> A
150 2
                return $this;
151
            }
152 2
            elseif (OrRule::operator == $operation) {
153
                // A || True <=> True
154 2
                $this->rules = null;
155 2
                return $this;
156
            }
157
            else {
158
                throw new InvalidArgumentException(
159
                    "Unhandled operation '$operation'"
160
                );
161
            }
162
        }
163
164 141
        if (   3 == count($rules_description)
165 141
            && is_string($rules_description[0])
166 141
            && is_string($rules_description[1])
167 141
        ) {
168
            // Atomic rules
169 6
            $new_rule = AbstractRule::generateSimpleRule(
170 6
                $rules_description[0], // field
171 6
                $rules_description[1], // operator
172 6
                $rules_description[2], // value
173 6
                $this->getOptions()
174 6
            );
175
176 6
            $this->addRule($new_rule, $operation);
177 5
        }
178
        elseif (count($rules_description) == count(array_filter($rules_description, function($arg) {
179 136
            return $arg instanceof LogicalFilter;
180 136
        })) ) {
181
            // Already instanciated rules
182 2
            foreach ($rules_description as $i => $filter) {
183 2
                $rules = $filter->getRules();
184 2
                if (null !== $rules) {
185 1
                    $this->addRule( $rules, $operation);
186 1
                }
187 2
            }
188 2
        }
189
        elseif (count($rules_description) == count(array_filter($rules_description, function($arg) {
190 135
            return $arg instanceof AbstractRule;
191 135
        })) ) {
192
            // Already instanciated rules
193 3
            foreach ($rules_description as $i => $new_rule) {
194 3
                $this->addRule( $new_rule, $operation);
195 3
            }
196 3
        }
197 134
        elseif (1 == count($rules_description) && is_array($rules_description[0])) {
198
            if (count($rules_description[0]) == count(array_filter($rules_description[0], function($arg) {
199 134
                return $arg instanceof AbstractRule;
200 134
            })) ) {
201
                // Case of $filter->or_([AbstractRule, AbstractRule, AbstractRule, ...])
202 2
                foreach ($rules_description[0] as $i => $new_rule) {
203 2
                    $this->addRule( $new_rule, $operation );
204 2
                }
205 2
            }
206
            else {
207 132
                $fake_root = new AndRule;
208
209 132
                $this->addCompositeRule_recursion(
210 132
                    $rules_description[0],
211
                    $fake_root
212 132
                );
213
214 129
                $this->addRule( $fake_root->getOperands()[0], $operation );
215
            }
216 131
        }
217
        else {
218 1
            throw new \InvalidArgumentException(
219
                "Bad set of arguments provided for rules addition: "
220 1
                .var_export($rules_description, true)
221 1
            );
222
        }
223
224 138
        return $this;
225
    }
226
227
    /**
228
     * Add one rule object to the filter
229
     *
230
     * @param AbstractRule $rule
231
     * @param string       $operation
232
     *
233
     * @return $this
234
     */
235 137
    protected function addRule( AbstractRule $rule, $operation=AndRule::operator )
236
    {
237 137
        if ( $this->rules && in_array( get_class($this->rules), [AndRule::class, OrRule::class] )
238 137
            && ! $this->rules->getOperands() ) {
239 1
            throw new \LogicException(
240
                 "You are trying to add rules to a LogicalFilter which had "
241
                ."only contradictory rules that have already been simplified: "
242 1
                .$this->rules
243 1
            );
244
        }
245
246 137
        if (null === $this->rules) {
247 137
            $this->rules = $rule;
0 ignored issues
show
Documentation Bug introduced by
$rule is of type object<JClaveau\LogicalFilter\Rule\AbstractRule>, but the property $rules was declared to be of type object<JClaveau\LogicalFilter\Rule\AndRule>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
248 137
        }
249 18
        elseif (($tmp_rules = $this->rules) // $this->rules::operator not supported in PHP 5.6
250 18
            && ($tmp_rules::operator != $operation)
251 18
        ) {
252 17
            if (AndRule::operator == $operation) {
253 12
                $this->rules = new AndRule([$this->rules, $rule]);
254 12
            }
255 6
            elseif (OrRule::operator == $operation) {
256 5
                $this->rules = new OrRule([$this->rules, $rule]);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \JClaveau\LogicalFil...y($this->rules, $rule)) 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...
257 5
            }
258
            else {
259 1
                throw new \InvalidArgumentException(
260 1
                    "\$operation must be '".AndRule::operator."' or '".OrRule::operator
261 1
                    ."' instead of: ".var_export($operation, true)
262 1
                );
263
            }
264 16
        }
265
        else {
266 7
            $this->rules->addOperand($rule);
267
        }
268
269 137
        return $this;
270
    }
271
272
    /**
273
     * Recursion auxiliary of addCompositeRule.
274
     *
275
     * @param array                 $rules_composition  The description of the
276
     *                                                  rules to add.
277
     * @param AbstractOperationRule $recursion_position The position in the
278
     *                                                  tree where rules must
279
     *                                                  be added.
280
     *
281
     * @return $this
282
     */
283 132
    protected function addCompositeRule_recursion(
284
        array $rules_composition,
285
        AbstractOperationRule $recursion_position
286
    ) {
287
        if ( ! array_filter($rules_composition, function ($rule_composition_part) {
288 132
            return is_string($rule_composition_part);
289 132
        })) {
290
            // at least one operator is required for operation rules
291 1
            throw new \InvalidArgumentException(
292
                "Please provide an operator for the operation: \n"
293 1
                .var_export($rules_composition, true)
294 1
            );
295
        }
296 131
        elseif ( 3 == count($rules_composition)
297 131
            && ! in_array( AndRule::operator, $rules_composition, true )
298 131
            && ! in_array( OrRule::operator,  $rules_composition, true )
299 131
            && ! in_array( NotRule::operator, $rules_composition, true )
300 131
            && ! in_array( AbstractRule::findSymbolicOperator( AndRule::operator ), $rules_composition, true )
301 131
            && ! in_array( AbstractRule::findSymbolicOperator( OrRule::operator ),  $rules_composition, true )
302 131
            && ! in_array( AbstractRule::findSymbolicOperator( NotRule::operator ), $rules_composition, true )
303 131
        ) {
304
            // atomic or composit rules
305 129
            $operand_left  = $rules_composition[0];
306 129
            $operation     = $rules_composition[1];
307 129
            $operand_right = $rules_composition[2];
308
309 129
            $rule = AbstractRule::generateSimpleRule(
310 129
                $operand_left, $operation, $operand_right, $this->getOptions()
311 129
            );
312 129
            $recursion_position->addOperand( $rule );
313 129
        }
314
        else {
315
            // operations
316 98
            if (   NotRule::operator == $rules_composition[0]
317 98
                || $rules_composition[0] == AbstractRule::findSymbolicOperator( NotRule::operator ) ) {
318 17
                $rule = new NotRule();
319 17
            }
320 93 View Code Duplication
            elseif (in_array( AndRule::operator, $rules_composition )
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...
321 93
                || in_array( AbstractRule::findSymbolicOperator( AndRule::operator ), $rules_composition )) {
322 82
                $rule = new AndRule();
323 82
            }
324 58 View Code Duplication
            elseif (in_array( OrRule::operator, $rules_composition )
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...
325 58
                || in_array( AbstractRule::findSymbolicOperator( OrRule::operator ), $rules_composition ) ) {
326 57
                $rule = new OrRule();
327 57
            }
328
            else {
329 1
                throw new \InvalidArgumentException(
330
                    "A rule description seems to be an operation but do "
331 1
                    ."not contains a valid operator: ".var_export($rules_composition, true)
332 1
                );
333
            }
334
335 97
            $operator = $rule::operator;
336
337 97
            $operands_descriptions = array_filter(
338 97
                $rules_composition,
339
                function ($operand) use ($operator) {
340 97
                    return ! in_array($operand, [$operator, AbstractRule::findSymbolicOperator( $operator )]);
341
                }
342 97
            );
343
344 97
            $non_true_rule_descriptions = array_filter(
345 97
                $operands_descriptions,
346
                function($operand) {
347 96
                    return null !== $operand  // no rule <=> true
348 96
                        || true !== $operand
349 96
                        ;
350
                }
351 97
            );
352
353 97
            foreach ($operands_descriptions as $i => $operands_description) {
354 96
                if (false === $operands_description) {
355 1
                    $operands_descriptions[ $i ] = ['and']; // FalseRule hack
356 1
                }
357 96
                elseif (null === $operands_description || true === $operands_description) {
358 1
                    $operands_description = ['and'];
359 1
                    if ( ! $non_true_rule_descriptions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $non_true_rule_descriptions 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...
360
                        throw new \LogicException(
361
                            "TrueRules are not implemented. Please add "
362
                            ."them to operations having other type of rules"
363
                        );
364
                    }
365
366 1
                    unset($operands_descriptions[ $i ]);
367 1
                }
368 97
            }
369
370 97
            $remaining_operations = array_filter(
371 97
                $operands_descriptions,
372
                function($operand) {
373 96
                    return ! is_array($operand)
374 96
                        && ! $operand instanceof AbstractRule
375 96
                        && ! $operand instanceof LogicalFilter
376 96
                        ;
377
                }
378 97
            );
379
380 97
            if ($remaining_operations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $remaining_operations 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...
381 1
                throw new \InvalidArgumentException(
382
                    "Mixing different operations in the same rule level not implemented: \n["
383 1
                    . implode(', ', $remaining_operations)."]\n"
384 1
                    . 'in ' . var_export($rules_composition, true)
385 1
                );
386
            }
387
388 97
            if (NotRule::operator == $operator && 1 != count($operands_descriptions)) {
389 1
                throw new \InvalidArgumentException(
390
                    "Negations can have only one operand: \n"
391 1
                    .var_export($rules_composition, true)
392 1
                );
393
            }
394
395 97
            foreach ($operands_descriptions as $operands_description) {
396 96
                if ($operands_description instanceof AbstractRule) {
397 1
                    $rule->addOperand($operands_description);
398 1
                }
399 96
                elseif ($operands_description instanceof LogicalFilter) {
400 2
                    $rule->addOperand($operands_description->getRules());
401 2
                }
402
                else {
403 95
                    $this->addCompositeRule_recursion(
404 95
                        $operands_description,
405
                        $rule
406 95
                    );
407
                }
408 97
            }
409
410 96
            $recursion_position->addOperand( $rule );
411
        }
412
413 130
        return $this;
414
    }
415
416
    /**
417
     * This method parses different ways to define the rules of a LogicalFilter
418
     * and add them as a new And part of the filter.
419
     * + You can add N already instanciated Rules.
420
     * + You can provide 3 arguments: $field, $operator, $value
421
     * + You can provide a tree of rules:
422
     * [
423
     *      'or',
424
     *      [
425
     *          'and',
426
     *          ['field_5', 'above', 'a'],
427
     *          ['field_5', 'below', 'a'],
428
     *      ],
429
     *      ['field_6', 'equal', 'b'],
430
     *  ]
431
     *
432
     * @param  mixed The descriptions of the rules to add
433
     * @return $this
434
     *
435
     * @todo remove the _ for PHP 7
436
     */
437 138
    public function and_()
438
    {
439 138
        $this->addRules( AndRule::operator, func_get_args());
440 135
        return $this;
441
    }
442
443
    /**
444
     * This method parses different ways to define the rules of a LogicalFilter
445
     * and add them as a new Or part of the filter.
446
     * + You can add N already instanciated Rules.
447
     * + You can provide 3 arguments: $field, $operator, $value
448
     * + You can provide a tree of rules:
449
     * [
450
     *      'or',
451
     *      [
452
     *          'and',
453
     *          ['field_5', 'above', 'a'],
454
     *          ['field_5', 'below', 'a'],
455
     *      ],
456
     *      ['field_6', 'equal', 'b'],
457
     *  ]
458
     *
459
     * @param  mixed The descriptions of the rules to add
460
     * @return $this
461
     *
462
     * @todo
463
     * @todo remove the _ for PHP 7
464
     */
465 7
    public function or_()
466
    {
467 7
        $this->addRules( OrRule::operator, func_get_args());
468 7
        return $this;
469
    }
470
471
    /**
472
     * @deprecated
473
     */
474 1
    public function matches($rules_to_match)
475
    {
476 1
        return $this->hasSolutionIf($rules_to_match);
477
    }
478
479
    /**
480
     * Checks that a filter matches another one.
481
     *
482
     * @param array|AbstractRule $rules_to_match
483
     *
484
     * @return bool Whether or not this combination of filters has
485
     *              potential solutions
486
     */
487 1
    public function hasSolutionIf($rules_to_match)
488
    {
489 1
        return $this
490 1
            ->copy()
491 1
            ->and_($rules_to_match)
492 1
            ->hasSolution()
493 1
            ;
494
    }
495
496
    /**
497
     * Retrieve all the rules.
498
     *
499
     * @param  bool $copy By default copy the rule tree to avoid side effects.
500
     *
501
     * @return AbstractRule The tree of rules
502
     */
503 56
    public function getRules($copy = true)
504
    {
505 56
        return $copy && $this->rules ? $this->rules->copy() : $this->rules;
506
    }
507
508
    /**
509
     * Remove any constraint being a duplicate of another one.
510
     *
511
     * @param  array $options stop_after | stop_before |
512
     * @return $this
513
     */
514 93
    public function simplify($options=[])
515
    {
516 93
        if ($this->rules) {
517
            // AndRule added to make all Operation methods available
518 90
            $this->rules = (new AndRule([$this->rules]))
519 90
                ->simplify( $options )
520
                // ->dump(true, false)
521
                ;
522 90
        }
523
524 93
        return $this;
525
    }
526
527
    /**
528
     * Checks if there is at least on set of conditions which is not
529
     * contradictory.
530
     *
531
     * Checking if a filter has solutions require to simplify it first.
532
     * To let the control on the balance between readability and
533
     * performances, the required simplification can be saved or not
534
     * depending on the $save_simplification parameter.
535
     *
536
     * @param  $save_simplification
537
     *
538
     * @return bool
539
     */
540 8
    public function hasSolution($save_simplification=true)
541
    {
542 8
        if ( ! $this->rules) {
543 1
            return true;
544
        }
545
546 7
        if ($save_simplification) {
547 6
            $this->simplify();
548 6
            return $this->rules->hasSolution();
549
        }
550
551 2
        return $this->copy()->simplify()->rules->hasSolution();
552
    }
553
554
    /**
555
     * Returns an array describing the rule tree of the Filter.
556
     *
557
     * @param array $options
558
     *
559
     * @return array A description of the rules.
560
     */
561 95
    public function toArray(array $options=[])
562
    {
563 95
        return $this->rules ? $this->rules->toArray($options) : $this->rules;
564 48
    }
565
566
    /**
567
     * Returns an array describing the rule tree of the Filter.
568
     *
569
     * @param $debug Provides a source oriented dump.
570
     *
571
     * @return array A description of the rules.
572
     */
573 4
    public function toString(array $options=[])
574
    {
575 4
        return $this->rules ? $this->rules->toString($options) : $this->rules;
576
    }
577
578
    /**
579
     * Returns a unique id corresponding to the set of rules of the filter
580
     *
581
     * @return string The unique semantic id
582
     */
583 1
    public function getSemanticId()
584
    {
585 1
        return $this->rules ? $this->rules->getSemanticId() : null;
586
    }
587
588
    /**
589
     * For implementing JsonSerializable interface.
590
     *
591
     * @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php
592
     */
593 1
    public function jsonSerialize()
594
    {
595 1
        return $this->toArray();
596
    }
597
598
    /**
599
     * @return string
600
     */
601 4
    public function __toString()
602
    {
603 4
        return $this->toString();
604
    }
605
606
    /**
607
     * @see    https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke
608
     * @param  mixed $row
609
     * @return bool
610
     */
611 3
    public function __invoke($row, $key=null)
612
    {
613 3
        return $this->validates($row, $key);
614
    }
615
616
    /**
617
     * Removes all the defined rules.
618
     *
619
     * @return $this
620
     */
621 2
    public function flushRules()
622
    {
623 2
        $this->rules = null;
624 2
        return $this;
625
    }
626
627
    /**
628
     * @param  array|callable Associative array of renamings or callable
629
     *                        that would rename the fields.
630
     *
631
     * @return string $this
632
     */
633 1
    public function renameFields($renamings)
634
    {
635 1
        if (method_exists($this->rules, 'renameField')) {
636
            $this->rules->renameField($renamings);
0 ignored issues
show
Bug introduced by
The method renameField() does not exist on JClaveau\LogicalFilter\Rule\AndRule. Did you maybe mean renameFields()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
637
        }
638 1
        elseif ($this->rules) {
639 1
            $this->rules->renameFields($renamings);
640 1
        }
641
642 1
        return $this;
643
    }
644
645
    /**
646
     * @param  array|callable Associative array of renamings or callable
647
     *                        that would rename the fields.
648
     *
649
     * @return string $this
650
     */
651 10
    public function removeRules($filter)
652
    {
653 10
        $cache_flush_required = false;
654
655 10
        $this->rules = (new RuleFilterer)->apply(
656 10
            new LogicalFilter($filter),
657 10
            $this->rules,
658
            [
659
                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...
660
                    // $rule->dump();
661 8
                    unset( $rows[$key] );
662 8
                    if ( ! $rows ) {
663 1
                        throw new \Exception(
664 1
                             "Removing the only rule $rule from the filter $this "
665
                            ."produces a case which has no possible solution due to missing "
666 1
                            ."implementation of TrueRule.\n"
667 1
                            ."Please see: https://github.com/jclaveau/php-logical-filter/issues/59"
668 1
                        );
669
                    }
670
671
                    // $matching_case->dump(true);
672 7
                    $cache_flush_required = true;
673 10
                },
674
                // Filterer::on_row_mismatches => function($rule, $key, &$rows, $matching_case) {
675
                    // $rule->dump();
676
                    // $matching_case && $matching_case->dump(true);
677
                // }
678
            ]
679 10
        );
680
681 7
        if ($cache_flush_required) {
682 7
            $this->rules->flushCache();
683 7
        }
684
685 7
        return $this;
686
    }
687
688
    /**
689
     * @param  array|callable Associative array of renamings or callable
690
     *                        that would rename the fields.
691
     *
692
     * @return array The rules matching the filter
693
     * @return array $options debug | leaves_only | clean_empty_branches
694
     *
695
     *
696
     * @todo Merge with rules
697
     */
698 4
    public function keepLeafRulesMatching($filter=[], array $options=[])
699
    {
700 4
        $clean_empty_branches = ! isset($options['clean_empty_branches']) || $options['clean_empty_branches'];
701
702 4
        $filter = (new LogicalFilter($filter, new RuleFilterer))
703
        // ->dump()
704 4
        ;
705
706 4
        $options[ Filterer::leaves_only ] = true;
707
708 4
        $this->rules = (new RuleFilterer)->apply($filter, $this->rules, $options);
709
        // $this->rules->dump(true);
710
711
712
        // clean the remaining branches
713 4
        if ($clean_empty_branches) {
714 4
            $this->rules = (new RuleFilterer)->apply(
715 4
                new LogicalFilter(['and',
716 4
                    ['operator', 'in', ['or', 'and', 'not', '!in']],
717 4
                    ['children', '=', 0],
718 4
                ]),
719 4
                $this->rules,
720
                [
721
                    Filterer::on_row_matches => function($rule, $key, &$rows) {
722 2
                        unset( $rows[$key] );
723 4
                    },
724
                    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...
725 4
                    },
726
                ]
727 4
            );
728
729
            // TODO replace it by a FalseRule
730 4
            if (false === $this->rules) {
731 1
                $this->rules = new AndRule;
732 1
            }
733 4
        }
734
735 4
        return $this;
736
    }
737
738
    /**
739
     * @param  array|callable Associative array of renamings or callable
740
     *                        that would rename the fields.
741
     *
742
     * @return array The rules matching the filter
743
     *
744
     *
745
     * @todo Merge with rules
746
     */
747 3
    public function listLeafRulesMatching($filter=[])
748
    {
749 3
        $filter = (new LogicalFilter($filter, new RuleFilterer))
750
        // ->dump()
751 3
        ;
752
753 3
        if ( ! $this->rules) {
754 1
            return [];
755
        }
756
757 2
        $out = [];
758 2
        (new RuleFilterer)->apply(
759 2
            $filter,
760 2
            $this->rules,
761
            [
762 2
                Filterer::on_row_matches => function(
763
                    AbstractRule $matching_rule,
764
                    $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...
765
                    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...
766
                ) use (&$out) {
767
                    if (   ! $matching_rule instanceof AndRule
768 2
                        && ! $matching_rule instanceof OrRule
769 2
                        && ! $matching_rule instanceof NotRule
770 2
                    ) {
771 2
                        $out[] = $matching_rule;
772 2
                    }
773 2
                },
774 2
                Filterer::leaves_only => true,
775
            ]
776 2
        );
777
778 2
        return $out;
779
    }
780
781
    /**
782
     * @param  array|callable Associative array of renamings or callable
783
     *                        that would rename the fields.
784
     *
785
     * @return array The rules matching the filter
786
     *
787
     *
788
     * @todo Make it available on AbstractRule also
789
     */
790 2
    public function onEachRule($filter=[], $options)
791
    {
792 2
        $filter = (new LogicalFilter($filter, new RuleFilterer))
793
        // ->dump()
794 2
        ;
795
796 2
        if ( ! $this->rules) {
797
            return [];
798
        }
799
800 2
        if (is_callable($options)) {
801
            $options = [
802 2
                Filterer::on_row_matches => $options,
803 2
            ];
804 2
        }
805
806 2
        (new RuleFilterer)->apply(
807 2
            $filter,
808 2
            $this->rules,
809
            $options
810 2
        );
811
812 2
        return $this;
813
    }
814
815
    /**
816
     */
817 1
    public function onEachCase(callable $action)
818
    {
819 1
        $this->simplify(['force_logical_core' => true]);
820
821 1
        if ( ! $this->rules) {
822
            return $this;
823
        }
824
825 1
        $operands = $this->rules->getOperands();
826
827 1
        foreach ($operands as $i => &$and_case) {
828
            $arguments = [
829 1
                &$and_case,
830 1
            ];
831 1
            call_user_func_array($action, $arguments);
832 1
        }
833
834
        // Debug::dumpJson($operands, true);
835 1
        $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...
836
837 1
        return $this;
838
    }
839
840
    /**
841
     * Clone the current object and its rules.
842
     *
843
     * @return LogicalFilter A copy of the current instance with a copied ruletree
844
     */
845 7
    public function copy()
846
    {
847 7
        return clone $this;
848
    }
849
850
    /**
851
     * Make a deep copy of the rules
852
     */
853 7
    public function __clone()
854
    {
855 7
        if ($this->rules) {
856 7
            $this->rules = $this->rules->copy();
857 7
        }
858 7
    }
859
860
    /**
861
     * Copy the current instance into the variable given as parameter
862
     * and returns the copy.
863
     *
864
     * @return LogicalFilter
865
     */
866 3
    public function saveAs( &$variable )
867
    {
868 3
        return $variable = $this;
869
    }
870
871
    /**
872
     * Copy the current instance into the variable given as parameter
873
     * and returns the copied instance.
874
     *
875
     * @return LogicalFilter
876
     */
877 1
    public function saveCopyAs( &$copied_variable )
878
    {
879 1
        $copied_variable = $this->copy();
880 1
        return $this;
881
    }
882
883
    /**
884
     * @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...
885
     * @param array $options    + callstack_depth=2 The level of the caller to dump
886
     *                          + mode='string' in 'export' | 'dump' | 'string'
887
     *
888
     * @return $this
889
     */
890 4
    public function dump($exit=false, array $options=[])
891
    {
892
        $default_options = [
893 4
            'callstack_depth' => 3,
894 4
            'mode'            => 'string',
895 4
        ];
896 4
        foreach ($default_options as $default_option => &$default_value) {
897 4
            if ( ! isset($options[ $default_option ])) {
898 4
                $options[ $default_option ] = $default_value;
899 4
            }
900 4
        }
901 4
        extract($options);
902
903 4
        if ($this->rules) {
904 4
            $this->rules->dump($exit, $options);
905 4
        }
906
        else {
907
            // TODO dump a TrueRule
908
            $bt     = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $callstack_depth);
909
            $caller = $bt[ $callstack_depth - 2 ];
910
911
            // get line and file from the previous level of the caller
912
            // TODO go deeper if this case exist?
913 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...
914
                $caller['file'] = $bt[ $callstack_depth - 3 ]['file'];
915
            }
916 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...
917
                $caller['line'] = $bt[ $callstack_depth - 3 ]['line'];
918
            }
919
920
            try {
921
                echo "\n" . $caller['file'] . ':' . $caller['line'] . "\n";
922
                var_export($this->toArray($options));
923
            }
924
            catch (\Exception $e) {
925
                echo "\nError while dumping: " . $e->getMessage() . "\n";
926
                var_export($caller);
927
                echo "\n\n";
928
                var_export($bt);
929
                echo "\n\n";
930
                var_export($this->toArray($options));
931
            }
932
            echo "\n\n";
933
934
            if ($exit) {
935
                exit;
936
            }
937
        }
938
939 4
        return $this;
940
    }
941
942
    /**
943
     * Applies the current instance to a set of data.
944
     *
945
     * @param  mixed                  $data_to_filter
946
     * @param  Filterer|callable|null $filterer
947
     *
948
     * @return mixed The filtered data
949
     */
950 5
    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...
951
    {
952 5 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...
953 5
            $filterer = $this->getDefaultFilterer();
954 5
        }
955
        elseif (is_callable($filterer)) {
956
            $filterer = new CustomizableFilterer($filterer);
957
        }
958
        elseif ( ! $filterer instanceof Filterer) {
959
            throw new \InvalidArgumentException(
960
                 "The given \$filterer must be null or a callable or a instance "
961
                ."of Filterer instead of: ".var_export($filterer, true)
962
            );
963
        }
964
965 5
        if ($data_to_filter instanceof LogicalFilter) {
966 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...
967 2
            return $data_to_filter->flushRules()->addRule( $filtered_rules );
968
        }
969
        else {
970 3
            return $filterer->apply( $this, $data_to_filter );
971
        }
972
    }
973
974
    /**
975
     * Applies the current instance to a value (and its index optionnally).
976
     *
977
     * @param  mixed                  $value_to_check
978
     * @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...
979
     * @param  Filterer|callable|null $filterer
980
     *
981
     * @return AbstractRule|false|true + False if the filter doesn't validates
982
     *                                 + Null if the target has no sens (operation filtered by field for example)
983
     *                                 + A rule tree containing the first matching case if there is one.
984
     */
985 4
    public function validates($value_to_check, $key_to_check=null, $filterer=null)
986
    {
987 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...
988 4
            $filterer = $this->getDefaultFilterer();
989 4
        }
990
        elseif (is_callable($filterer)) {
991
            $filterer = new CustomizableFilterer($filterer);
992
        }
993
        elseif ( ! $filterer instanceof Filterer) {
994
            throw new \InvalidArgumentException(
995
                 "The given \$filterer must be null or a callable or a instance "
996
                ."of Filterer instead of: ".var_export($filterer, true)
997
            );
998
        }
999
1000 4
        return $filterer->hasMatchingCase( $this, $value_to_check, $key_to_check );
1001
    }
1002
1003
    /**/
1004
}
1005