Total Complexity | 152 |
Total Lines | 758 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like AndRule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AndRule, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
9 | class AndRule extends AbstractOperationRule |
||
10 | { |
||
11 | /** @var string operator */ |
||
12 | const operator = 'and'; |
||
13 | |||
14 | /** |
||
15 | * Replace all the OrRules of the RuleTree by one OrRule at its root. |
||
16 | * |
||
17 | * @todo rename as RootifyDisjunjctions? |
||
18 | * @todo return $this (implements a Rule monad?) |
||
19 | * |
||
20 | * @return OrRule copied operands with one OR at its root |
||
21 | */ |
||
22 | public function rootifyDisjunctions(array $simplification_options) |
||
23 | { |
||
24 | if (!$this->isNormalizationAllowed($simplification_options)) |
||
25 | return $this; |
||
|
|||
26 | |||
27 | $this->moveSimplificationStepForward( self::rootify_disjunctions, $simplification_options ); |
||
28 | |||
29 | $upLiftedOperands = []; |
||
30 | foreach ($this->getOperands() as $operand) { |
||
31 | $operand = $operand->copy(); |
||
32 | if ($operand instanceof AbstractOperationRule) |
||
33 | $operand = $operand->rootifyDisjunctions($simplification_options); |
||
34 | |||
35 | $upLiftedOperands[] = $operand; |
||
36 | } |
||
37 | |||
38 | // If the AndRule doesn't contain any OrRule , there is nothing to uplift |
||
39 | if (!array_filter($upLiftedOperands, function($operand) { |
||
40 | return $operand instanceof OrRule; |
||
41 | })) { |
||
42 | return new AndRule($upLiftedOperands); |
||
43 | } |
||
44 | |||
45 | $firstAndOperand = new AndRule(); |
||
46 | |||
47 | // This OrRule should contain only AndRules during its generation |
||
48 | $upLiftedOr = new OrRule([ |
||
49 | $firstAndOperand |
||
50 | ]); |
||
51 | |||
52 | // var_dump($upLiftedOperands); |
||
53 | // $this->dump(true); |
||
54 | |||
55 | foreach ($upLiftedOperands as $i => $operand) { |
||
56 | |||
57 | if ($operand instanceof NotRule) { |
||
58 | if ( ($operand instanceof NotEqualRule || $operand instanceof NotInRule) |
||
59 | && ! $operand->isNormalizationAllowed($simplification_options) |
||
60 | ) { |
||
61 | foreach ($upLiftedOr->getOperands() as $upLifdtedOperand) { |
||
62 | $upLifdtedOperand->addOperand( $operand->copy() ); |
||
63 | } |
||
64 | } |
||
65 | else { |
||
66 | throw new \LogicException( |
||
67 | "Rootifying disjunctions MUST be done after negations removal instead of '".$operand."' \n" |
||
68 | .$operand |
||
69 | ); |
||
70 | } |
||
71 | } |
||
72 | elseif ($operand instanceof OrRule && $operand->isNormalizationAllowed($simplification_options)) { |
||
73 | |||
74 | // If an operand is an Or, me transform the current |
||
75 | // (A' || A") && (B') <=> (A' && B') || (A" && B'); |
||
76 | // (A' || A") && (B' || B") <=> (A' && B') || (A' && B") || (A" && B') || (A" && B"); |
||
77 | // (A' || A") && (B' || B") && (C' || C") <=> |
||
78 | // (A' && B' && C') || (A' && B' && C") || (A' && B" && C') || (A' && B" && C") |
||
79 | // || (A" && B' && C') || (A" && B' && C") || (A" && B" && C') || (A" && B" && C"); |
||
80 | $newUpLiftedOr = new OrRule; |
||
81 | foreach ($operand->getOperands() as $subOperand) { |
||
82 | foreach ($upLiftedOr->getOperands() as $upLiftedOrSubOperand) { |
||
83 | $newUpLiftedOrSubOperand = $upLiftedOrSubOperand->copy(); |
||
84 | $newUpLiftedOrSubOperand->addOperand( $subOperand->copy() ); |
||
85 | if ($newUpLiftedOrSubOperand->simplify($simplification_options)->hasSolution($simplification_options)) |
||
86 | $newUpLiftedOr->addOperand( $newUpLiftedOrSubOperand ); |
||
87 | } |
||
88 | } |
||
89 | |||
90 | $upLiftedOr = $newUpLiftedOr; |
||
91 | } |
||
92 | else { |
||
93 | // append the operand to all the operands of the $upLiftedOr |
||
94 | foreach ($upLiftedOr->getOperands() as $upLifdtedOperand) { |
||
95 | if (!$upLifdtedOperand instanceof AndRule) { |
||
96 | throw new \LogicException( |
||
97 | "Operands of the uplifted OrRule MUST be AndRules during" |
||
98 | ."the combination." |
||
99 | ); |
||
100 | } |
||
101 | |||
102 | $upLifdtedOperand->addOperand( $operand->copy() ); |
||
103 | } |
||
104 | } |
||
105 | } |
||
106 | |||
107 | return $upLiftedOr; |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * @param array $options + show_instance=false Display the operator of the rule or its instance id |
||
112 | * |
||
113 | * @return array |
||
114 | * |
||
115 | * @todo same as OrRule |
||
116 | */ |
||
117 | public function toArray(array $options=[]) |
||
118 | { |
||
119 | $default_options = [ |
||
120 | 'show_instance' => false, |
||
121 | 'sort_operands' => false, |
||
122 | 'semantic' => false, |
||
123 | ]; |
||
124 | foreach ($default_options as $default_option => &$default_value) { |
||
125 | if (!isset($options[ $default_option ])) |
||
126 | $options[ $default_option ] = $default_value; |
||
127 | } |
||
128 | |||
129 | if (!$options['show_instance'] && !empty($this->cache['array'])) |
||
130 | return $this->cache['array']; |
||
131 | |||
132 | $operands_as_array = [ |
||
133 | $options['show_instance'] ? $this->getInstanceId() : self::operator, |
||
134 | ]; |
||
135 | |||
136 | $operands = $this->operands; |
||
137 | if ($options['semantic']) { |
||
138 | // Semantic array: ['operator', 'semantic_id_of_operand1', 'semantic_id_of_operand2', ...] |
||
139 | // with sorted semantic ids |
||
140 | $operands_semantic_ids = array_keys($operands); |
||
141 | sort($operands_semantic_ids); |
||
142 | return array_merge( |
||
143 | [self::operator], |
||
144 | $operands_semantic_ids |
||
145 | ); |
||
146 | } |
||
147 | else { |
||
148 | foreach ($operands as $operand) |
||
149 | $operands_as_array[] = $operand->toArray($options); |
||
150 | |||
151 | if (!$options['show_instance']) |
||
152 | return $this->cache['array'] = $operands_as_array; |
||
153 | else |
||
154 | return $operands_as_array; |
||
155 | } |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | */ |
||
160 | public function toString(array $options=[]) |
||
161 | { |
||
162 | $operator = self::operator; |
||
163 | if (!$this->operands) { |
||
164 | return $this->cache['string'] = "['{$operator}']"; |
||
165 | } |
||
166 | |||
167 | $indent_unit = isset($options['indent_unit']) ? $options['indent_unit'] : ''; |
||
168 | $line_break = $indent_unit ? "\n" : ''; |
||
169 | |||
170 | $out = "['{$operator}',$line_break"; |
||
171 | |||
172 | foreach ($this->operands as $operand) { |
||
173 | $out .= implode("\n", array_map(function($line) use (&$indent_unit) { |
||
174 | return $indent_unit.$line; |
||
175 | }, explode("\n", $operand->toString($options)) )) . ",$line_break"; |
||
176 | } |
||
177 | |||
178 | $out .= ']'; |
||
179 | |||
180 | return $this->cache['string'] = $out; |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Remove AndRules operands of AndRules |
||
185 | */ |
||
186 | public function removeSameOperationOperands() |
||
187 | { |
||
188 | foreach ($this->operands as $i => $operand) { |
||
189 | if ( ! is_a($operand, AndRule::class)) |
||
190 | continue; |
||
191 | |||
192 | if ( ! $operands = $operand->getOperands()) |
||
193 | continue; |
||
194 | |||
195 | // Id AND is an operand on AND they can be merge (and the same with OR) |
||
196 | foreach ($operands as $sub_operand) { |
||
197 | $this->addOperand( $sub_operand->copy() ); |
||
198 | } |
||
199 | unset($this->operands[$i]); |
||
200 | |||
201 | // possibility of mono-operand or dupicates |
||
202 | $has_been_changed = true; |
||
203 | } |
||
204 | |||
205 | return !empty($has_been_changed); |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * Removes rule branches that cannot produce result like: |
||
210 | * A = 1 || (B < 2 && B > 3) <=> A = 1 |
||
211 | * |
||
212 | * @return AndRule $this |
||
213 | */ |
||
214 | public function removeInvalidBranches(array $simplification_options) |
||
308 | } |
||
309 | |||
310 | /** |
||
311 | * Checks if a simplified AndRule has incompatible operands like: |
||
312 | * + a = 3 && a > 4 |
||
313 | * + a = 3 && a < 2 |
||
314 | * + a > 3 && a < 2 |
||
315 | * |
||
316 | * @return bool If the AndRule can have a solution or not |
||
317 | */ |
||
318 | public function hasSolution(array $contextual_options=[]) |
||
334 | } |
||
335 | |||
336 | |||
337 | /** |
||
338 | * + if A > 2 && A > 1 <=> A > 2 |
||
339 | * + if A < 2 && A < 1 <=> A < 1 |
||
340 | */ |
||
341 | protected static function simplifySameOperands(array $operandsByFields) |
||
452 | } |
||
453 | |||
454 | /** |
||
455 | */ |
||
456 | protected static function simplifyDifferentOperands(array $operandsByFields) |
||
489 | } |
||
490 | |||
491 | /** |
||
492 | * + if A = 2 && A > 1 <=> A = 2 |
||
493 | * + if A = 2 && A < 4 <=> A = 2 |
||
494 | */ |
||
495 | protected static function simplifyDifferentOperandsForField($field, array $operandsByOperator) |
||
496 | { |
||
497 | // EqualRule comparisons |
||
498 | if (!empty($operandsByOperator[ EqualRule::operator ])) { |
||
499 | |||
500 | $equalRule = reset( $operandsByOperator[ EqualRule::operator ] ); |
||
501 | |||
502 | if (!empty($operandsByOperator[ NotEqualRule::operator ])) { |
||
503 | foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $not_equal_rule) { |
||
504 | |||
505 | if ($equalRule->getValue() !== null) { |
||
506 | if ($not_equal_rule->getValue() === null) // means if exists <=> equals something |
||
507 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
508 | elseif ($not_equal_rule->getValue() != $equalRule->getValue()) |
||
509 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
510 | } |
||
511 | elseif ($equalRule->getValue() === null ) { |
||
512 | if ($not_equal_rule->getValue() !== null) |
||
513 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
514 | // else we let the "equal null" and the "not equal null" for the romeInvalidBranches step |
||
515 | } |
||
516 | } |
||
517 | } |
||
518 | |||
519 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
520 | |||
521 | $aboveRule = reset($operandsByOperator[ AboveRule::operator ]); |
||
522 | if ($equalRule->getValue() !== null && $aboveRule->getMinimum() < $equalRule->getValue()) |
||
523 | unset($operandsByOperator[ AboveRule::operator ]); |
||
524 | } |
||
525 | |||
526 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
527 | |||
528 | $belowRule = reset($operandsByOperator[ BelowRule::operator ]); |
||
529 | if ($equalRule->getValue() !== null && $belowRule->getMaximum() > $equalRule->getValue()) |
||
530 | unset($operandsByOperator[ BelowRule::operator ]); |
||
531 | } |
||
532 | |||
533 | if (!empty($operandsByOperator[ InRule::operator ])) { |
||
534 | |||
535 | $possibilities = reset($operandsByOperator[ InRule::operator ])->getPossibilities(); |
||
536 | |||
537 | if (in_array($equalRule->getValue(), $possibilities)) { |
||
538 | unset($operandsByOperator[ InRule::operator ]); |
||
539 | } |
||
540 | else { |
||
541 | // We flush possibilities of the InRule |
||
542 | // TODO Replace it by a FalseRule |
||
543 | $operandsByOperator[ InRule::operator ][0]->setPossibilities([]); |
||
544 | // and also remove the equal rule to shorten the reste of the simplification process |
||
545 | unset($operandsByOperator[ EqualRule::operator ]); |
||
546 | } |
||
547 | } |
||
548 | |||
549 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
550 | |||
551 | $notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
||
552 | if (in_array($equalRule->getValue(), $notInRule->getPossibilities())) { |
||
553 | // ['field', '=', 4] && ['field', '!in', [4]...] <=> false |
||
554 | return []; |
||
555 | } |
||
556 | else { |
||
557 | unset($operandsByOperator[ NotInRule::operator ]); |
||
558 | } |
||
559 | // $notInRule->dump(true); |
||
560 | } |
||
561 | |||
562 | if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) { |
||
563 | |||
564 | $belowOrEqualRule = reset($operandsByOperator[ BelowOrEqualRule::operator ]); |
||
565 | if ($equalRule->getValue() <= $belowOrEqualRule->getMaximum()) { |
||
566 | unset($operandsByOperator[ BelowOrEqualRule::operator ]); |
||
567 | } |
||
568 | else { |
||
569 | // ['field', '=', 4] && ['field', '<=', [3]...] <=> false |
||
570 | return []; |
||
571 | } |
||
572 | } |
||
573 | |||
574 | if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) { |
||
575 | |||
576 | $aboveOrEqualRule = reset($operandsByOperator[ AboveOrEqualRule::operator ]); |
||
577 | if ($equalRule->getValue() >= $aboveOrEqualRule->getMinimum()) { |
||
578 | unset($operandsByOperator[ AboveOrEqualRule::operator ]); |
||
579 | } |
||
580 | else { |
||
581 | // ['field', '=', 4] && ['field', '<=', [3]...] <=> false |
||
582 | return []; |
||
583 | } |
||
584 | } |
||
585 | } |
||
586 | |||
587 | // NotEqualRule null comparisons |
||
588 | if (!empty($operandsByOperator[ NotEqualRule::operator ])) { |
||
589 | if (!empty($operandsByOperator[ NotEqualRule::operator ])) { |
||
590 | foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $notEqualRule) { |
||
591 | |||
592 | if ($notEqualRule->getValue() === null) { |
||
593 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
594 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
595 | } |
||
596 | |||
597 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
598 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
599 | } |
||
600 | |||
601 | if (!empty($operandsByOperator[ EqualRule::operator ])) { |
||
602 | if (reset($operandsByOperator[ EqualRule::operator ])->getValue() !== null) |
||
603 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
604 | } |
||
605 | } |
||
606 | else { |
||
607 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
608 | if ($operandsByOperator[ AboveRule::operator ][0]->getMinimum() >= $notEqualRule->getValue()) |
||
609 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
610 | } |
||
611 | |||
612 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
613 | if ($operandsByOperator[ BelowRule::operator ][0]->getMaximum() <= $notEqualRule->getValue()) |
||
614 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
615 | } |
||
616 | } |
||
617 | |||
618 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
619 | $notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
||
620 | if (!in_array($notEqualRule->getValue(), $notInRule->getPossibilities())) { |
||
621 | // TODO Replace it by a FalseRule |
||
622 | $operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
||
623 | array_merge($notInRule->getPossibilities(), [$notEqualRule->getValue()]) |
||
624 | ); |
||
625 | } |
||
626 | |||
627 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
628 | } |
||
629 | |||
630 | if (!empty($operandsByOperator[ InRule::operator ])) { |
||
631 | $inRule = reset($operandsByOperator[ InRule::operator ]); |
||
632 | |||
633 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
634 | array_diff($inRule->getPossibilities(), [$notEqualRule->getValue()]) |
||
635 | ); |
||
636 | } |
||
637 | } |
||
638 | } |
||
639 | } |
||
640 | |||
641 | // Comparison between InRules and NotInRules |
||
642 | // This is an optimization to avoid NotIn explosion |
||
643 | if (!empty($operandsByOperator[ InRule::operator ])) { |
||
644 | $inRule = $operandsByOperator[ InRule::operator ][0]; |
||
645 | |||
646 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
647 | $notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
||
648 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
649 | array_diff( $inRule->getPossibilities(), $notInRule->getPossibilities()) |
||
650 | ); |
||
651 | unset($operandsByOperator[ NotInRule::operator ]); |
||
652 | } |
||
653 | |||
654 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
655 | $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getMaximum(); |
||
656 | |||
657 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
658 | array_filter( $inRule->getPossibilities(), function ($possibility) use ($upper_limit) { |
||
659 | return $possibility < $upper_limit; |
||
660 | } ) |
||
661 | ); |
||
662 | |||
663 | unset($operandsByOperator[ BelowRule::operator ]); |
||
664 | } |
||
665 | |||
666 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
667 | $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum(); |
||
668 | |||
669 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
670 | array_filter( $inRule->getPossibilities(), function ($possibility) use ($lower_limit) { |
||
671 | return $possibility > $lower_limit; |
||
672 | } ) |
||
673 | ); |
||
674 | |||
675 | unset($operandsByOperator[ AboveRule::operator ]); |
||
676 | } |
||
677 | } |
||
678 | |||
679 | // Comparison between NotInRules and > or < |
||
680 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
681 | $notInRule = $operandsByOperator[ NotInRule::operator ][0]; |
||
682 | |||
683 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
684 | $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit(); |
||
685 | |||
686 | $operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
||
687 | array_filter( $notInRule->getPossibilities(), function ($possibility) use ($upper_limit) { |
||
688 | return $possibility < $upper_limit; |
||
689 | } ) |
||
690 | ); |
||
691 | } |
||
692 | |||
693 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
694 | $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum(); |
||
695 | |||
696 | $operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
||
697 | array_filter( $notInRule->getPossibilities(), function ($possibility) use ($lower_limit) { |
||
698 | return $possibility > $lower_limit; |
||
699 | } ) |
||
700 | ); |
||
701 | } |
||
702 | } |
||
703 | |||
704 | // Comparison between <= and > or < |
||
705 | if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) { |
||
706 | $belowOrEqualRule = $operandsByOperator[ BelowOrEqualRule::operator ][0]; |
||
707 | |||
708 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
709 | $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit(); |
||
710 | |||
711 | if ($belowOrEqualRule->getMaximum() >= $upper_limit) { |
||
712 | // [field < 3] && [field <= 3] |
||
713 | // [field < 3] && [field <= 4] |
||
714 | unset($operandsByOperator[ BelowOrEqualRule::operator ][0]); |
||
715 | } |
||
716 | else { |
||
717 | // [field < 3] && [field <= 2] |
||
718 | unset($operandsByOperator[ BelowRule::operator ][0]); |
||
719 | } |
||
720 | } |
||
721 | |||
722 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
723 | $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getLowerLimit(); |
||
724 | |||
725 | if ($belowOrEqualRule->getMaximum() <= $lower_limit) { |
||
726 | // [field > 3] && [field <= 2] <=> false |
||
727 | return []; |
||
728 | } |
||
729 | } |
||
730 | |||
731 | if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) { |
||
732 | $minimum = reset($operandsByOperator[ AboveOrEqualRule::operator ])->getMinimum(); |
||
733 | |||
734 | if ($belowOrEqualRule->getMaximum() < $minimum) { |
||
735 | // [field <= 3] && [field >= 4] <=> false |
||
736 | return []; |
||
737 | } |
||
738 | elseif ($belowOrEqualRule->getMaximum() == $minimum) { |
||
739 | // [field <= 3] && [field >= 3] <=> [field = 3] |
||
740 | unset($operandsByOperator[ BelowOrEqualRule::operator ]); |
||
741 | unset($operandsByOperator[ AboveOrEqualRule::operator ]); |
||
742 | $operandsByOperator[ EqualRule::operator ][] = new EqualRule($field, $minimum); |
||
743 | |||
744 | if (count($operandsByOperator[ EqualRule::operator ]) > 1) { |
||
745 | $operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator); |
||
746 | } |
||
747 | } |
||
748 | } |
||
749 | } |
||
750 | |||
751 | return $operandsByOperator; |
||
752 | } |
||
753 | |||
754 | /** |
||
755 | * This method is meant to be used during simplification that would |
||
756 | * need to change the class of the current instance by a normal one. |
||
757 | * |
||
758 | * @return AndRule The current instance (of or or subclass) or a new AndRule |
||
759 | */ |
||
760 | public function setOperandsOrReplaceByOperation($new_operands) |
||
767 | } |
||
768 | } |
||
772 |