AbstractOperationRule::unifyAtomicOperands()   B
last analyzed

Complexity

Conditions 9
Paths 50

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 9.3399

Importance

Changes 0
Metric Value
cc 9
nc 50
nop 2
dl 0
loc 49
ccs 26
cts 31
cp 0.8387
crap 9.3399
rs 7.5571
c 0
b 0
f 0
1
<?php
2
namespace JClaveau\LogicalFilter\Rule;
3
4
use       JClaveau\VisibilityViolator;
5
6
/**
7
 * Operation rules:
8
 * + Or
9
 * + And
10
 * + Not
11
 */
12
abstract class AbstractOperationRule extends AbstractRule
13
{
14
    /**
15
     * This property should never be null.
16
     *
17
     * @var array<AbstractRule> $operands
18
     */
19
    protected $operands = [];
20
21
    const remove_negations        = 'remove_negations';
22
    const rootify_disjunctions    = 'rootify_disjunctions';
23
    const unify_atomic_operands   = 'unify_atomic_operands';
24
    const remove_invalid_branches = 'remove_invalid_branches';    // simplified after this step
25
26
    const simplified              = self::remove_invalid_branches;
27
28
    /**
29
     * The order is important!
30
     *
31
     * @var array $simplification_steps
32
     */
33
    const simplification_steps = [
34
        AbstractOperationRule::remove_negations,
35
        AbstractOperationRule::rootify_disjunctions,
36
        AbstractOperationRule::unify_atomic_operands,
37
        AbstractOperationRule::remove_invalid_branches,
38
    ];
39
40
    /**
41
     * @var null|string $simplified
42
     */
43
    protected $current_simplification_step = null;
44
45
    /**
46
     */
47 288
    public function __construct( array $operands=[] )
48
    {
49 288
        $this->setOperands( $operands );
50 288
        $this->flushCache();
51 288
    }
52
53
    /**
54
     * @return bool
55
     */
56
    public function isSimplified()
57
    {
58
        return self::simplified == $this->current_simplification_step;
59
    }
60
61
    /**
62
     * Adds an operand to the logical operation (&& or ||).
63
     *
64
     * @param  AbstractRule $new_operand
65
     *
66
     * @return $this
67
     */
68 293
    public function addOperand( AbstractRule $new_operand )
69
    {
70 293
        if ( ! isset($this->operands[ $id = $new_operand->getSemanticId() ])) {
71 293
            $this->operands[ $id ] = $new_operand;
72
73 293
            if ($this->current_simplification_step) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->current_simplification_step of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
74 149
                $this->current_simplification_step = null;
75 149
            }
76
77 293
            $this->flushCache();
78 293
        }
79
80 293
        return $this;
81
    }
82
83
    /**
84
     * @return array
85
     */
86 282
    public function getOperands()
87
    {
88 282
        return array_values( $this->operands );
89
    }
90
91
    /**
92
     * @return $this
93
     */
94 291
    public function setOperands(array $operands)
95
    {
96 291
        $this->operands = [];
97 291
        foreach ($operands as $operand) {
98 169
            $this->addOperand($operand);
99 291
        }
100
101 291
        return $this;
102
    }
103
104
    /**
105
     * @param  array|callable $renamings Associative array of renamings or callable
106
     *                                   that would rename the fields.
107
     *
108
     * @return $this
109
     */
110 1
    public function renameFields($renamings)
111
    {
112 1
        $this->renameFields_andReturnIsChanged($renamings);
113 1
        return $this;
114
    }
115
116
    /**
117
     * @param  array|callable $renamings Associative array of renamings or callable
118
     *                                   that would rename the fields.
119
     *
120
     * @return boolean Whether or not the operation changed semantically
0 ignored issues
show
Documentation introduced by
Should the return type not be AbstractOperationRule?

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...
121
     */
122 1
    public function renameFields_andReturnIsChanged($renamings)
