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

AbstractOperationRule::getSimplificationStep()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
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 156
    public function __construct( array $operands=[] )
48
    {
49 156
        $this->setOperands( $operands );
50 156
        $this->flushCache();
51 156
    }
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 156
    public function addOperand( AbstractRule $new_operand )
69
    {
70 156
        if ( ! isset($this->operands[ $id = $new_operand->getSemanticId() ])) {
71 156
            $this->operands[ $id ] = $new_operand;
72
73 156
            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 96
                $this->current_simplification_step = null;
75 96
            }
76
77 156
            $this->flushCache();
78 156
        }
79
80 156
        return $this;
81
    }
82
83
    /**
84
     * @return array
85
     */
86 151
    public function getOperands()
87
    {
88 151
        return array_values( $this->operands );
89
    }
90
91
    /**
92
     * @return $this
93
     */
94 157
    public function setOperands(array $operands)
95
    {
96 157
        $this->operands = [];
97 157
        foreach ($operands as $operand) {
98 122
            $this->addOperand($operand);
99 157
        }
100
101 157
        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 string $this
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...
109
     */
110 1
    public function renameFields($renamings)
111
    {
112 1
        foreach ($this->operands as $operand) {
113 1
            if (method_exists($operand, 'renameField')) {
114 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...
115 1
            }
116
            else {
117 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...
118
            }
119 1
        }
120
121
        // TODO flush cache only in case of change?
122 1
        $this->flushCache();
123
124 1
        return $this;
125
    }
126
127
    /**
128
     * @param string $step_to_go_to
129
     * @param array  $simplification_options
130
     * @param bool   $force
131
     */
132 102
    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...
133
    {
134 102
        if ( ! in_array($step_to_go_to, self::simplification_steps)) {
135
            throw new \InvalidArgumentException(
136
                "Invalid simplification step to go to: ".$step_to_go_to
137
            );
138
        }
139
140
        // if ($this->isNormalizationAllowed($simplification_options) && !$force && $this->current_simplification_step != null) {
141 102
        if ( ! $force && null !== $this->current_simplification_step) {
142 23
            $steps_indices = array_flip(self::simplification_steps);
143
144 23
            $current_index = $steps_indices[ $this->current_simplification_step ];
145 23
            $target_index  = $steps_indices[ $step_to_go_to ];
146
147 23
            if ( $current_index >= $target_index ) {
148
                // allow recall of previous step without going back
149 4
                return;
150
            }
151 21
            elseif ( $current_index < $target_index - 1 ) {
152
                throw new \LogicException(
153
                    "$step_to_go_to MUST be fullfilled after " . self::simplification_steps[$target_index - 1]
154
                    . " instead of the current step: " . $this->current_simplification_step
155
                    ."\nfor: " . $this
156
                );
157
            }
158 21
        }
159
160 102
        $this->current_simplification_step = $step_to_go_to;
161 102
    }
162
163
    /**
164
     * @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...
165
     */
166 28
    public function getSimplificationStep()
167
    {
168 28
        return $this->current_simplification_step;
169
    }
170
171
    /**
172
     * Checks if a simplification step is reached.
173
     *
174
     * @param  string $step
175
     *
176
     * @return bool
177
     */
178 42
    public function simplicationStepReached($step)
179
    {
180 42
        if ( ! in_array($step, self::simplification_steps)) {
181
            throw new \InvalidArgumentException(
182
                "Invalid simplification step: ".$step
183
            );
184
        }
185
186 42
        if (null === $this->current_simplification_step) {
187
            return false;
188
        }
189
190 42
        $steps_indices = array_flip(self::simplification_steps);
191
192 42
        $current_index = $steps_indices[ $this->current_simplification_step ];
193 42
        $step_index    = $steps_indices[ $step ];
194
195 42
        return $current_index >= $step_index;
196
    }
197
198
    /**
199
     * Replace NotRule objects by the negation of their operands.
200
     *
201
     * @return AbstractOperationRule $this or a $new rule with negations removed
202
     */
203 99
    public function removeNegations(array $contextual_options)
204
    {
205 99
        if ( ! $this->isNormalizationAllowed($contextual_options)) {
206 28
            return $this;
207
        }
208
209 99
        $this->moveSimplificationStepForward(self::remove_negations, $contextual_options);
210
211 99
        $new_rule = $this;
212 99
        if ($operands = $this->operands) {
213 95
            foreach ($operands as $i => $operand) {
214 95
                if ($operand instanceof NotRule) {
215 27
                    $operands[$i] = $operand->negateOperand($contextual_options);
216 27
                }
217
218 95
                if ($operands[$i] instanceof AbstractOperationRule) {
219 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...
220 59
                }
221 95
            }
222
223 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...
224 95
        }
225
226 99
        return $new_rule;
227
    }
228
229
    /**
230
     * Operation cleaning consists of removing operation with one operand
231
     * and removing operations having a same type of operation as operand.
232
     *
233
     * This operation has been required between every steps until now.
234
     *
235
     * @toopt Trigger cleaning requirement during simplification steps
236
     *
237
     * @param  array    $simplification_options
238
     * @param  bool     $recurse
239
     *
240
     * @return AbstractOperationRule
241
     */
242 99
    public function cleanOperations(array $simplification_options, $recurse=true)
243
    {
244 99
        if ($recurse) {
245 99
            foreach ($this->operands as $i => $operand) {
246
                if (     $operand instanceof AbstractOperationRule
247 99
                    && ! $operand instanceof InRule
248 99
                    && ! $operand instanceof NotEqualRule
249 99
                    && ! $operand instanceof NotInRule
250 99
                ) {
251 83
                    $this->operands[$i] = $operand->cleanOperations($simplification_options);
252 83
                }
253 99
            }
254 99
        }
255
256 99
        if ($this instanceof NotRule) {
257 8
            return $this;
258
        }
259
260 99
        $is_modified = true;
261 99
        while ($is_modified) {
262 99
            $is_modified = false;
263
264 99
            if ($this->removeMonooperandOperationsOperands($simplification_options)) {
265 76
                $is_modified = true;
266 76
            }
267
268 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...
269 59
                $is_modified = true;
270 59
            }
271 99
        }
272
273 99
        return $this;
274
    }
