* SPDX-License-Identifier: AGPL-3.0-only ************************************/ /* crmv@103881 */ /** * Class used to generate unique condition ids */ class ConditionIds { static $id = 1; public static function get() { return self::$id++; } } /** * Represent a single condition (a leaf in the tree) */ class ConditionLeaf { protected $id; public $relation; public $cond; protected $depth = 1; protected $weight = 1; public function __construct($relation, $cond) { $this->id = ConditionIds::get(); $this->relation = $relation ?: 'Main'; $this->cond = $cond; } public function getWeight() { return $this->weight; } public function getDepth() { return $this->depth; } public function setDepth($d) { $this->depth = $d; } public function checkOr() { return true; } // Debug functions /* public function toSql() { return $this->cond; } protected function cond2str($cond) { $cond = ''; if ($this->cond) { $cond = "{$this->cond['fieldid']} {$this->cond['comparator']} {$this->cond['value']}"; } return $cond; } public function draw(&$gdTree, $parentId = null) { $title = "LEAF"; $cond = $this->cond2str($this->cond); $text = $cond." ({$this->relation})"; $w = 9*strlen($text); $gdTree->add($this->id, $parentId, $title, $text, $w); } public function __toString() { $cond = $this->cond2str($this->cond); $r = str_repeat(" ", $this->depth*4) . " LEAF {$this->relation} ($cond):\n
"; return $r; } */ } /** * Represent a pair of conditions, joined by either OR or AND */ class ConditionNode { public $type; // and/or protected $id; protected $left; // can be a ConditionNode or a ConditionLeaf protected $right; // idem protected $leftModules = array(); protected $rightModules = array(); protected $weight = 0; // how many leaves are there behind this node protected $depth = 0; // how many generations from the root public function __construct($type, $left = null, $right = null) { $this->id = ConditionIds::get(); if (!$type) { throw new Exception("Missing type in node"); } $this->type = strtolower($type); if ($left) { $this->left = $left; $this->left->setDepth($this->depth+1); $this->weight += $left->getWeight(); $this->mergeModules($left, 'left'); } if ($right) { $this->right = $right; $this->right->setDepth($this->depth+1); $this->weight += $right->getWeight(); $this->mergeModules($right, 'right'); } } public function getWeight() { return $this->weight; } public function getDepth() { return $this->depth; } public function setDepth($d) { $this->depth = $d; if ($this->left) { $this->left->setDepth($d+1); } if ($this->right) { $this->right->setDepth($d+1); } } protected function mergeModules($nodeOrLeaf, $side = 'left') { if ($side == 'left') { if ($nodeOrLeaf instanceof ConditionLeaf) { $this->leftModules[] = $nodeOrLeaf->relation; } else { $this->leftModules = array_merge($this->leftModules, $nodeOrLeaf->leftModules, $nodeOrLeaf->rightModules); } $this->leftModules = array_unique($this->leftModules); } else { if ($nodeOrLeaf instanceof ConditionLeaf) { $this->rightModules[] = $nodeOrLeaf->relation; } else { $this->rightModules = array_merge($this->rightModules, $nodeOrLeaf->leftModules, $nodeOrLeaf->rightModules); } $this->rightModules = array_unique($this->rightModules); } } public function add($nodeOrLeaf, $type = null) { $nodeOrLeaf->setDepth($this->depth + 1); $this->weight += $nodeOrLeaf->getWeight(); if (!$this->left) { $this->left = $nodeOrLeaf; $this->mergeModules($nodeOrLeaf, 'left'); } elseif (!$this->right) { $this->right = $nodeOrLeaf; $this->mergeModules($nodeOrLeaf, 'right'); } else { // put it where the weight is the minimum if ($this->left->getWeight() <= $this->right->getWeight()) { $this->left = new ConditionNode($type, $this->left, $nodeOrLeaf); $this->left->setDepth($this->depth+1); // this code is ok for a generic tree, but in this case, it pushes down leaves of different type /*if ($this->left instanceof ConditionLeaf) { $this->left = new ConditionNode($type, $this->left, $nodeOrLeaf); $this->left->setDepth($this->depth+1); } else { $this->left->add($nodeOrLeaf, $type); }*/ $this->mergeModules($this->left, 'left'); } else { $this->right = new ConditionNode($type, $this->right, $nodeOrLeaf); $this->right->setDepth($this->depth+1); // idem /*if ($this->right instanceof ConditionLeaf) { $this->right = new ConditionNode($type, $this->right, $nodeOrLeaf); $this->right->setDepth($this->depth+1); } else { $this->right->add($nodeOrLeaf, $type); }*/ $this->mergeModules($this->right, 'right'); } } } public function checkOr() { $r = true; if (count($this->leftModules) > 0 && count($this->rightModules) > 0) { if ($this->type == 'or') { if (count($this->leftModules) > 1 || count($this->rightModules) > 1) { $r = false; } elseif ($this->leftModules[0] != $this->rightModules[0]) { $r = false; } } if ($r && $this->left instanceof ConditionNode) { $r = $r && $this->left->checkOr(); } if ($r && $this->right instanceof ConditionNode) { $r = $r && $this->right->checkOr(); } } return $r; } // Debug functions /* public function toSql() { $sql = ""; if ($this->left && $this->right) { $leftSql = $this->left->toSql(); $rightSql = $this->right->toSql(); $sql = "($leftSql ".strtoupper($this->type)." $rightSql)"; } elseif ($this->left) { $leftSql = $this->left->toSql(); $sql = "$leftSql"; } elseif ($this->right) { $rightSql = $this->right->toSql(); $sql = "$rightSql"; } return $sql; } public function draw(&$gdTree, $parentId = null) { $title = strtoupper($this->type); $text = ""; $w = 60; $gdTree->add($this->id, $parentId, $title, $text, $w); if ($this->left) { $this->left->draw($gdTree, $this->id); } if ($this->right) { $this->right->draw($gdTree, $this->id); } } public function __toString() { $pad = str_repeat(" ", $this->depth*4); $r = $pad . " NODE {$this->type} ({$this->weight}):\n
"; if ($this->left) { $r .= $pad." LEFT (".implode(',', $this->leftModules)."):
\n"; $r .= strval($this->left) ?: ""; } else { $r .= $pad." LEFT: X
\n"; } if ($this->right) { $r .= $pad." RIGHT (".implode(',', $this->rightModules)."):
\n"; $r .= strval($this->right) ?: ""; } else { $r .= $pad." RIGHT: X
\n"; } return $r; } */ } /** * The complete tree of the conditions */ class ConditionsTree { protected $root = null; /** * Parse a hierarchy of conditions in a tree-like structure */ public function parse($conditions) { if (is_array($conditions) && !empty($conditions)) { if (empty($conditions['glue'])) { // list of conditions $tree = $this->parseList($conditions); } else { // one group/condition $tree = $this->parseCondition($conditions); } $this->root = $tree; } } /** * Check if there are OR nodes with different modules on each side * In this case, the push down won't be available */ public function checkOrNodes() { if ($this->root) { return $this->root->checkOr(); } else { return true; } } protected function parseList($conditions) { $cond = $conditions[0]; $current = $this->parseCondition($cond); if (count($conditions) == 1) { return $current; } $type = strtolower($cond['glue']); $ortrees = array(); for ($i=1; $iparseCondition($cond); } else { //add to current (and) tree $newcond = $this->parseCondition($cond); if ($current instanceof ConditionLeaf) { $current = new ConditionNode($type, $current, $newcond); } else { if ($current->type == $type) { $current->add($newcond, $type); } else { $current = new ConditionNode($type, $current, $newcond); } } } $type = strtolower($cond['glue']); } if ($current) { $ortrees[] = $current; } if (count($ortrees) == 1) { $tree = $ortrees[0]; } else { $tree = $this->mergeNodes($ortrees, 'or'); } return $tree; } /** * Parse a group or a condition and returns a node/leaf * Returns a subtree */ protected function parseCondition($cond) { if ($cond['conditions']) { // has sub nodes $node = $this->parseList($cond['conditions']); } elseif ($cond['fieldid']) { $node = new ConditionLeaf($cond['relation'], $cond); } return $node; } protected function mergeNodes($list, $type = 'or') { if (count($list) == 1) { return $list[0]; } else { $base = new ConditionNode($type, $list[0], $list[1]); if (count($list) > 2) { for ($i=2; $iroot); } public function toQuery() { if ($this->root) { return $this->root->toSql(); } else { return ""; } } public function draw($output = "storage/tree_debug.png") { // NOT IMPLEMTENTED } */ }