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