Filterer   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 287
Duplicated Lines 14.63 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 77.99%

Importance

Changes 0
Metric Value
dl 42
loc 287
ccs 124
cts 159
cp 0.7799
rs 8.96
c 0
b 0
f 0
wmc 43
lcom 1
cbo 2

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setCustomActions() 0 5 1
A onRowMatches() 9 23 3
B onRowMismatches() 9 32 6
A getChildren() 0 4 1
A setChildren() 0 3 1
A apply() 0 24 5
B foreachRow() 24 51 9
A hasMatchingCase() 0 13 3
C applyOnRow() 0 82 14

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Filterer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

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

1
<?php
2
/**
3
 * Filterer
4
 *
5
 * @package php-logical-filter
6
 * @author  Jean Claveau
7
 */
8
namespace JClaveau\LogicalFilter\Filterer;
9
10
use       JClaveau\LogicalFilter\Filterer\FiltererInterface;
11
use       JClaveau\LogicalFilter\LogicalFilter;
12
13
use       JClaveau\LogicalFilter\Rule\InRule;
14
use       JClaveau\LogicalFilter\Rule\NotInRule;
15
use       JClaveau\LogicalFilter\Rule\EqualRule;
16
use       JClaveau\LogicalFilter\Rule\BelowRule;
17
use       JClaveau\LogicalFilter\Rule\AboveRule;
18
use       JClaveau\LogicalFilter\Rule\NotEqualRule;
19
use       JClaveau\LogicalFilter\Rule\AbstractAtomicRule;
20
use       JClaveau\LogicalFilter\Rule\AbstractOperationRule;
21
22
/**
23
 * This filterer provides the tools and API to apply a LogicalFilter once it has
24
 * been simplified.
25
 */