123
    {
124 1
        $is_changed = false;
125
126 1
        foreach ($this->operands as $operand) {
127 1
            if ($operand->renameFields_andReturnIsChanged($renamings)) {
128 1
                $is_changed = true;
129 1
            }
130 1
        }
131
132 1
        if ($is_changed) {
133 1
            $this->flushCache();
134 1
        }
135
136
        // TODO remove this forced cache flushing ONLY when carefully
137
        // unit tested
138 1
        $this->flushCache();
139 1
        return $this;
140
    }
141
142
    /**
143
     * @param string $step_to_go_to
144
     * @param array  $simplification_options
145
     * @param bool   $force
146
     */
147 152
    public function moveSimplificationStepForward($step_to_go_to, array $simplification_options, $force=false)
0 ignored issues
show
Unused Code introduced by
The parameter $simplification_options 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...
148
    {
149 152
        if ( ! in_array($step_to_go_to, self::simplification_steps)) {
150
            throw new \InvalidArgumentException(
151
                "Invalid simplification step to go to: ".$step_to_go_to
152
            );
153
        }
154
155
        // if ($this->isNormalizationAllowed($simplification_options) && !$force && $this->current_simplification_step != null) {
156 152
        if ( ! $force && null !== $this->current_simplification_step) {
157 31
            $steps_indices = array_flip(self::simplification_steps);
158
159 31
            $current_index = $steps_indices[ $this->current_simplification_step ];
160 31
            $target_index  = $steps_indices[ $step_to_go_to ];
161
162 31
            if ( $current_index >= $target_index ) {
163
                // allow recall of previous step without going back
164 4
                return;
165
            }
166 29
            elseif ( $current_index < $target_index - 1 ) {
167
                throw new \LogicException(
168
                    "$step_to_go_to MUST be fullfilled after " . self::simplification_steps[$target_index - 1]
169
                    . " instead of the current step: " . $this->current_simplification_step
170
                    ."\nfor: " . $this
171
                );
172
            }
173 29
        }
174
175 152
        $this->current_simplification_step = $step_to_go_to;
176 152
    }
177
178
    /**
179
     * @return string The current simplification step
0 ignored issues
show
Documentation introduced by
Should the return type not be null|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...
180
     */
181 57
    public function getSimplificationStep()
182
    {
183 57
        return $this->current_simplification_step;
184
    }
185
186
    /**
187
     * Checks if a simplification step is reached.
188
     *
189
     * @param  string $step
190
     *
191
     * @return bool
192
     */
193 68
    public function simplicationStepReached($step)
194
    {
195 68
        if ( ! in_array($step, self::simplification_steps)) {
196
            throw new \InvalidArgumentException(
197
                "Invalid simplification step: ".$step
198
            );
199
        }
200
201 68
        if (null === $this->current_simplification_step) {
202
            return false;
203
        }
204
205 68
        $steps_indices = array_flip(self::simplification_steps);
206
207 68
        $current_index = $steps_indices[ $this->current_simplification_step ];
208 68
        $step_index    = $steps_indices[ $step ];
209
210 68
        return $current_index >= $step_index;
211
    }
212
213
    /**
214
     * Replace NotRule objects by the negation of their operands.
215
     *
216
     * @return AbstractOperationRule $this or a $new rule with negations removed
217
     */
218 152
    public function removeNegations(array $contextual_options)
