Completed
Push — master ( d05d36...1c1a74 )
by Jean
03:54
created

LogicalFilter::addMinimalCase()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

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 8
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 148
    public function __construct($rules=[], Filterer $default_filterer=null, array $options=[])
55
    {
56 148
        if ($rules instanceof AbstractRule) {
57 1
            $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 1
        }
59 148
        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 148
        if ($default_filterer) {
67 11
            $this->default_filterer = $default_filterer;
68 11
        }
69
70 148
        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 148
        if ($rules) {
75 128
            $this->and_( $rules );
76 127
        }
77 147
    }
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 55
    public static function getDefaultOptions()
105
    {
106 55
        return self::$default_options;
107
    }
108
109
    /**
110
     * @return array
111
     */
112 138
    public function getOptions()
113
    {
114 138
        $options = self::$default_options;
115 138
        foreach ($this->options as $name => $value) {
116 4
            $options[$name] = $value;
117 138
        }
118
119 138
        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 144
    protected function addRules($operation, array $rules_description)
142
    {
143 144
        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 144
        if (   3 == count($rules_description)
165 144
            && is_string($rules_description[0])
166 144
            && is_string($rules_description[1])
167 144
        ) {
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 139
            return $arg instanceof LogicalFilter;
180 139
        })) ) {
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 138
            return $arg instanceof AbstractRule;
191 138
        })) ) {
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 137
        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 137
                return $arg instanceof AbstractRule;
200 137
            })) ) {
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 135
                $fake_root = new AndRule;
208
209 135
                $this->addCompositeRule_recursion(
210 135
                    $rules_description[0],
211
                    $fake_root
212 135
                );
213
214 132
                $this->addRule($fake_root->getOperands()[0], $operation);
215
            }
216 134
        }
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 141
        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 140
    protected function addRule( AbstractRule $rule, $operation=AndRule::operator )
