1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5use dokuwiki\Utf8\Sort; 6 7/** 8 * Object to create a tree of values 9 * 10 * You should not create these yourself, but use the NestedResult class instead 11 */ 12class NestedValue 13{ 14 /** @var Value */ 15 protected $value; 16 17 /** @var NestedValue[] */ 18 protected $children = []; 19 20 /** @var Value[][] */ 21 protected $resultRows = []; 22 23 /** @var int the nesting depth */ 24 protected $depth; 25 /** 26 * @var mixed|string 27 */ 28 protected $parentPath; 29 30 /** 31 * Create a nested version of the given value 32 * 33 * @param Value|null $value The value to store, null for root node 34 * @param int $depth The depth of this node (avoids collision where the same values are selected on multiple levels) 35 */ 36 public function __construct(?Value $value, $parentPath = '', $depth = 0) 37 { 38 $this->value = $value; 39 $this->parentPath = $parentPath; 40 $this->depth = $depth; 41 } 42 43 /** 44 * @return int 45 */ 46 public function getDepth() 47 { 48 return $this->depth; 49 } 50 51 /** 52 * @param int $depth 53 */ 54 public function setDepth($depth) 55 { 56 $this->depth = $depth; 57 foreach ($this->children as $child) { 58 $child->setDepth($depth + 1); 59 } 60 } 61 62 /** 63 * Access the stored value 64 * 65 * @return Value|null the value stored in this node, null for root node 66 */ 67 public function getValueObject() 68 { 69 return $this->value; 70 } 71 72 /** 73 * Add a child node 74 * 75 * Nodes with the same key (__toString()) will be overwritten 76 * 77 * @param NestedValue $child 78 * @return void 79 */ 80 public function addChild(NestedValue $child) 81 { 82 $this->children[(string)$child] = $child; // ensures uniqueness 83 } 84 85 /** 86 * Get all child nodes 87 * 88 * @param bool $sort should children be sorted alphabetically? 89 * @return NestedValue[] 90 */ 91 public function getChildren($sort = false) 92 { 93 $children = $this->children; 94 95 if ($sort) { 96 usort($children, [$this, 'sortChildren']); 97 } elseif (isset($children[''])) { 98 // even when not sorting, make sure the n/a entries are last 99 $naKids = $children['']; 100 unset($children['']); 101 $children[''] = $naKids; 102 } 103 return array_values($children); 104 } 105 106 /** 107 * Add a result row to this node 108 * 109 * Only unique rows will be stored, duplicates are detected by hashing the row values' toString result 110 * 111 * @param Value[] $row 112 * @return void 113 */ 114 public function addResultRow($row) 115 { 116 // only add unique rows 117 $ident = md5(array_reduce($row, static fn($carry, $value) => $carry . $value, '')); 118 119 $this->resultRows[$ident] = $row; 120 } 121 122 /** 123 * Get all result rows stored in this node 124 * 125 * @return Value[][] 126 */ 127 public function getResultRows() 128 { 129 return array_values($this->resultRows); 130 } 131 132 /** 133 * Get a unique key for this node 134 * 135 * @return string 136 */ 137 public function __toString() 138 { 139 if (!$this->value instanceof Value) return ''; // root node 140 return $this->parentPath . '/' . $this->value->__toString(); 141 } 142 143 /** 144 * Custom comparator to sort the children of this node 145 * 146 * @param NestedValue $a 147 * @param NestedValue $b 148 * @return int 149 */ 150 public function sortChildren(NestedValue $a, NestedValue $b) 151 { 152 $compA = implode('-', (array)$a->getValueObject()->getCompareValue()); 153 $compB = implode('-', (array)$b->getValueObject()->getCompareValue()); 154 155 // sort empty values to the end 156 if ($compA === $compB) { 157 return 0; 158 } 159 if ($compA === '') { 160 return 1; 161 } 162 if ($compB === '') { 163 return -1; 164 } 165 166 // note: the way NestedResults build the NestedValues, the value object should 167 // always contain a single value only. But since the associated column is still 168 // a multi-value column, getCompareValue() will still return an array. 169 // So here we treat all returns as array and join them with a dash (even though 170 // there should never be more than one value in there) 171 return Sort::strcmp($compA, $compB); 172 } 173 174 /** 175 * print the tree for debugging 176 * 177 * @param bool $sort use sorted children? 178 * @return string 179 */ 180 public function dump($sort = true) 181 { 182 $return = ''; 183 184 if ($this->value) { 185 $val = implode(', ', (array)$this->value->getDisplayValue()); 186 if ($val === '') $val = '{n/a}'; 187 $return .= str_pad('', $this->getDepth() * 4, ' '); 188 $return .= $val; 189 $return .= "\n"; 190 } else { 191 $return .= "*\n"; 192 } 193 194 foreach ($this->getResultRows() as $row) { 195 $return .= str_pad('', $this->getDepth() * 4, ' '); 196 foreach ($row as $value) { 197 $val = implode(', ', (array)$value->getDisplayValue()); 198 if ($val === '') $val = '{n/a}'; 199 $return .= ' ' . $val; 200 } 201 $return .= "\n"; 202 } 203 204 foreach ($this->getChildren($sort) as $child) { 205 $return .= $child->dump(); 206 } 207 208 return $return; 209 } 210} 211