xref: /plugin/struct/meta/NestedValue.php (revision 4bffd43680da9f5ab6f22a645c1f04bbe99210f8)
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