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

AbstractOperationRule::removeNegations()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
nc 3
nop 1
dl 0
loc 25
ccs 17
cts 17
cp 1
crap 6
rs 8.8977
c 0
b 0
f 0
1
<?php
2
namespace JClaveau\LogicalFilter\Rule;
3
use       JClaveau\VisibilityViolator;
4
5
/**
6
 * Operation rules:
7
 * + Or
8
 * + And
9
 * + Not
10
 */
11
abstract class AbstractOperationRule extends AbstractRule
12
{
13
    /**
14
     * This property should never be null.
15
     *
16
     * @var array<AbstractRule> $operands
17
     */
18
    protected $operands = [];
19
20
    const remove_negations        = 'remove_negations';
21
    const rootify_disjunctions    = 'rootify_disjunctions';
22
    const unify_atomic_operands   = 'unify_atomic_operands';
23
    const remove_invalid_branches = 'remove_invalid_branches';    // simplified after this step
24
25
    const simplified              = self::remove_invalid_branches;
26
27
    /**
28
     * The order is important!
29
     *
30
     * @var array $simplification_steps
31
     */
32
    const simplification_steps = [
33
        AbstractOperationRule::remove_negations,
34
        AbstractOperationRule::rootify_disjunctions,
35
        AbstractOperationRule::unify_atomic_operands,
36
        AbstractOperationRule::remove_invalid_branches,
37
    ];
38
39
    /**
40
     * @var null|string $simplified
41
     */
42
    protected $current_simplification_step = null;
43
44
    /**
45
     */
46 153
    public function __construct( array $operands=[] )
47
    {
48 153
        $this->setOperands( $operands );
49 153
        $this->flushCache();
50 153
    }
51
52
    /**
53
     * @return bool
54
     */
55
    public function isSimplified()
56
    {
57
        return self::simplified == $this->current_simplification_step;
58
    }
59
60
    /**
61
     * Adds an operand to the logical operation (&& or ||).
62
     *
63
     * @param  AbstractRule $new_operand
64
     *
65
     * @return $this
66
     */
67 153
    public function addOperand( AbstractRule $new_operand )
68
    {
69 153
        if ( ! isset($this->operands[ $id = $new_operand->getSemanticId() ])) {
70 153
            $this->operands[ $id ] = $new_operand;
71
72 153
            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...
73 96
                $this->current_simplification_step = null;
74 96
            }
75
76 153
            $this->flushCache();
77 153
        }
78
79 153
        return $this;
80
    }
81
82
    /**
83
     * @return array
84
     */
85 148
    public function getOperands()
86
    {
87 148
        return array_values( $this->operands );
88
    }
89
90
    /**
91
     * @return $this
92
     */
93 154
    public function setOperands(array $operands)
94
    {
95 154
        $this->operands = [];
96 154
        foreach ($operands as $operand) {
97 119
            $this->addOperand($operand);
98 154
        }
99
100 154
        return $this;
101
    }
102
103
    /**
104
     * @param  array|callable $renamings Associative array of renamings or callable
105
     *                                   that would rename the fields.
106
     *
107
     * @return string $this
108
     */
109 1
    public function renameFields($renamings)
110
    {
111 1
        foreach ($this->operands as $operand) {
112 1
            if (method_exists($operand, 'renameField')) {
113 1
                $operand->renameField($renamings);
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 renameField() 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\EqualRule, JClaveau\LogicalFilter\Rule\NotEqualRule, 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...
114 1
            }
115
            else {
116 1
                $operand->renameFields($renamings);
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 renameFields() 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...
117
            }
118 1
        }
119
120
        // TODO flush cache only in case of change?
121 1
        $this->flushCache();
122
123 1
        return $this;
124
    }
125
126
    /**
127
     * @param string $step_to_go_to
128
     * @param array  $simplification_options
129
     * @param bool   $force
130
     */
131 99
    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...
