1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5use dokuwiki\plugin\struct\types\Text; 6use dokuwiki\Utf8\PhpString; 7 8/** 9 * This class builds a nested tree from a search result 10 * 11 * This is used to create the nested output in the AggregationList 12 */ 13class NestedResult 14{ 15 /** @var NestedValue[] */ 16 protected $nodes = []; 17 18 /** @var NestedValue[] */ 19 protected $indexNodes = []; 20 21 /** @var Value[][] */ 22 protected $result; 23 24 /** 25 * @param Value[][] $result the original search result 26 * @return void 27 */ 28 public function __construct($result) 29 { 30 $this->result = $result; 31 } 32 33 /** 34 * Get the nested result 35 * 36 * @param int $nesting the nesting level to use 37 * @param int $index the number of characters to use for indexing 38 * @return NestedValue the root node of the nested tree 39 */ 40 public function getRoot($nesting, $index = 0) 41 { 42 $this->nodes = []; 43 $root = new NestedValue(null, -1); 44 45 if (!$this->result) return $root; 46 47 foreach ($this->result as $row) { 48 $this->nestBranch($root, $row, $nesting); 49 } 50 51 $root = $this->createIndex($root, $index); 52 return $root; 53 } 54 55 /** 56 * Add a top level index to the tree 57 * 58 * @param NestedValue $root Root node of the tree 59 * @param int $index Number of characters to use for indexing 60 * @return NestedValue new root node 61 */ 62 protected function createIndex(NestedValue $root, $index) 63 { 64 if (!$index) return $root; 65 $this->indexNodes = []; 66 67 $children = $root->getChildren(); 68 $resultRows = $root->getResultRows(); 69 if ($children) { 70 // there are children, so we are a nested result 71 foreach ($children as $child) { 72 $indexValue = $child->getValueObject(); 73 $indexNode = $this->getIndexNode($indexValue, $index); 74 $indexNode->addChild($child); 75 $child->setDepth(1); // increase child's depth from 0 to 1 76 } 77 } elseif ($resultRows) { 78 // no children, so we are currently a flat result 79 foreach ($resultRows as $row) { 80 $indexValue = $row[0]; 81 $indexNode = $this->getIndexNode($indexValue, $index); 82 $indexNode->addResultRow($row); 83 } 84 } 85 86 // now all results are added to index nodes - use them as children 87 $newRoot = new NestedValue(null, -1); 88 foreach ($this->indexNodes as $node) { 89 $newRoot->addChild($node); 90 } 91 return $newRoot; 92 } 93 94 /** 95 * Creates nested nodes for a given result row 96 * 97 * Splits up multi Values into separate nodes, when used in nesting 98 * 99 * @param Value[] $row current result row to work on 100 * @param int $nesting number of wanted nesting levels 101 * @param int $depth current nesting depth (used in recursion) 102 */ 103 protected function nestBranch(NestedValue $parent, $row, $nesting, $depth = 0) 104 { 105 // nesting level reached, add row and return 106 if ($depth >= $nesting) { 107 $parent->addResultRow($row); 108 return; 109 } 110 111 $valObj = array_shift($row); 112 if (!$valObj instanceof Value) return; // no more values to nest, usually shouldn't happen 113 114 $parentPath = (string) $parent; 115 116 if ($valObj->getColumn()->isMulti() && $valObj->getValue()) { 117 // split up multi values into separate nodes 118 $values = $valObj->getValue(); 119 if ($values) { 120 foreach ($values as $value) { 121 $newValue = new Value($valObj->getColumn(), $value); 122 $node = $this->getNodeForValue($newValue, $parentPath, $depth); 123 $parent->addChild($node); 124 $this->nestBranch($node, $row, $nesting, $depth + 1); 125 } 126 } else { 127 $newValue = new Value($valObj->getColumn(), ''); // add empty node 128 $node = $this->getNodeForValue($newValue, $parentPath, $depth); 129 $parent->addChild($node); 130 $this->nestBranch($node, $row, $nesting, $depth + 1); 131 } 132 } else { 133 $node = $this->getNodeForValue($valObj, $parentPath, $depth); 134 $parent->addChild($node); 135 $this->nestBranch($node, $row, $nesting, $depth + 1); 136 } 137 } 138 139 /** 140 * Create or get existing Node from the tree 141 * 142 * @param Value $value 143 * @param int $depth 144 * @return NestedValue 145 */ 146 protected function getNodeForValue(Value $value, $parentPath, $depth) 147 { 148 $node = new NestedValue($value, $parentPath, $depth); 149 $key = (string)$node; 150 if (!isset($this->nodes[$key])) { 151 $this->nodes[$key] = $node; 152 } 153 return $this->nodes[$key]; 154 } 155 156 /** 157 * Create or get an existing Node for indexing 158 * 159 * @param Value $value 160 * @param int $index 161 * @return NestedValue 162 */ 163 protected function getIndexNode(Value $value, $index) 164 { 165 $compare = $value->getDisplayValue(); 166 if (is_array($compare)) $compare = $compare[0]; 167 $key = PhpString::strtoupper(PhpString::substr($compare, 0, $index)); 168 169 if (!isset($this->indexNodes[$key])) { 170 $col = new Column( 171 0, 172 new Text([], '%index%', false), 173 -1, 174 true, 175 $value->getColumn()->getTable() 176 ); 177 $this->indexNodes[$key] = new NestedValue(new Value($col, $key), 0); 178 } 179 180 return $this->indexNodes[$key]; 181 } 182} 183