219
    {
220 152
        if ( ! $this->isNormalizationAllowed($contextual_options)) {
221 45
            return $this;
222
        }
223
224 152
        $this->moveSimplificationStepForward(self::remove_negations, $contextual_options);
225
226 152
        $new_rule = $this;
227 152
        if ($operands = $this->operands) {
228 148
            foreach ($operands as $i => $operand) {
229 148
                if ($operand instanceof NotRule) {
230 34
                    $operands[$i] = $operand->negateOperand($contextual_options);
231 34
                }
232
233 148
                if ($operands[$i] instanceof AbstractOperationRule) {
234 93
                    $operands[$i]->removeNegations( $contextual_options );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class JClaveau\LogicalFilter\Rule\AbstractRule as the method removeNegations() does only exist in the following sub-classes of JClaveau\LogicalFilter\Rule\AbstractRule: JClaveau\LogicalFilter\Rule\AboveOrEqualRule, JClaveau\LogicalFilter\Rule\AbstractOperationRule, JClaveau\LogicalFilter\Rule\AndRule, JClaveau\LogicalFilter\Rule\BelowOrEqualRule, JClaveau\LogicalFilter\Rule\BetweenOrEqualBothRule, JClaveau\LogicalFilter\R...BetweenOrEqualLowerRule, JClaveau\LogicalFilter\R...BetweenOrEqualUpperRule, JClaveau\LogicalFilter\Rule\BetweenRule, JClaveau\LogicalFilter\Rule\InRule, JClaveau\LogicalFilter\Rule\NotEqualRule, JClaveau\LogicalFilter\Rule\NotInRule, JClaveau\LogicalFilter\Rule\NotRule, JClaveau\LogicalFilter\Rule\OrRule. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
235 93
                }
236 148
            }
237
238 148
            $new_rule = $this->setOperandsOrReplaceByOperation($operands, $contextual_options);
0 ignored issues
show
Bug introduced by
The method setOperandsOrReplaceByOperation() does not exist on JClaveau\LogicalFilter\Rule\AbstractOperationRule. Did you maybe mean setOperands()?

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...
239 148
        }
240
241 152
        return $new_rule;
242
    }
243
244
    /**
245
     * Operation cleaning consists of removing operation with one operand
246
     * and removing operations having a same type of operation as operand.
247
     *
248
     * This operation has been required between every steps until now.
249
     *
250
     * @toopt Trigger cleaning requirement during simplification steps
251
     *
252
     * @param  array    $simplification_options
253
     * @param  bool     $recurse
254
     *
255
     * @return AbstractOperationRule
256
     */
257 152
    public function cleanOperations(array $simplification_options, $recurse=true)
258
    {
259 152
        if ($recurse) {
260 152
            foreach ($this->operands as $i => $operand) {
261
                if (     $operand instanceof AbstractOperationRule
262 152
                    && ! $operand instanceof InRule
263 152
                    && ! $operand instanceof NotEqualRule
264 152
                    && ! $operand instanceof NotInRule
265 152
                ) {
266 123
                    $this->operands[$i] = $operand->cleanOperations($simplification_options);
267 123
                }
268 152
            }
269 152
        }
270
271 152
        if ($this instanceof NotRule) {
272 9
            return $this;
273
        }
274
275 152
        $is_modified = true;
276 152
        while ($is_modified) {
277 152
            $is_modified = false;
278
279 152
            if ($this->removeMonooperandOperationsOperands($simplification_options)) {
280 121
                $is_modified = true;
281 121
            }
282
283 152
            if ($this->removeSameOperationOperands($simplification_options)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class JClaveau\LogicalFilter\Rule\AbstractOperationRule as the method removeSameOperationOperands() does only exist in the following sub-classes of JClaveau\LogicalFilter\Rule\AbstractOperationRule: JClaveau\LogicalFilter\Rule\AboveOrEqualRule, JClaveau\LogicalFilter\Rule\AndRule, JClaveau\LogicalFilter\Rule\BelowOrEqualRule, JClaveau\LogicalFilter\Rule\BetweenOrEqualBothRule, JClaveau\LogicalFilter\R...BetweenOrEqualLowerRule, JClaveau\LogicalFilter\R...BetweenOrEqualUpperRule, JClaveau\LogicalFilter\Rule\BetweenRule, JClaveau\LogicalFilter\Rule\InRule, JClaveau\LogicalFilter\Rule\OrRule. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
284 90
                $is_modified = true;
285 90
            }
286 152
        }
287
288 152
        return $this;
289
    }
290
291
    /**
292
     * If a child is an OrRule or an AndRule and has only one child,
293
     * replace it by its child.
294
     *
295
     * @used-by removeSameOperationOperands() Ping-pong recursion
296
     *
297
     * @return bool If something has been simplified or not
298
     */
299 152
    public function removeMonooperandOperationsOperands(array $simplification_options)
