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, function ($carry, $value) { 118 return $carry . $value; 119 }, '')); 120 121 $this->resultRows[$ident] = $row; 122 } 123 124 /** 125 * Get all result rows stored in this node 126 * 127 * @return Value[][] 128 */ 129 public function getResultRows() 130 { 131 return array_values($this->resultRows); 132 } 133 134 /** 135 * Get a unique key for this node 136 * 137 * @return string 138 */ 139 public function __toString() 140 { 141 if (!$this->value instanceof Value) return ''; // root node 142 return $this->parentPath . '/' . $this->value->__toString(); 143 } 144 145 /** 146 * Custom comparator to sort the children of this node 147 * 148 * @param NestedValue $a 149 * @param NestedValue $b 150 * @return int 151 */ 152 public function sortChildren(NestedValue $a, NestedValue $b) 153 { 154 $compA = implode('-', (array)$a->getValueObject()->getCompareValue()); 155 $compB = implode('-', (array)$b->getValueObject()->getCompareValue()); 156 157 // sort empty values to the end 158 if ($compA === $compB) { 159 return 0; 160 } 161 if ($compA === '') { 162 return 1; 163 } 164 if ($compB === '') { 165 return -1; 166 } 167 168 // note: the way NestedResults build the NestedValues, the value object should 169 // always contain a single value only. But since the associated column is still 170 // a multi-value column, getCompareValue() will still return an array. 171 // So here we treat all returns as array and join them with a dash (even though 172 // there should never be more than one value in there) 173 return Sort::strcmp($compA, $compB); 174 } 175 176 /** 177 * print the tree for debugging 178 * 179 * @param bool $sort use sorted children? 180 * @return string 181 */ 182 public function dump($sort = true) 183 { 184 $return = ''; 185 186 if ($this->value) { 187 $val = implode(', ', (array)$this->value->getDisplayValue()); 188 if ($val === '') $val = '{n/a}'; 189 $return .= str_pad('', $this->getDepth() * 4, ' '); 190 $return .= $val; 191 $return .= "\n"; 192 } else { 193 $return .= "*\n"; 194 } 195 196 foreach ($this->getResultRows() as $row) { 197 $return .= str_pad('', $this->getDepth() * 4, ' '); 198 foreach ($row as $value) { 199 $val = implode(', ', (array)$value->getDisplayValue()); 200 if ($val === '') $val = '{n/a}'; 201 $return .= ' ' . $val; 202 } 203 $return .= "\n"; 204 } 205 206 foreach ($this->getChildren($sort) as $child) { 207 $return .= $child->dump(); 208 } 209 210 return $return; 211 } 212} 213