132
    {
133 99
        if ( ! in_array($step_to_go_to, self::simplification_steps)) {
134
            throw new \InvalidArgumentException(
135
                "Invalid simplification step to go to: ".$step_to_go_to
136
            );
137
        }
138
139
        // if ($this->isNormalizationAllowed($simplification_options) && !$force && $this->current_simplification_step != null) {
140 99
        if ( ! $force && null !== $this->current_simplification_step) {
141 23
            $steps_indices = array_flip(self::simplification_steps);
142
143 23
            $current_index = $steps_indices[ $this->current_simplification_step ];
144 23
            $target_index  = $steps_indices[ $step_to_go_to ];
145
146 23
            if ( $current_index >= $target_index ) {
147
                // allow recall of previous step without going back
148 4
                return;
149
            }
150 21
            elseif ( $current_index < $target_index - 1 ) {
151
                throw new \LogicException(
152
                    "$step_to_go_to MUST be fullfilled after " . self::simplification_steps[$target_index - 1]
153
                    . " instead of the current step: " . $this->current_simplification_step
154
                    ."\nfor: " . $this
155
                );
156
            }
157 21
        }
158
159 99
        $this->current_simplification_step = $step_to_go_to;
160 99
    }
161
162
    /**
163
     * Checks if a simplification step is reached.
164
     *
165
     * @param  string $step
166
     *
167
     * @return bool
168
     */
169 53
    public function simplicationStepReached($step)
170
    {
171 53
        if ( ! in_array($step, self::simplification_steps)) {
172
            throw new \InvalidArgumentException(
173
                "Invalid simplification step: ".$step
174
            );
175
        }
176
177 53
        if (null === $this->current_simplification_step) {
178
            return false;
179
        }
180
181 53
        $steps_indices = array_flip(self::simplification_steps);
182
183 53
        $current_index = $steps_indices[ $this->current_simplification_step ];
184 53
        $step_index    = $steps_indices[ $step ];
185
186 53
        return $current_index >= $step_index;
187
    }
188
189
    /**
190
     * Replace NotRule objects by the negation of their operands.
191
     *
192
     * @return AbstractOperationRule $this or a $new rule with negations removed
193
     */
194 99
    public function removeNegations(array $contextual_options)
195
    {
196 99
        if ( ! $this->isNormalizationAllowed($contextual_options)) {
197 28
            return $this;
198
        }
199
200 99
        $this->moveSimplificationStepForward(self::remove_negations, $contextual_options);
201
202 99
        $new_rule = $this;
203 99
        if ($operands = $this->operands) {
204 95
            foreach ($operands as $i => $operand) {
205 95
                if ($operand instanceof NotRule) {
206 27
                    $operands[$i] = $operand->negateOperand(false, $contextual_options);
207 27
                }
208
209 95
                if ($operands[$i] instanceof AbstractOperationRule) {
210 59
                    $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...
211 59
                }
212 95
            }
213
214 95
            $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...
215 95
        }
216
217 99
        return $new_rule;
218
    }
219
220
    /**
221
     * Operation cleaning consists of removing operation with one operand
222
     * and removing operations having a same type of operation as operand.
223
     *
224
     * This operation has been required between every steps until now.
225
     *
226
     * @toopt Trigger cleaning requirement during simplification steps
227
     *
228
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
229
     */
230 99
    public function cleanOperations($simplification_options, $recurse=true)
231
    {
232 99
        if ($recurse) {
233 99
            foreach ($this->operands as $i => $operand) {
234
                if ( $operand instanceof AbstractOperationRule
235 99
                && ! $operand instanceof InRule
236 99
                && ! $operand instanceof NotEqualRule
237 99
                && ! $operand instanceof NotInRule
238 99
            ) {
239 83
                    $this->operands[$i] = $operand->cleanOperations($simplification_options);
240 83
                }
241 99
            }
242 99
        }
243
244 99
        if ($this instanceof NotRule) {
245 8
            return $this;
246
        }
247
248 99
        $is_modified = true;
249 99
        while ($is_modified) {
250 99
            $is_modified = false;
251
252 99
            if ($this->removeMonooperandOperationsOperands($simplification_options)) {
253 76
                $is_modified = true;
254 76
            }
255
256 99
            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...
257 59
                $is_modified = true;
258 59
            }
259 99
        }