300
    {
301 152
        foreach ($this->operands as $i => $operand) {
302 152
            if ( ! $operand instanceof AbstractOperationRule || $operand instanceof NotRule) {
303 139
                continue;
304
            }
305
306 151
            if ($operand instanceof InRule && ! $operand->isNormalizationAllowed($simplification_options)) {
307 34
                $count = count($operand->getPossibilities());
308 34
            }
309
            else {
310 151
                $count = count($operand->getOperands());
311
            }
312
313
            if (
314 151
                    ($operand instanceof AndRule || $operand instanceof OrRule)
315 151
                && 1 == $count
316 151
            ) {
317 121
                $sub_operands       = $operand->getOperands();
318 121
                $this->operands[$i] = reset($sub_operands);
319 121
                $has_been_changed   = true;
320 121
            }
321 152
        }
322
323 152
        return ! empty($has_been_changed);
324
    }
325
326
    /**
327
     * Removes duplicates between the current AbstractOperationRule.
328
     *
329
     * @return AbstractOperationRule the simplified rule
330
     */
331 152
    public function unifyAtomicOperands($simplification_strategy_step = false, array $contextual_options)
332
    {
333 152
        if ($simplification_strategy_step) {
334 152
            $this->moveSimplificationStepForward( self::unify_atomic_operands, $contextual_options );
335 152
        }
336
337
        // $this->dump(true);
338
339 152
        if ( ! $this->isNormalizationAllowed($contextual_options)) {
340 49
            return $this;
341
        }
342
343 152
        $operands = $this->getOperands();
344 152
        foreach ($operands as &$operand) {
345 152
            if ($operand instanceof AbstractOperationRule) {
346 98
                $operand = $operand->unifyAtomicOperands($simplification_strategy_step, $contextual_options);
347 98
            }
348 152
        }
349
350 152
        $class = get_class($this);
351
352 152
        $operandsByFields = $class::groupOperandsByFieldAndOperator_static($operands);
353 152
        $operandsByFields = $class::simplifySameOperands($operandsByFields);
354
355 152
        if ($this instanceof AndRule) {
356
            // unifiying operands of different types
357 151
            $operandsByFields = $class::simplifyDifferentOperands($operandsByFields);
358 151
        }
359
360
        // Remove the index by fields and operators
361 152
        $unifiedOperands = [];
362 152
        foreach ($operandsByFields as $field => $operandsByOperator) {
363 149
            foreach ($operandsByOperator as $operator => $operands) {
364
                try {
365 149
                    $unifiedOperands = array_merge($unifiedOperands, $operands);
366
                }
367 149
                catch (\Exception $e) {
368
                    VisibilityViolator::setHiddenProperty(
369
                        $e, 'message',
370
                        $e->getMessage() . "\n" . var_export($operandsByOperator, true)
371
                    );
372
373
                    throw $e;
374
                }
375 149
            }
376 152
        }
377
378 152
        return $this->setOperandsOrReplaceByOperation( $unifiedOperands, $contextual_options );
0 ignored issues
show
Bug introduced by
The method setOperandsOrReplaceByOperation() does not exist on JClaveau\LogicalFilter\Rule\AbstractOperationRule. Did you maybe mean setOperands()?

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...
379
    }
380
381
    private static $simplification_cache = [];
382
383
    /**
384
     * Simplify the current OperationRule.
385
     * + If an OrRule or an AndRule contains only one operand, it's equivalent
386
     *   to it.
387
     * + If an OrRule has an other OrRule as operand, they can be merged
388
     * + If an AndRule has an other AndRule as operand, they can be merged
389
     *
390
     * @param  array $options stop_after | stop_before | force_logical_core
391
     *
392
     * @return AbstractRule the simplified rule
393
     */
394 152
    final public function simplify($options=[])