275
276
    /**
277
     * If a child is an OrRule or an AndRule and has only one child,
278
     * replace it by its child.
279
     *
280
     * @used-by removeSameOperationOperands() Ping-pong recursion
281
     *
282
     * @return bool If something has been simplified or not
283
     */
284 99
    public function removeMonooperandOperationsOperands(array $simplification_options)
285
    {
286 99
        foreach ($this->operands as $i => $operand) {
287 99
            if ( ! $operand instanceof AbstractOperationRule || $operand instanceof NotRule) {
288 96
                continue;
289
            }
290
291 98
            if ($operand instanceof InRule && ! $operand->isNormalizationAllowed($simplification_options)) {
292 20
                $count = count($operand->getPossibilities());
293 20
            }
294
            else {
295 98
                $count = count($operand->getOperands());
296
            }
297
298
            if (
299 98
                    ($operand instanceof AndRule || $operand instanceof OrRule)
300 98
                && 1 == $count
301 98
            ) {
302 76
                $sub_operands       = $operand->getOperands();
303 76
                $this->operands[$i] = reset($sub_operands);
304 76
                $has_been_changed   = true;
305 76
            }
306 99
        }
307
308 99
        return ! empty($has_been_changed);
309
    }
310
311
    /**
312
     * Removes duplicates between the current AbstractOperationRule.
313
     *
314
     * @return AbstractOperationRule the simplified rule
315
     */
316 99
    public function unifyAtomicOperands($simplification_strategy_step = false, array $contextual_options)