260
261 99
        return $this;
262
    }
263
264
265
    /**
266
     * If a child is an OrRule or an AndRule and has only one child,
267
     * replace it by its child.
268
     *
269
     * @used-by removeSameOperationOperands() Ping-pong recursion
270
     *
271
     * @return bool If something has been simplified or not
272
     */
273 99
    public function removeMonooperandOperationsOperands(array $simplification_options)
274
    {
275 99
        foreach ($this->operands as $i => $operand) {
276 99
            if ( ! $operand instanceof AbstractOperationRule || $operand instanceof NotRule) {
277 96
                continue;
278
            }
279
280 98
            if ($operand instanceof InRule && ! $operand->isNormalizationAllowed($simplification_options)) {
281 20
                $count = count($operand->getPossibilities());
282 20
            }
283
            else {
284 98
                $count = count($operand->getOperands());
285
            }
286
287
            if (
288 98
                    ($operand instanceof AndRule || $operand instanceof OrRule)
289 98
                && 1 == $count
290 98
            ) {
291 76
                $sub_operands       = $operand->getOperands();
292 76
                $this->operands[$i] = reset($sub_operands);
293 76
                $has_been_changed   = true;
294 76
            }
295 99
        }
296
297 99
        return ! empty($has_been_changed);
298
    }
299
300
    /**
301
     * Removes duplicates between the current AbstractOperationRule.
302
     *
303
     * @return AbstractOperationRule the simplified rule
304
     */
305 99
    public function unifyAtomicOperands($simplification_strategy_step = false, array $contextual_options)
306
    {
307 99
        if ($simplification_strategy_step) {
308 99
            $this->moveSimplificationStepForward( self::unify_atomic_operands, $contextual_options );
309 99
        }
310
311
        // $this->dump(true);
312
313 99
        if ( ! $this->isNormalizationAllowed($contextual_options)) {
314 23
            return $this;
315
        }
316
317 99
        $operands = $this->getOperands();
318 99
        foreach ($operands as &$operand) {
319 99
            if ($operand instanceof AbstractOperationRule) {
320 64
                $operand = $operand->unifyAtomicOperands($simplification_strategy_step, $contextual_options);
321 64
            }
322 99
        }
323
324 99
        $class = get_class($this);
325
326 99
        $operandsByFields = $class::groupOperandsByFieldAndOperator_static($operands);
327 99
        $operandsByFields = $class::simplifySameOperands($operandsByFields);
328
329 99
        if ($this instanceof AndRule) {
330
            // unifiying operands of different types
331 98
            $operandsByFields = $class::simplifyDifferentOperands($operandsByFields);
332 98
        }
333
334
        // Remove the index by fields and operators
335 99
        $unifiedOperands = [];
336 99
        foreach ($operandsByFields as $field => $operandsByOperator) {
337 96
            foreach ($operandsByOperator as $operator => $operands) {
338
                try {
339 96
                    $unifiedOperands = array_merge($unifiedOperands, $operands);
340
                }
341 96
                catch (\Exception $e) {
342
                    VisibilityViolator::setHiddenProperty(
343
                        $e, 'message',
344
                        $e->getMessage() . "\n" . var_export($operandsByOperator, true)
345
                    );
346
347
                    throw $e;
348
                }
349 96
            }
350 99
        }
351
352 99
        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...
353
    }
354
355
    private static $simplification_cache = [];
356
357
    /**
358
     * Simplify the current OperationRule.
359
     * + If an OrRule or an AndRule contains only one operand, it's equivalent
360
     *   to it.
361
     * + If an OrRule has an other OrRule as operand, they can be merged
362
     * + If an AndRule has an other AndRule as operand, they can be merged
363
     *
364
     * @param  array $options stop_after | stop_before | force_logical_core
365
     *
366
     * @return AbstractRule the simplified rule
367
     */