395
    {
396 152
        $step_to_stop_before = ! empty($options['stop_before'])        ? $options['stop_before'] : null;
397 152
        $step_to_stop_after  = ! empty($options['stop_after'])         ? $options['stop_after']  : null;
398 152
        $force_logical_core  = ! empty($options['force_logical_core']) ? $options['force_logical_core'] : false;
399
400 152
        if ($step_to_stop_before && ! in_array($step_to_stop_before, self::simplification_steps)) {
401
            throw new \InvalidArgumentException(
402
                "Invalid simplification step to stop at: ".$step_to_stop_before
403
            );
404
        }
405
406 152
        ksort($options);
407 152
        $options_id = hash('md4', serialize($options));
408
409 152
        $id = $this->getSemanticId().'-'.$options_id;
410 152
        if (isset(self::$simplification_cache[$id])) {
411 12
            return self::$simplification_cache[$id]->copy();
412
        }
413
414 152
        $this->flushCache();
415
416 152
        $cache_keys = [$id];
417
418
        // $this->dump(true);
419 152
        $this->cleanOperations($options);
420
        // $this->dump(true);
421 152
        $instance = $this->unifyAtomicOperands(false, $options);
422
423 152
        $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
424
425 152
        if (self::remove_negations == $step_to_stop_before) {
426
            return $instance;
427
        }
428
429
        // $this->dump(!true);
430 152
        $instance = $instance->removeNegations($options);
431
432
        // $instance->dump(true);
433
434 152
        if (self::remove_negations == $step_to_stop_after ||
435 152
            self::rootify_disjunctions == $step_to_stop_before ) {
436
            return $instance;
437
        }
438
439
        // $instance->dump(true);
440
441 152
        $instance->cleanOperations($options);
442 152
        $instance = $instance->rootifyDisjunctions($options);
443
444
        // $instance->dump(true);
445
446 152
        if (self::rootify_disjunctions == $step_to_stop_after ||
447 152
            self::unify_atomic_operands == $step_to_stop_before ) {
448
            return $instance;
449
        }
450
451 152
        if ( ! $instance instanceof AbstractAtomicRule) {
452 152
            $instance->cleanOperations($options);
453 152
            $instance->unifyAtomicOperands(true, $options);
454
455
            // $instance->dump(true);
456
457 152
            if (self::unify_atomic_operands == $step_to_stop_after ||
458 152
                self::remove_invalid_branches == $step_to_stop_before ) {
459 1
                return $instance;
460
            }
461
462 151
            $instance->cleanOperations($options);
463 151
            if (method_exists($instance, 'removeInvalidBranches')) {
464 151
                $instance->removeInvalidBranches($options);
465 151
            }
466 151
        }
467
468
        // $instance->dump(true);
469 151
        $instance->cleanOperations($options);
470
471
        // the root rule cannot be cleaned so we wrap it and apply a
472
        // last non recursive clean
473
        // TODO kind of monad|become|cese
474
        // @see https://github.com/jclaveau/php-logical-filter/issues/20
475 151
        if ($instance instanceof AndRule || $instance instanceof OrRule ) {
476 151
            if ( ! $instance->getOperands()) {
477 23
                return $instance;
478
            }
479
480 143
            $operands = (new AndRule([$instance]))
481 143
                ->cleanOperations($options, false)
482
                // ->dump(true)
483 143
                ->getOperands();
484
485 143
            if (1 == count($operands)) {
486 110
                $instance = reset($operands);
487 110
            }
488 143
        }
489
490 143
        if ($force_logical_core) {
491 10
            $instance = $instance->addMinimalCase();
492 10
        }
493
494 143
        $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
495 143
        foreach ($cache_keys as $cache_key) {
496 143
            self::$simplification_cache[ $cache_key ] = $instance;
497 143
        }
498
499 143
        return $instance->copy();
500
    }
501
502
    /**
503
     * Indexes operands by their fields and operators. This sorting is
504
     * used during the simplification step.
505
     *
506
     * @return array The 3 dimensions array of operands: field > operator > i
507
     */
508 141 View Code Duplication
    public function groupOperandsByFieldAndOperator()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