317
    {
318 99
        if ($simplification_strategy_step) {
319 99
            $this->moveSimplificationStepForward( self::unify_atomic_operands, $contextual_options );
320 99
        }
321
322
        // $this->dump(true);
323
324 99
        if ( ! $this->isNormalizationAllowed($contextual_options)) {
325 26
            return $this;
326
        }
327
328 99
        $operands = $this->getOperands();
329 99
        foreach ($operands as &$operand) {
330 99
            if ($operand instanceof AbstractOperationRule) {
331 64
                $operand = $operand->unifyAtomicOperands($simplification_strategy_step, $contextual_options);
332 64
            }
333 99
        }
334
335 99
        $class = get_class($this);
336
337 99
        $operandsByFields = $class::groupOperandsByFieldAndOperator_static($operands);
338 99
        $operandsByFields = $class::simplifySameOperands($operandsByFields);
339
340 99
        if ($this instanceof AndRule) {
341
            // unifiying operands of different types
342 98
            $operandsByFields = $class::simplifyDifferentOperands($operandsByFields);
343 98
        }
344
345
        // Remove the index by fields and operators
346 99
        $unifiedOperands = [];
347 99
        foreach ($operandsByFields as $field => $operandsByOperator) {
348 96
            foreach ($operandsByOperator as $operator => $operands) {
349
                try {
350 96
                    $unifiedOperands = array_merge($unifiedOperands, $operands);
351
                }
352 96
                catch (\Exception $e) {
353
                    VisibilityViolator::setHiddenProperty(
354
                        $e, 'message',
355
                        $e->getMessage() . "\n" . var_export($operandsByOperator, true)
356
                    );
357
358
                    throw $e;
359
                }
360 96
            }
361 99
        }
362
363 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...
364
    }
365
366
    private static $simplification_cache = [];
367
368
    /**
369
     * Simplify the current OperationRule.
370
     * + If an OrRule or an AndRule contains only one operand, it's equivalent
371
     *   to it.
372
     * + If an OrRule has an other OrRule as operand, they can be merged
373
     * + If an AndRule has an other AndRule as operand, they can be merged
374
     *
375
     * @param  array $options stop_after | stop_before | force_logical_core
376
     *
377
     * @return AbstractRule the simplified rule
378
     */
379 102
    final public function simplify($options=[])
380
    {
381 102
        $step_to_stop_before = ! empty($options['stop_before'])        ? $options['stop_before'] : null;
382 102
        $step_to_stop_after  = ! empty($options['stop_after'])         ? $options['stop_after']  : null;
383 102
        $force_logical_core  = ! empty($options['force_logical_core']) ? $options['force_logical_core'] : false;
384
385 102
        if ($step_to_stop_before && ! in_array($step_to_stop_before, self::simplification_steps)) {
386
            throw new \InvalidArgumentException(
387
                "Invalid simplification step to stop at: ".$step_to_stop_before
388
            );
389
        }
390
391 102
        ksort($options);
392 102
        $options_id = hash('md4', serialize($options));
393
394 102
        $id = $this->getSemanticId().'-'.$options_id;
395 102
        if (isset(self::$simplification_cache[$id])) {
396 21
            return self::$simplification_cache[$id]->copy();
397
        }
398
399 99
        $this->flushCache();
400
401 99
        $cache_keys = [$id];
402
403
        // $this->dump(true);
404 99
        $this->cleanOperations($options);
405
        // $this->dump(true);
406 99
        $instance = $this->unifyAtomicOperands(false, $options);
407
408 99
        $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
409
410 99
        if (self::remove_negations == $step_to_stop_before) {
411
            return $instance;
412
        }
413
414
        // $this->dump(!true);
415 99
        $instance = $instance->removeNegations($options);
416
417
        // $instance->dump(true);
418
419 99
        if (self::remove_negations == $step_to_stop_after ||
420 99
            self::rootify_disjunctions == $step_to_stop_before ) {
421
            return $instance;
422
        }
423
424
        // $instance->dump(true);
425
426 99
        $instance->cleanOperations($options);
427 99
        $instance = $instance->rootifyDisjunctions($options);
428
429
        // $instance->dump(true);
430
431 99
        if (self::rootify_disjunctions == $step_to_stop_after ||
432 99
            self::unify_atomic_operands == $step_to_stop_before ) {
433
            return $instance;
434
        }
435
436 99
        if ( ! $instance instanceof AbstractAtomicRule) {
437 99
            $instance->cleanOperations($options);
438 99
            $instance->unifyAtomicOperands(true, $options);
439
440
            // $instance->dump(true);
441
442 99
            if (self::unify_atomic_operands == $step_to_stop_after ||
443 99
                self::remove_invalid_branches == $step_to_stop_before ) {
444 1
                return $instance;
445
            }
446
447 98
            $instance->cleanOperations($options);
448 98
            if (method_exists($instance, 'removeInvalidBranches')) {
449 98
                $instance->removeInvalidBranches($options);
450 98
            }
451 98
        }
452
453
        // $instance->dump(true);
454 98
        $instance->cleanOperations($options);
455
456
        // the root rule cannot be cleaned so we wrap it and apply a
457
        // last non recursive clean
458
        // TODO kind of monad|become|cese
459
        //@see https://github.com/jclaveau/php-logical-filter/issues/20
460 98
        if ($instance instanceof AndRule || $instance instanceof OrRule ) {
461 98
            if ( ! $instance->getOperands()) {
462 22
                return $instance;
463
            }
464
465 90
            $operands = (new AndRule([$instance]))
466 90
                ->cleanOperations($options, false)
467
                // ->dump(true)
468 90
                ->getOperands();
469
470 90
            if (1 == count($operands)) {
471 79
                $instance = reset($operands);
472 79
            }
473 90
        }
474
475 90
        if ($force_logical_core) {
476
            // Adding a minimal case structure  already considered as
477
            // 'simplified' can only be done here
478 10
            $instance = $instance->addMinimalCase();
479 10
        }
480
481 90
        $cache_keys[] = $instance->getSemanticId().'-'.$options_id;
482 90
        foreach ($cache_keys as $cache_key) {
483 90
            self::$simplification_cache[ $cache_key ] = $instance;
484 90
        }
485
486 90
        return $instance->copy();
487
    }