368 102
    final public function simplify($options=[])
369
    {
370 102
        $step_to_stop_before = ! empty($options['stop_before'])        ? $options['stop_before'] : null;
371 102
        $step_to_stop_after  = ! empty($options['stop_after'])         ? $options['stop_after']  : null;
372 102
        $force_logical_core  = ! empty($options['force_logical_core']) ? $options['force_logical_core'] : false;
373
374 102
        if ($step_to_stop_before && ! in_array($step_to_stop_before, self::simplification_steps)) {
375
            throw new \InvalidArgumentException(
376
                "Invalid simplification step to stop at: ".$step_to_stop_before
377
            );
378
        }
379
380 102
        ksort($options);
381 102
        $options_id = hash('md4', serialize($options));
382
383 102
        $id = $this->getSemanticId().'-'.$options_id;
384 102
        if (isset(self::$simplification_cache[$id])) {
385 22
            return self::$simplification_cache[$id]->copy();
386
        }
387
388 99
        $this->flushCache();
389
390 99
        $cache_keys = [$id];
391
392
        // $this->dump(true);
393 99
        $this->cleanOperations($options);
394
        // $this->dump(true);
395 99
        $instance = $this->unifyAtomicOperands(false, $options);
396
397 99
        $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
398
399 99
        if (self::remove_negations == $step_to_stop_before) {
400
            return $instance;
401
        }
402
403
        // $this->dump(!true);
404 99
        $instance = $instance->removeNegations($options);
405
406
        // $instance->dump(true);
407
408 99
        if (self::remove_negations == $step_to_stop_after ||
409 99
            self::rootify_disjunctions == $step_to_stop_before ) {
410
            return $instance;
411
        }
412
413
        // $instance->dump(true);
414
415 99
        $instance->cleanOperations($options);
416 99
        $instance = $instance->rootifyDisjunctions($options);
417
418
        // $instance->dump(true);
419
420 99
        if (self::rootify_disjunctions == $step_to_stop_after ||
421 99
            self::unify_atomic_operands == $step_to_stop_before ) {
422
            return $instance;
423
        }
424
425 99
        if ( ! $instance instanceof AbstractAtomicRule) {
426 99
            $instance->cleanOperations($options);
427 99
            $instance->unifyAtomicOperands(true, $options);
428
429
            // $instance->dump(true);
430
431 99
            if (self::unify_atomic_operands == $step_to_stop_after ||
432 99
                self::remove_invalid_branches == $step_to_stop_before ) {
433 1
                return $instance;
434
            }
435
436 98
            $instance->cleanOperations($options);
437 98
            if (method_exists($instance, 'removeInvalidBranches')) {
438 98
                $instance->removeInvalidBranches($options);
439 98
            }
440 98
        }
441
442
        // $instance->dump(true);
443 98
        $instance->cleanOperations($options);
444
445
        // the root rule cannot be cleaned so we wrap it and apply a
446
        // last non recursive clean
447
        // TODO kind of monad|become|cese
448
        //@see https://github.com/jclaveau/php-logical-filter/issues/20
449 98
        if ($instance instanceof AndRule || $instance instanceof OrRule ) {
450 98
            if ( ! $instance->getOperands()) {
451 22
                return $instance;
452
            }
453
454 90
            $operands = (new AndRule([$instance]))
455 90
                ->cleanOperations($options, false)
456
                // ->dump(true)
457 90
                ->getOperands();
458
459 90
            if (1 == count($operands)) {
460 79
                $instance = reset($operands);
461 79
            }
462 90
        }
463
464
465 90
        if ($force_logical_core) {
466 36
            $instance = $instance->forceLogicalCore();
467
            // for the simplification status at
468 36
            foreach ($operands = $instance->getOperands() as $andOperand) {
469 36
                if ( ! $andOperand instanceof AndRule) {
470
                    throw new \LogicException(
471
                        "A rule is intended to be an and case: \n"
472
                        .$andOperand
473
                        ."\nof:\n"
474
                        .$instance
475
                    );
476
                }
477
478 36
                $andOperand->moveSimplificationStepForward(self::simplified, $options, true);
479 36
            }
480 36
            $instance->setOperands($operands);
481 36
            $instance->moveSimplificationStepForward(self::simplified, $options, true);
482
483 36
            $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
484
            // self::$simplification_cache[ $instance->getSemanticId().'-'.$options_id ] = $instance;
485 36
        }
486
487 90
        $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
488 90
        foreach ($cache_keys as $cache_key) {
489 90
            self::$simplification_cache[ $cache_key ] = $instance;
490 90
        }
491
492 90
        return $instance->copy();
493
    }