26
abstract class Filterer implements FiltererInterface
27
{
28
    const leaves_only       = 'leaves_only';
29
    const on_row_matches    = 'on_row_matches';
30
    const on_row_mismatches = 'on_row_mismatches';
31
32
    /** @var array $custom_actions */
33
    protected $custom_actions = [
34
        // self::on_row_matches    => null,
35
        // self::on_row_mismatches => null,
36
    ];
37
38
    /**
39
     */
40
    public function setCustomActions(array $custom_actions)
41
    {
42
        $this->custom_actions = $custom_actions;
43
        return $this;
44
    }
45
46
    /**
47
     */
48 62
    public function onRowMatches(&$row, $key, &$rows, $matching_case, $options)
49
    {
50 62 View Code Duplication
        if (isset($options[ self::on_row_matches ])) {
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...
51 18
            $callback = $options[ self::on_row_matches ];
52 18
        }
53 45
        elseif (isset($this->custom_actions[ self::on_row_matches ])) {
54
            $callback = $this->custom_actions[ self::on_row_matches ];
55
        }
56
        else {
57 45
            return;
58
        }
59
60
        $args = [
61
            // &$row,
62 18
            $row,
63 18
            $key,
64 18
            &$rows,
65 18
            $matching_case,
66 18
            $options,
67 18
        ];
68
69 18
        call_user_func_array($callback, $args);
70 17
    }
71
72
    /**
73
     */
74 63
    public function onRowMismatches(&$row, $key, &$rows, $matching_case, $options)
75
    {
76 63
        if (   ! $this->custom_actions
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->custom_actions 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...
77 63
            && ! isset($options[self::on_row_mismatches])
78 63
            && ! isset($options[self::on_row_matches])
79 63
        ) {
80
            // Unset by default ONLY if NO custom action defined
81 47
            unset($rows[$key]);
82 47
            return;
83
        }
84
85 17 View Code Duplication
        if (isset($options[ self::on_row_mismatches ])) {
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...
86 2
            $callback = $options[ self::on_row_mismatches ];
87 2
        }
88 15
        elseif (isset($this->custom_actions[ self::on_row_mismatches ])) {
89
            $callback = $this->custom_actions[ self::on_row_mismatches ];
90
        }
91
        else {
92 15
            return;
93
        }
94
95
        $args = [
96
            // &$row,
97 2
            $row,
98 2
            $key,
99 2
            &$rows,
100 2
            $matching_case,
101 2
            $options,
102 2
        ];
103
104 2
        call_user_func_array($callback, $args);
105 2
    }
106
107
    /**
108
     * @return array
109
     */
110 25
    public function getChildren($row)
111
    {
112 25
        return [];
113
    }
114
115
    /**
116
     */
117
    public function setChildren(&$row, $filtered_children)
118
    {
119
    }
120
121
    /**
122
     * @param LogicalFilter   $filter
123
     * @param Iterable        $tree_to_filter
124
     * @param array           $options
125
     */
126 70
    public function apply( LogicalFilter $filter, $tree_to_filter, $options=[] )
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
127
    {
128 70
        if (! $filter->hasSolution()) {
129
            return null;
130
        }
131
132 70
        if (! isset($options['recurse'])) {
133 70
            $options['recurse'] = 'before';
134 70
        }
135
        elseif (! in_array($options['recurse'], ['before', 'after', null])) {
136
            throw new \InvalidArgumentException(
137
                "Invalid value for 'recurse' option: "
138
                .var_export($options['recurse'], true)
139
                ."\nInstead of ['before', 'after', null]"
140
            );
141
        }
142
143 70
        return $this->foreachRow(
144 70
            !$filter->getRules() ? [] : $filter->addMinimalCase()->getRules()->getOperands(),
145 70
            $tree_to_filter,
146 70
            $path=[],
147
            $options
148 70
        );
149
    }
150
151
    /**
152
     */
153 70
    protected function foreachRow(array $root_cases, $tree_to_filter, array $path, $options=[])
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
154
    {
155
        // Once the rules are prepared, we parse the data
156 70
        foreach ($tree_to_filter as $row_index => $row_to_filter) {
157 70
            array_push($path, $row_index);
158
159 70 View Code Duplication
            if ('before' == $options['recurse']) {
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...
160 70
                if ($children = $this->getChildren($row_to_filter)) {
161 43
                    $filtered_children = $this->foreachRow(
162 43
                        $root_cases,
163 43
                        $children,
164 43
                        $path,
165
                        $options
166 43
                    );
167
168 39
                    $this->setChildren($row_to_filter, $filtered_children);
169 39
                }
170 70
            }
171
172 70
            $matching_case = $this->applyOnRow($root_cases, $row_to_filter, $path, $options);
173
174 65
            if ($matching_case) {
175 62
                $this->onRowMatches($row_to_filter, $row_index, $tree_to_filter, $matching_case, $options);
176 61
            }
177 63
            elseif (false === $matching_case) {
178
                // No case match the rule
179 63
                $this->onRowMismatches($row_to_filter, $row_index, $tree_to_filter, $matching_case, $options);
180 63
            }
181 16
            elseif (null === $matching_case) {
182
                // We simply avoid rules
183
                // row out of scope
184 16
            }
185
186 64 View Code Duplication
            if ('after' == $options['recurse']) {
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...
187
                if ($children = $this->getChildren($row_to_filter)) {
188
                    $filtered_children = $this->foreachRow(
189
                        $root_cases,
190
                        $children,
191
                        $path,
192
                        $options
193
                    );
194
195
                    $this->setChildren($row_to_filter, $filtered_children);
196
                }
197
            }
198
199 64
            array_pop($path);
200 64
        }
201
202 64
        return $tree_to_filter;
203
    }
204
205
    /**
206
     * @param LogicalFilter   $filter
207
     * @param Iterable        $tree_to_filter
0 ignored issues
show
Documentation introduced by
There is no parameter named $tree_to_filter. Did you maybe mean $filter?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

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

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

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

Loading history...
208
     * @param array           $options
209
     *
210
     * @return bool
211
     */
212 4
    public function hasMatchingCase( LogicalFilter $filter, $row_to_check, $key_to_check, $options=[] )
213
    {
214 4
        if (! $filter->hasSolution()) {
215
            return null;
216
        }
217
218 4
        return $this->applyOnRow(
219 4
            !$filter->getRules() ? [] : $filter->addMinimalCase()->getRules()->getOperands(),
220 4
            $row_to_check,
221 4
            $path=[$key_to_check],
222
            $options
223 4
        );
224
    }
225
226
    /**
227
     */
228 74
    protected function applyOnRow(array $root_cases, $row_to_filter, array $path, $options=[])
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
229
    {
230 74
        $operands_validation_row_cache = [];
231
232 74
        if (! $root_cases) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $root_cases 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...
233 1
            $matching_case = true;
234 1
        }
235
        else {
236 73
            $matching_case = null;
237 73
            foreach ($root_cases as $and_case_index => $and_case) {
238 73
                if (! empty($options['debug'])) {
239
                    var_dump("Case $and_case_index: ".$and_case);
240
                }
241
242 73
                $case_is_good = null;
243 73
                foreach ($and_case->getOperands() as $i => $rule) {
244 73
                    $class = get_class($rule);
245
246 73
                    if (in_array($class, [OrRule::class, AndRule::class, ])) {
247
                        $field = null;
248
                        $value = $rule->getOperands();
249
                    }
250 73
                    elseif ($rule instanceof AbstractAtomicRule || ! $rule->isNormalizationAllowed($options)) {
251 73
                        $field = $rule->getField();
252 73
                        $value = $rule->getValues();
253 73
                    }
254
                    else {
255
                        throw new \LogicException(
256
                            "Filtering with a rule which has not been simplified: $rule"
257
                        );
258
                    }
259
260 73
                    $operator = $rule::operator;
261
262 73
                    $cache_key = $and_case_index.'~|~'.$field.'~|~'.$operator;
263
264 73
                    if (! empty($operands_validation_row_cache[ $cache_key ])) {
265
                        $is_valid = $operands_validation_row_cache[ $cache_key ];
266
                    }
267
                    else {
268 73
                        $is_valid = $this->validateRule(
269 73
                            $field,
270 73
                            $operator,
271 73
                            $value,
272 73
                            $row_to_filter,
273 73
                            $path,
274 73
                            $root_cases,
275
                            $options
276 73
                        );
277
278 68
                        $operands_validation_row_cache[ $cache_key ] = $is_valid;
279
                    }
280
281 68
                    if (false === $is_valid) {
282
                        // one of the rules of the and_case do not validate
283
                        // so all the and_case is invalid
284 67
                        $case_is_good = false;
285 67
                        break;
286
                    }
287 66
                    elseif (true === $is_valid) {
288
                        // one of the rules of the and_case do not validate
289
                        // so all the and_case is invalid
290 65
                        $case_is_good = true;
291 65
                    }
292 68
                }
293
294 68
                if (true === $case_is_good) {
295
                    // at least one and_case works so we can stop here
296 65
                    $matching_case = $and_case;
297 65
                    break;
298
                }
299 67
                elseif (false === $case_is_good) {
300 67
                    $matching_case = false;
301 67
                }
302 17
                elseif (null === $case_is_good) {
303
                    // row out of scope
304 17
                }
305 68
            }
306
        }
307
308 69
        return $matching_case;
309
    }
310
311
    /**/
312
}
313