236
    {
237 140
        if ($this->rules && in_array( get_class($this->rules), [AndRule::class, OrRule::class])
238 140
            && ! $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 140
        if (null === $this->rules) {
247 140
            $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 140
        }
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 140
        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 135
    protected function addCompositeRule_recursion(
284
        array $rules_composition,
285
        AbstractOperationRule $recursion_position
286
    ) {
287
        if ( ! array_filter($rules_composition, function ($rule_composition_part) {
288 135
            return is_string($rule_composition_part);
289 135
        })) {
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 134
        elseif ( 3 == count($rules_composition)
297 134
            && ! in_array( AndRule::operator, $rules_composition, true )
298 134
            && ! in_array( OrRule::operator, $rules_composition, true )
299 134
            && ! in_array( NotRule::operator, $rules_composition, true )
300 134
            && ! in_array( AbstractRule::findSymbolicOperator( AndRule::operator ), $rules_composition, true )
301 134
            && ! in_array( AbstractRule::findSymbolicOperator( OrRule::operator ), $rules_composition, true )
302 134
            && ! in_array( AbstractRule::findSymbolicOperator( NotRule::operator ), $rules_composition, true )
303 134
        ) {
304
            // atomic or composit rules
305 132
            $operand_left  = $rules_composition[0];
306 132
            $operation     = $rules_composition[1];
307 132
            $operand_right = $rules_composition[2];
308
309 132
            $rule = AbstractRule::generateSimpleRule(
310 132
                $operand_left, $operation, $operand_right, $this->getOptions()
311 132
            );
312 132
            $recursion_position->addOperand( $rule );
313 132
        }
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 (empty($non_true_rule_descriptions)) {
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 ( ! empty($remaining_operations)) {
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 133
        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 141
    public function and_()
438
    {
439 141
        $this->addRules( AndRule::operator, func_get_args());
440 138
        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 59
    public function getRules($copy = true)
504
    {
505 59
        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 92
    public function simplify($options=[])
515
    {
516 92
        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 92
        return $this;
525
    }
526
527
528
    /**
529
     * Forces the two firsts levels of the tree to be an OrRule having
530
     * only AndRules as operands:
531
     * ['field', '=', '1'] <=> ['or', ['and', ['field', '=', '1']]]
532
     * As a simplified ruleTree will alwways be reduced to this structure
533
     * with no suboperands others than atomic ones or a simpler one like:
534
     * ['or', ['field', '=', '1'], ['field2', '>', '3']]
535
     *
536
     * This helpes to ease the result of simplify()
537
     *
538
     * @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...
539
     */
540 29
    public function addMinimalCase()
541
    {
542 29
        if ($this->rules) {
543 29
            $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...
544 29
        }
545
546 29
        return $this;
547
    }
548
549
    /**
550
     * Checks if there is at least on set of conditions which is not
551
     * contradictory.
552
     *
553
     * Checking if a filter has solutions require to simplify it first.
554
     * To let the control on the balance between readability and
555
     * performances, the required simplification can be saved or not
556
     * depending on the $save_simplification parameter.
557
     *
558
     * @param  $save_simplification
559
     *
560
     * @return bool
561
     */
562 37
    public function hasSolution($save_simplification=true)
563
    {
564 37
        if (! $this->rules) {
565 2
            return true;
566
        }
567
568 35
        if ($save_simplification) {
569 34
            $this->simplify();
570 34
            return $this->rules->hasSolution();
571
        }
572
573 2
        return $this->copy()->simplify()->rules->hasSolution();
574
    }
575
576
    /**
577
     * Returns an array describing the rule tree of the Filter.
578
     *
579
     * @param array $options
580
     *
581
     * @return array A description of the rules.
582
     */
583 95
    public function toArray(array $options=[])
584
    {
585 95
        return $this->rules ? $this->rules->toArray($options) : $this->rules;
586
    }
587
588
    /**
589
     * Returns an array describing the rule tree of the Filter.
590
     *
591
     * @param $debug Provides a source oriented dump.
592
     *
593
     * @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...
594
     */
595 4
    public function toString(array $options=[])
596
    {
597 4
        return $this->rules ? $this->rules->toString($options) : $this->rules;
598
    }
599
600
    /**
601
     * Returns a unique id corresponding to the set of rules of the filter
602
     *
603
     * @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...
604
     */
605 1
    public function getSemanticId()
606
    {
607 1
        return $this->rules ? $this->rules->getSemanticId() : null;
608
    }
609
610
    /**
611
     * For implementing JsonSerializable interface.
612
     *
613
     * @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php
614
     */
615 1
    public function jsonSerialize()
616
    {
617 1
        return $this->toArray();
618
    }
619
620
    /**
621
     * @return string
622
     */
623 4
    public function __toString()
624
    {
625 4
        return $this->toString();
626
    }
627
628
    /**
629
     * @see    https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke
630
     * @param  mixed $row
631
     * @return bool
632
     */
633 3
    public function __invoke($row, $key=null)
634
    {
635 3
        return $this->validates($row, $key);
636
    }
637
638
    /**
639
     * Removes all the defined rules.
640
     *
641
     * @return $this
642
     */
643 2
    public function flushRules()
644
    {
645 2
        $this->rules = null;
646 2
        return $this;
647
    }
648
649
    /**
650
     * @param  array|callable Associative array of renamings or callable
651
     *                        that would rename the fields.
652
     *
653
     * @return LogicalFilter  $this
654
     */
655 1
    public function renameFields($renamings)
656
    {
657 1
        if (method_exists($this->rules, 'renameField')) {
658
            $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...
659
        }
660 1
        elseif ($this->rules) {
661 1
            $this->rules->renameFields($renamings);
662 1
        }
663
664 1
        return $this;
665
    }
666
667
    /**
668
     * @param  array|callable Associative array of renamings or callable
669
     *                        that would rename the fields.
670
     *
671
     * @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...
672
     */
673 10
    public function removeRules($filter)
674
    {
675 10
        $cache_flush_required = false;
676
677 10
        $this->rules = (new RuleFilterer)->apply(
678 10
            new LogicalFilter($filter),
679 10
            $this->rules,
680
            [
681
                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...
682
                    // $rule->dump();
683 8
                    unset( $rows[$key] );
684 8
                    if ( ! $rows ) {
685 1
                        throw new \Exception(
686 1
                             "Removing the only rule $rule from the filter $this "
687
                            ."produces a case which has no possible solution due to missing "
688 1
                            ."implementation of TrueRule.\n"
689 1
                            ."Please see: https://github.com/jclaveau/php-logical-filter/issues/59"
690 1
                        );
691
                    }
692
693
                    // $matching_case->dump(true);
694 7
                    $cache_flush_required = true;
695 10
                },
696
                // Filterer::on_row_mismatches => function($rule, $key, &$rows, $matching_case) {
697
                    // $rule->dump();
698
                    // $matching_case && $matching_case->dump(true);
699
                // }
700
            ]
701 10
        );
702
703 7
        if ($cache_flush_required) {
704 7
            $this->rules->flushCache();
705 7
        }
706
707 7
        return $this;
708
    }
709
710
    /**
711
     * @param  array|callable Associative array of renamings or callable
712
     *                        that would rename the fields.
713
     *
714
     * @return array The rules matching the filter
715
     * @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...
716
     *
717
     *
718
     * @todo Merge with rules
719
     */
720 4
    public function keepLeafRulesMatching($filter=[], array $options=[])
721
    {
722 4
        $clean_empty_branches = ! isset($options['clean_empty_branches']) || $options['clean_empty_branches'];
723
724 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...
725
        // ->dump()
726 4
        ;
727
728 4
        $options[ Filterer::leaves_only ] = true;
729
730 4
        $this->rules = (new RuleFilterer)->apply($filter, $this->rules, $options);
731
        // $this->rules->dump(true);
732
733
        // clean the remaining branches
734 4
        if ($clean_empty_branches) {
735 4
            $this->rules = (new RuleFilterer)->apply(
736 4
                new LogicalFilter(['and',
737 4
                    ['operator', 'in', ['or', 'and', 'not', '!in']],
738 4
                    ['children', '=', 0],
739 4
                ]),
740 4
                $this->rules,
741
                [
742
                    Filterer::on_row_matches => function($rule, $key, &$rows) {
743 2
                        unset($rows[$key]);
744 4
                    },
745
                    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...
746 4
                    },
747
                ]
748 4
            );
749
750
            // TODO replace it by a FalseRule
751 4
            if (false === $this->rules) {
752 1
                $this->rules = new AndRule;
753 1
            }
754 4
        }
755
756 4
        return $this;
757
    }
758
759
    /**
760
     * @param  array|callable Associative array of renamings or callable
761
     *                        that would rename the fields.
762
     *
763
     * @return array The rules matching the filter
764
     *
765
     *
766
     * @todo Merge with rules
767
     */
768 3
    public function listLeafRulesMatching($filter=[])
769
    {
770 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...
771
        // ->dump()
772 3
        ;
773
774 3
        if (! $this->rules) {
775 1
            return [];
776
        }
777
778 2
        $out = [];
779 2
        (new RuleFilterer)->apply(
780 2
            $filter,
781 2
            $this->rules,
782
            [
783 2
                Filterer::on_row_matches => function(
784
                    AbstractRule $matching_rule,
785
                    $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...
786
                    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...
787
                ) use (&$out) {
788
                    if (   ! $matching_rule instanceof AndRule
789 2
                        && ! $matching_rule instanceof OrRule
790 2
                        && ! $matching_rule instanceof NotRule
791 2
                    ) {
792 2
                        $out[] = $matching_rule;
793 2
                    }
794 2
                },
795 2
                Filterer::leaves_only => true,
796
            ]
797 2
        );
798
799 2
        return $out;
800
    }
801
802
    /**
803
     * $filter->onEachRule(
804
     *      ['field', 'in', [...]],
805
     *      function ($rule, $key, array &$rules) {
806
     *          // ...
807
     * })
808
     *
809
     * $filter->onEachRule(
810
     *      ['field', 'in', [...]],
811
     *      [
812
     *          Filterer::on_row_matches => function ($rule, $key, array &$rules) {
813
     *              // ...
814
     *          },
815
     *          Filterer::on_row_mismatches => function ($rule, $key, array &$rules) {
816
     *              // ...
817
     *          },
818
     *      ]
819
     * )
820
     *
821
     * @todo Make it available on AbstractRule also
822
     *
823
     * @param  array|LogicalFilter
824
     * @param  array|callable Associative array of renamings or callable
825
     *                        that would rename the fields.
826
     *
827
     * @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...
828
     */
829 2
    public function onEachRule($filter=[], $options)
830
    {
831 2
        $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...
832
        // ->dump()
833 2
        ;
834
835 2
        if (! $this->rules) {
836
            return [];
837
        }
838
839 2
        if (is_callable($options)) {
840
            $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...
841 2
                Filterer::on_row_matches => $options,
842 2
            ];
843 2
        }
844
845 2
        (new RuleFilterer)->apply(
846 2
            $filter,
847 2
            $this->rules,
848
            $options
849 2
        );
850
851 2
        return $this;
852
    }
853
854
    /**
855
     * $filter->onEachCase(function (AndRule $case, $key, array &$caseRules) {
856
     *      // do whatever you want on the current case...
857
     * })
858
     *
859
     * @param  array|callable $action Callback to apply on each case.
860
     * @return LogicalFilter  $this
861
     *
862
     * @todo Make it available on AbstractRule also
863
     */
864 1
    public function onEachCase(callable $action)
865
    {
866 1
        $this->simplify()->addMinimalCase();
867
868 1
        if (! $this->rules) {
869
            return $this;
870
        }
871
872 1
        $operands = $this->rules->getOperands();
873
874 1
        foreach ($operands as $i => &$and_case) {
875
            $arguments = [
876 1
                &$and_case,
877 1
            ];
878 1
            call_user_func_array($action, $arguments);
879 1
        }
880
881 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...
882
883 1
        return $this;
884
    }
885
886
    /**
887
     * Clone the current object and its rules.
888
     *
889
     * @return LogicalFilter A copy of the current instance with a copied ruletree
890
     */
891 7
    public function copy()
892
    {
893 7
        return clone $this;
894
    }
895
896
    /**
897
     * Make a deep copy of the rules
898
     */
899 7
    public function __clone()
900
    {
901 7
        if ($this->rules) {
902 7
            $this->rules = $this->rules->copy();
903 7
        }
904 7
    }
905
906
    /**
907
     * Copy the current instance into the variable given as parameter
908
     * and returns the copy.
909
     *
910
     * @return LogicalFilter
911
     */
912 3
    public function saveAs( &$variable)
913
    {
914 3
        return $variable = $this;
915
    }
916
917
    /**
918
     * Copy the current instance into the variable given as parameter
919
     * and returns the copied instance.
920
     *
921
     * @return LogicalFilter
922
     */
923 1
    public function saveCopyAs( &$copied_variable)
924
    {
925 1
        $copied_variable = $this->copy();
926 1
        return $this;
927
    }
928
929
    /**
930
     * @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...
931
     * @param array $options    + callstack_depth=2 The level of the caller to dump
932
     *                          + mode='string' in 'export' | 'dump' | 'string'
933
     *
934
     * @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...
935
     */
936 4
    public function dump($exit=false, array $options=[])
937
    {
938
        $default_options = [
939 4
            'callstack_depth' => 3,
940 4
            'mode'            => 'string',
941 4
        ];
942 4
        foreach ($default_options as $default_option => &$default_value) {
943 4
            if ( ! isset($options[ $default_option ])) {
944 4
                $options[ $default_option ] = $default_value;
945 4
            }
946 4
        }
947 4
        extract($options);
948
949 4
        if ($this->rules) {
950 4
            $this->rules->dump($exit, $options);
951 4
        }
952
        else {
953
            // TODO dump a TrueRule
954
            $bt     = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $callstack_depth);
955
            $caller = $bt[ $callstack_depth - 2 ];
956
957
            // get line and file from the previous level of the caller
958
            // TODO go deeper if this case exist?
959 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...
960
                $caller['file'] = $bt[ $callstack_depth - 3 ]['file'];
961
            }
962
963 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...
964
                $caller['line'] = $bt[ $callstack_depth - 3 ]['line'];
965
            }
966
967
            try {
968
                echo "\n" . $caller['file'] . ':' . $caller['line'] . "\n";
969
                var_export($this->toArray($options));
970
            }
971
            catch (\Exception $e) {
972
                echo "\nError while dumping: " . $e->getMessage() . "\n";
973
                var_export($caller);
974
                echo "\n\n";
975
                var_export($bt);
976
                echo "\n\n";
977
                var_export($this->toArray($options));
978
            }
979
            echo "\n\n";
980
981
            if ($exit) {
982
                exit;
983
            }
984
        }
985
986 4
        return $this;
987
    }
988
989
    /**
990
     * Applies the current instance to a set of data.
991
     *
992
     * @param  mixed                  $data_to_filter
993
     * @param  Filterer|callable|null $filterer
994
     *
995
     * @return mixed The filtered data
996
     */
997 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...
998
    {
999 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...
1000 5
            $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...
1001 5
        }
1002
        elseif (is_callable($filterer)) {
1003
            $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...
1004
        }
1005
        elseif (! $filterer instanceof Filterer) {
1006
            throw new \InvalidArgumentException(
1007
                 "The given \$filterer must be null or a callable or a instance "
1008
                ."of Filterer instead of: ".var_export($filterer, true)
1009
            );
1010
        }
1011
1012 5
        if ($data_to_filter instanceof LogicalFilter) {
1013 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...
1014 2
            return $data_to_filter->flushRules()->addRule( $filtered_rules );
1015
        }
1016
        else {
1017 3
            return $filterer->apply($this, $data_to_filter);
1018
        }
1019
    }
1020
1021
    /**
1022
     * Applies the current instance to a value (and its index optionnally).
1023
     *
1024
     * @param  mixed                  $value_to_check
1025
     * @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...
1026
     * @param  Filterer|callable|null $filterer
1027
     *
1028
     * @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...
1029
     *                                 + Null if the target has no sens (operation filtered by field for example)
1030
     *                                 + A rule tree containing the first matching case if there is one.
1031
     */
1032 4
    public function validates($value_to_check, $key_to_check=null, $filterer=null)
1033
    {
1034 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...
1035 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...
1036 4
        }
1037
        elseif (is_callable($filterer)) {
1038
            $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...
1039
        }
1040
        elseif (! $filterer instanceof Filterer) {
1041
            throw new \InvalidArgumentException(
1042
                 "The given \$filterer must be null or a callable or a instance "
1043
                ."of Filterer instead of: ".var_export($filterer, true)
1044
            );
1045
        }
1046
1047 4
        return $filterer->hasMatchingCase($this, $value_to_check, $key_to_check);
1048
    }
1049
1050
    /**/
1051
}
1052