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