509
    {
510 141
        $operandsByFields = [];
511 141
        foreach ($this->operands as $operand) {
512
513
            // Operation rules have no field but we need to keep them anyway
514 136
            $field = method_exists($operand, 'getField') ? $operand->getField() : '';
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class JClaveau\LogicalFilter\Rule\AbstractRule as the method getField() does only exist in the following sub-classes of JClaveau\LogicalFilter\Rule\AbstractRule: JClaveau\LogicalFilter\Rule\AboveOrEqualRule, JClaveau\LogicalFilter\Rule\AboveRule, JClaveau\LogicalFilter\Rule\AbstractAtomicRule, JClaveau\LogicalFilter\Rule\BelowOrEqualRule, JClaveau\LogicalFilter\Rule\BelowRule, JClaveau\LogicalFilter\Rule\BetweenOrEqualBothRule, JClaveau\LogicalFilter\R...BetweenOrEqualLowerRule, JClaveau\LogicalFilter\R...BetweenOrEqualUpperRule, JClaveau\LogicalFilter\Rule\BetweenRule, JClaveau\LogicalFilter\Rule\EqualRule, JClaveau\LogicalFilter\Rule\InRule, JClaveau\LogicalFilter\Rule\NotEqualRule, JClaveau\LogicalFilter\Rule\NotInRule, JClaveau\LogicalFilter\Rule\RegexpRule. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
515
516
            // For FilteredValue and FilteredKey
517 136
            $field = (string) $field;
518
519 136
            if ( ! isset($operandsByFields[ $field ])) {
520 136
                $operandsByFields[ $field ] = [];
521 136
            }
522
523 136
            if ( ! isset($operandsByFields[ $field ][ $operand::operator ])) {
524 136
                $operandsByFields[ $field ][ $operand::operator ] = [];
525 136
            }
526
527 136
            $operandsByFields[ $field ][ $operand::operator ][] = $operand;
528 141
        }
529
530 141
        return $operandsByFields;
531
    }
532
533
    /**
534
     * Indexes operands by their fields and operators. This sorting is
535
     * used during the simplification step.
536
     *
537
     * @return array The 3 dimensions array of operands: field > operator > i
538
     */
539 152 View Code Duplication
    protected static function groupOperandsByFieldAndOperator_static($operands)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
540
    {
541 152
        $operandsByFields = [];
542 152
        foreach ($operands as $operand) {
543
544
            // Operation rules have no field but we need to keep them anyway
545 152
            $field = method_exists($operand, 'getField') ? $operand->getField() : '';
546
547
            // For FilteredValue and FilteredKey
548 152
            $field = (string) $field;
549
550 152
            if ( ! isset($operandsByFields[ $field ])) {
551 152
                $operandsByFields[ $field ] = [];
552 152
            }
553
554 152
            if ( ! isset($operandsByFields[ $field ][ $operand::operator ])) {
555 152
                $operandsByFields[ $field ][ $operand::operator ] = [];
556 152
            }
557
558 152
            $operandsByFields[ $field ][ $operand::operator ][] = $operand;
559 152
        }
560
561 152
        return $operandsByFields;
562
    }
563
564
    /**
565
     * Clones the rule and its operands.
566
     *
567
     * @return AbstractOperationRule A copy of the current instance with copied operands.
568
     */
569 156
    final public function copy()
570
    {
571 156
        return clone $this;
572
    }
573
574
    /**
575
     * Make a deep copy of operands
576
     */
577 156
    public function __clone()
578
    {
579 156
        foreach ($this->operands as $operand_id => &$operand) {
580 139
            $this->operands[$operand_id] = $operand->copy();
581 156
        }
582 156
    }
583
584
    /**
585
     */
586 152
    public function isNormalizationAllowed(array $current_simplification_options)
587
    {
588 152
        return true;
589
    }
590
591
    /**
592
     * Returns an operand based on its position
593
     *
594
     * @return AbstractRule|null The operand if it exists or null
595
     */
596 98
    protected function getOperandAt($index=0)
597
    {
598 98
        $operands = array_values($this->operands);
599 98
        if (isset($operands[$index])) {
600 96
            return $operands[$index];
601
        }
602 5
    }
603
604
    /**/
605
}
606