488
489
    /**
490
     * Indexes operands by their fields and operators. This sorting is
491
     * used during the simplification step.
492
     *
493
     * @return array The 3 dimensions array of operands: field > operator > i
494
     */
495 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...
496
    {
497 93
        $operandsByFields = [];
498 93
        foreach ($this->operands as $operand) {
499
500
            // Operation rules have no field but we need to keep them anyway
501 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...
502
503
            // For FilteredValue and FilteredKey
504 88
            $field = (string) $field;
505
506 88
            if ( ! isset($operandsByFields[ $field ])) {
507 88
                $operandsByFields[ $field ] = [];
508 88
            }
509
510 88
            if ( ! isset($operandsByFields[ $field ][ $operand::operator ])) {
511 88
                $operandsByFields[ $field ][ $operand::operator ] = [];
512 88
            }
513
514 88
            $operandsByFields[ $field ][ $operand::operator ][] = $operand;
515 93
        }
516
517 93
        return $operandsByFields;
518
    }
519
520
    /**
521
     * Indexes operands by their fields and operators. This sorting is
522
     * used during the simplification step.
523
     *
524
     * @return array The 3 dimensions array of operands: field > operator > i
525
     */
526 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...
527
    {
528 99
        $operandsByFields = [];
529 99
        foreach ($operands as $operand) {
530
531
            // Operation rules have no field but we need to keep them anyway
532 99
            $field = method_exists($operand, 'getField') ? $operand->getField() : '';
533
534
            // For FilteredValue and FilteredKey
535 99
            $field = (string) $field;
536
537 99
            if ( ! isset($operandsByFields[ $field ])) {
538 99
                $operandsByFields[ $field ] = [];
539 99
            }
540
541 99
            if ( ! isset($operandsByFields[ $field ][ $operand::operator ])) {
542 99
                $operandsByFields[ $field ][ $operand::operator ] = [];
543 99
            }
544
545 99
            $operandsByFields[ $field ][ $operand::operator ][] = $operand;
546 99
        }
547
548 99
        return $operandsByFields;
549
    }
550
551
    /**
552
     * Clones the rule and its operands.
553
     *
554
     * @return AbstractOperationRule A copy of the current instance with copied operands.
555
     */
556 109
    final public function copy()
557
    {
558 109
        return clone $this;
559
    }
560
561
    /**
562
     * Make a deep copy of operands
563
     */
564 109
    public function __clone()
565
    {
566 109
        foreach ($this->operands as $operand_id => &$operand) {
567 91
            $this->operands[$operand_id] = $operand->copy();
568 109
        }
569 109
    }
570
571
    /**
572
     */
573 101
    public function isNormalizationAllowed(array $current_simplification_options)
574
    {
575 101
        return true;
576
    }
577
578
    /**
579
     * Returns an operand based on its position
580
     *
581
     * @return AbstractRule|null The operand if it exists or null
582
     */
583 51
    protected function getOperandAt($index=0)
584
    {
585 51
        $operands = array_values($this->operands);
586 51
        if (isset($operands[$index])) {
587 51
            return $operands[$index];
588
        }
589 1
    }
590
591
    /**/
592
}
593