Total Complexity | 153 |
Total Lines | 763 |
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) |
||
494 | } |
||
495 | |||
496 | /** |
||
497 | * + if A = 2 && A > 1 <=> A = 2 |
||
498 | * + if A = 2 && A < 4 <=> A = 2 |
||
499 | */ |
||
500 | protected static function simplifyDifferentOperandsForField($field, array $operandsByOperator) |
||
501 | { |
||
502 | // EqualRule comparisons |
||
503 | if (!empty($operandsByOperator[ EqualRule::operator ])) { |
||
504 | |||
505 | $equalRule = reset( $operandsByOperator[ EqualRule::operator ] ); |
||
506 | |||
507 | if (!empty($operandsByOperator[ NotEqualRule::operator ])) { |
||
508 | foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $not_equal_rule) { |
||
509 | |||
510 | if ($equalRule->getValue() !== null) { |
||
511 | if ($not_equal_rule->getValue() === null) // means if exists <=> equals something |
||
512 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
513 | elseif ($not_equal_rule->getValue() != $equalRule->getValue()) |
||
514 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
515 | } |
||
516 | elseif ($equalRule->getValue() === null ) { |
||
517 | if ($not_equal_rule->getValue() !== null) |
||
518 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
519 | // else we let the "equal null" and the "not equal null" for the romeInvalidBranches step |
||
520 | } |
||
521 | } |
||
522 | } |
||
523 | |||
524 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
525 | |||
526 | $aboveRule = reset($operandsByOperator[ AboveRule::operator ]); |
||
527 | if ($equalRule->getValue() !== null && $aboveRule->getMinimum() < $equalRule->getValue()) |
||
528 | unset($operandsByOperator[ AboveRule::operator ]); |
||
529 | } |
||
530 | |||
531 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
532 | |||
533 | $belowRule = reset($operandsByOperator[ BelowRule::operator ]); |
||
534 | if ($equalRule->getValue() !== null && $belowRule->getMaximum() > $equalRule->getValue()) |
||
535 | unset($operandsByOperator[ BelowRule::operator ]); |
||
536 | } |
||
537 | |||
538 | if (!empty($operandsByOperator[ InRule::operator ])) { |
||
539 | |||
540 | $possibilities = reset($operandsByOperator[ InRule::operator ])->getPossibilities(); |
||
541 | |||
542 | if (in_array($equalRule->getValue(), $possibilities)) { |
||
543 | unset($operandsByOperator[ InRule::operator ]); |
||
544 | } |
||
545 | else { |
||
546 | // We flush possibilities of the InRule |
||
547 | // TODO Replace it by a FalseRule |
||
548 | $operandsByOperator[ InRule::operator ][0]->setPossibilities([]); |
||
549 | // and also remove the equal rule to shorten the reste of the simplification process |
||
550 | unset($operandsByOperator[ EqualRule::operator ]); |
||
551 | } |
||
552 | } |
||
553 | |||
554 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
555 | |||
556 | $notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
||
557 | if (in_array($equalRule->getValue(), $notInRule->getPossibilities())) { |
||
558 | // ['field', '=', 4] && ['field', '!in', [4]...] <=> false |
||
559 | return []; |
||
560 | } |
||
561 | else { |
||
562 | unset($operandsByOperator[ NotInRule::operator ]); |
||
563 | } |
||
564 | // $notInRule->dump(true); |
||
565 | } |
||
566 | |||
567 | if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) { |
||
568 | |||
569 | $belowOrEqualRule = reset($operandsByOperator[ BelowOrEqualRule::operator ]); |
||
570 | if ($equalRule->getValue() <= $belowOrEqualRule->getMaximum()) { |
||
571 | unset($operandsByOperator[ BelowOrEqualRule::operator ]); |
||
572 | } |
||
573 | else { |
||
574 | // ['field', '=', 4] && ['field', '<=', [3]...] <=> false |
||
575 | return []; |
||
576 | } |
||
577 | } |
||
578 | |||
579 | if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) { |
||
580 | |||
581 | $aboveOrEqualRule = reset($operandsByOperator[ AboveOrEqualRule::operator ]); |
||
582 | if ($equalRule->getValue() >= $aboveOrEqualRule->getMinimum()) { |
||
583 | unset($operandsByOperator[ AboveOrEqualRule::operator ]); |
||
584 | } |
||
585 | else { |
||
586 | // ['field', '=', 4] && ['field', '<=', [3]...] <=> false |
||
587 | return []; |
||
588 | } |
||
589 | } |
||
590 | } |
||
591 | |||
592 | // NotEqualRule null comparisons |
||
593 | if (!empty($operandsByOperator[ NotEqualRule::operator ])) { |
||
594 | if (!empty($operandsByOperator[ NotEqualRule::operator ])) { |
||
595 | foreach ($operandsByOperator[ NotEqualRule::operator ] as $i => $notEqualRule) { |
||
596 | |||
597 | if ($notEqualRule->getValue() === null) { |
||
598 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
599 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
600 | } |
||
601 | |||
602 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
603 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
604 | } |
||
605 | |||
606 | if (!empty($operandsByOperator[ EqualRule::operator ])) { |
||
607 | if (reset($operandsByOperator[ EqualRule::operator ])->getValue() !== null) |
||
608 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
609 | } |
||
610 | } |
||
611 | else { |
||
612 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
613 | if ($operandsByOperator[ AboveRule::operator ][0]->getMinimum() >= $notEqualRule->getValue()) |
||
614 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
615 | } |
||
616 | |||
617 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
618 | if ($operandsByOperator[ BelowRule::operator ][0]->getMaximum() <= $notEqualRule->getValue()) |
||
619 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
620 | } |
||
621 | } |
||
622 | |||
623 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
624 | $notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
||
625 | if (!in_array($notEqualRule->getValue(), $notInRule->getPossibilities())) { |
||
626 | // TODO Replace it by a FalseRule |
||
627 | $operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
||
628 | array_merge($notInRule->getPossibilities(), [$notEqualRule->getValue()]) |
||
629 | ); |
||
630 | } |
||
631 | |||
632 | unset($operandsByOperator[ NotEqualRule::operator ][$i]); |
||
633 | } |
||
634 | |||
635 | if (!empty($operandsByOperator[ InRule::operator ])) { |
||
636 | $inRule = reset($operandsByOperator[ InRule::operator ]); |
||
637 | |||
638 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
639 | array_diff($inRule->getPossibilities(), [$notEqualRule->getValue()]) |
||
640 | ); |
||
641 | } |
||
642 | } |
||
643 | } |
||
644 | } |
||
645 | |||
646 | // Comparison between InRules and NotInRules |
||
647 | // This is an optimization to avoid NotIn explosion |
||
648 | if (!empty($operandsByOperator[ InRule::operator ])) { |
||
649 | $inRule = $operandsByOperator[ InRule::operator ][0]; |
||
650 | |||
651 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
652 | $notInRule = reset($operandsByOperator[ NotInRule::operator ]); |
||
653 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
654 | array_diff( $inRule->getPossibilities(), $notInRule->getPossibilities()) |
||
655 | ); |
||
656 | unset($operandsByOperator[ NotInRule::operator ]); |
||
657 | } |
||
658 | |||
659 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
660 | $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getMaximum(); |
||
661 | |||
662 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
663 | array_filter( $inRule->getPossibilities(), function ($possibility) use ($upper_limit) { |
||
664 | return $possibility < $upper_limit; |
||
665 | } ) |
||
666 | ); |
||
667 | |||
668 | unset($operandsByOperator[ BelowRule::operator ]); |
||
669 | } |
||
670 | |||
671 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
672 | $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum(); |
||
673 | |||
674 | $operandsByOperator[ InRule::operator ][0]->setPossibilities( |
||
675 | array_filter( $inRule->getPossibilities(), function ($possibility) use ($lower_limit) { |
||
676 | return $possibility > $lower_limit; |
||
677 | } ) |
||
678 | ); |
||
679 | |||
680 | unset($operandsByOperator[ AboveRule::operator ]); |
||
681 | } |
||
682 | } |
||
683 | |||
684 | // Comparison between NotInRules and > or < |
||
685 | if (!empty($operandsByOperator[ NotInRule::operator ])) { |
||
686 | $notInRule = $operandsByOperator[ NotInRule::operator ][0]; |
||
687 | |||
688 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
689 | $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit(); |
||
690 | |||
691 | $operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
||
692 | array_filter( $notInRule->getPossibilities(), function ($possibility) use ($upper_limit) { |
||
693 | return $possibility < $upper_limit; |
||
694 | } ) |
||
695 | ); |
||
696 | } |
||
697 | |||
698 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
699 | $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getMinimum(); |
||
700 | |||
701 | $operandsByOperator[ NotInRule::operator ][0]->setPossibilities( |
||
702 | array_filter( $notInRule->getPossibilities(), function ($possibility) use ($lower_limit) { |
||
703 | return $possibility > $lower_limit; |
||
704 | } ) |
||
705 | ); |
||
706 | } |
||
707 | } |
||
708 | |||
709 | // Comparison between <= and > or < |
||
710 | if (!empty($operandsByOperator[ BelowOrEqualRule::operator ])) { |
||
711 | $belowOrEqualRule = $operandsByOperator[ BelowOrEqualRule::operator ][0]; |
||
712 | |||
713 | if (!empty($operandsByOperator[ BelowRule::operator ])) { |
||
714 | $upper_limit = reset($operandsByOperator[ BelowRule::operator ])->getUpperLimit(); |
||
715 | |||
716 | if ($belowOrEqualRule->getMaximum() >= $upper_limit) { |
||
717 | // [field < 3] && [field <= 3] |
||
718 | // [field < 3] && [field <= 4] |
||
719 | unset($operandsByOperator[ BelowOrEqualRule::operator ][0]); |
||
720 | } |
||
721 | else { |
||
722 | // [field < 3] && [field <= 2] |
||
723 | unset($operandsByOperator[ BelowRule::operator ][0]); |
||
724 | } |
||
725 | } |
||
726 | |||
727 | if (!empty($operandsByOperator[ AboveRule::operator ])) { |
||
728 | $lower_limit = reset($operandsByOperator[ AboveRule::operator ])->getLowerLimit(); |
||
729 | |||
730 | if ($belowOrEqualRule->getMaximum() <= $lower_limit) { |
||
731 | // [field > 3] && [field <= 2] <=> false |
||
732 | return []; |
||
733 | } |
||
734 | } |
||
735 | |||
736 | if (!empty($operandsByOperator[ AboveOrEqualRule::operator ])) { |
||
737 | $minimum = reset($operandsByOperator[ AboveOrEqualRule::operator ])->getMinimum(); |
||
738 | |||
739 | if ($belowOrEqualRule->getMaximum() < $minimum) { |
||
740 | // [field <= 3] && [field >= 4] <=> false |
||
741 | return []; |
||
742 | } |
||
743 | elseif ($belowOrEqualRule->getMaximum() == $minimum) { |
||
744 | // [field <= 3] && [field >= 3] <=> [field = 3] |
||
745 | unset($operandsByOperator[ BelowOrEqualRule::operator ]); |
||
746 | unset($operandsByOperator[ AboveOrEqualRule::operator ]); |
||
747 | $operandsByOperator[ EqualRule::operator ][] = new EqualRule($field, $minimum); |
||
748 | |||
749 | if (count($operandsByOperator[ EqualRule::operator ]) > 1) { |
||
750 | $operandsByOperator = self::simplifyDifferentOperandsForField($field, $operandsByOperator); |
||
751 | } |
||
752 | } |
||
753 | } |
||
754 | } |
||
755 | |||
756 | return $operandsByOperator; |
||
757 | } |
||
758 | |||
759 | /** |
||
760 | * This method is meant to be used during simplification that would |
||
761 | * need to change the class of the current instance by a normal one. |
||
762 | * |
||
763 | * @return AndRule The current instance (of or or subclass) or a new AndRule |
||
764 | */ |
||
765 | public function setOperandsOrReplaceByOperation($new_operands) |
||
772 | } |
||
773 | } |
||
777 |