494
495
    /**
496
     * Indexes operands by their fields and operators. This sorting is
497
     * used during the simplification step.
498
     *
499
     * @return array The 3 dimensions array of operands: field > operator > i
500
     */
501 93 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...
502
    {
503 93
        $operandsByFields = [];
504 93
        foreach ($this->operands as $operand) {
505
506
            // Operation rules have no field but we need to keep them anyway
507 88
            $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...
508
509
            // For FilteredValue and FilteredKey
510 88
            $field = (string) $field;
511
512 88
            if ( ! isset($operandsByFields[ $field ])) {
513 88
                $operandsByFields[ $field ] = [];
514 88
            }
515
516 88
            if ( ! isset($operandsByFields[ $field ][ $operand::operator ])) {
517 88
                $operandsByFields[ $field ][ $operand::operator ] = [];
518 88
            }
519
520 88
            $operandsByFields[ $field ][ $operand::operator ][] = $operand;
521 93
        }
522
523 93
        return $operandsByFields;
524
    }
525
526
    /**
527
     * Indexes operands by their fields and operators. This sorting is
528
     * used during the simplification step.
529
     *
530
     * @return array The 3 dimensions array of operands: field > operator > i
531
     */
532 99 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...
533
    {
534 99
        $operandsByFields = [];
535 99
        foreach ($operands as $operand) {
536
537
            // Operation rules have no field but we need to keep them anyway
538 99
            $field = method_exists($operand, 'getField') ? $operand->getField() : '';
539
540
            // For FilteredValue and FilteredKey
541 99
            $field = (string) $field;
542
543 99
            if ( ! isset($operandsByFields[ $field ])) {
544 99
                $operandsByFields[ $field ] = [];
545 99
            }
546
547 99
            if ( ! isset($operandsByFields[ $field ][ $operand::operator ])) {
548 99
                $operandsByFields[ $field ][ $operand::operator ] = [];
549 99
            }
550
551 99
            $operandsByFields[ $field ][ $operand::operator ][] = $operand;
552 99
        }
553
554 99
        return $operandsByFields;
555
    }
556
557
    /**
558
     * Clones the rule and its operands.
559
     *
560
     * @return AbstractOperationRule A copy of the current instance with copied operands.
561
     */
562 106
    final public function copy()
563
    {
564 106
        return clone $this;
565
    }
566
567
    /**
568
     * Make a deep copy of operands
569
     */
570 106
    public function __clone()
571
    {
572 106
        foreach ($this->operands as $operand_id => &$operand) {
573 91
            $this->operands[$operand_id] = $operand->copy();
574 106
        }
575 106
    }
576
577
    /**
578
     */
579 102
    public function isNormalizationAllowed(array $current_simplification_options)
580
    {
581 102
        return true;
582
    }
583
584
    /**
585
     * Returns an operand based on its position
586
     *
587
     * @return AbstractRule|null The operand if it exists or null
588
     */
589 49
    protected function getOperandAt($index=0)
590
    {
591 49
        $operands = array_values($this->operands);
592 49
        if (isset($operands[$index])) {
593 49
            return $operands[$index];
594
        }
595 1
    }
596
597
    /**/
598
}
599