NestedTree::_generateTreeData()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 4
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace XoopsModules\Extgallery;
4
5
/**
6
 * ExtGallery Class Manager
7
 *
8
 * You may not change or alter any portion of this comment or credits
9
 * of supporting developers from this source code or any supporting source code
10
 * which is considered copyrighted (c) material of the original comment or credit authors.
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
 *
15
 * @copyright   {@link https://xoops.org/ XOOPS Project}
16
 * @license     GNU GPL 2 (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
17
 * @author      Zoullou (http://www.zoullou.net)
18
 * @package     ExtGallery
19
 */
20
21
use XoopsModules\Extgallery;
22
23
/**
24
 * Class NestedTree
25
 * @package XoopsModules\Extgallery
26
 */
27
class NestedTree
28
{
29
    public $db;
30
    public $table;
31
    public $fields;
32
33
    /**
34
     * Constructor. Set the database table name and necessary field names
35
     *
36
     * @param        $db
37
     * @param string $table       Name of the tree database table
38
     * @param string $idField     Name of the primary key ID field
39
     * @param string $parentField Name of the parent ID field
40
     * @param string $sortField   Name of the field to sort data.
41
     */
42
    public function __construct(\XoopsDatabase $db, $table, $idField, $parentField, $sortField)
43
    {
44
        $this->db     = $db;
45
        $this->table  = $db->prefix($table);
46
        $this->fields = [
47
            'id'     => $idField,
48
            'parent' => $parentField,
49
            'sort'   => $sortField,
50
        ];
51
    }
52
53
    /**
54
     * Fetch the node data for the node identified by $id
55
     *
56
     * @param int $id    The ID of the node to fetch
57
     * @return null|\XoopsObject An object containing the node's
58
     *                   data, or null if node not found
59
     */
60
    public function getNode($id)
61
    {
62
        $query = \sprintf('SELECT * FROM `%s` WHERE `%s` = %d', $this->table, $this->fields['id'], $id);
63
64
        $result = $this->db->query($query);
65
        $row    = $this->db->fetchArray($result);
66
        if ($row) {
67
            return $row;
68
        }
69
70
        return null;
71
    }
72
73
    /**
74
     * Fetch the descendants of a node, or if no node is specified, fetch the
75
     * entire tree. Optionally, only return child data instead of all descendant
76
     * data.
77
     *
78
     * @param int  $id             The ID of the node to fetch descendant data for.
79
     *                             Specify an invalid ID (e.g. 0) to retrieve all data.
80
     * @param bool $includeSelf    Whether or not to include the passed node in the
81
     *                             the results. This has no meaning if fetching entire tree.
82
     * @param bool $childrenOnly   True if only returning children data. False if
83
     *                             returning all descendant data
84
     * @return array The descendants of the passed now
85
     */
86
    public function getDescendants($id = 0, $includeSelf = false, $childrenOnly = false)
87
    {
88
        $idField = $this->fields['id'];
89
90
        $node = $this->getNode($id);
91
        if (null === $node) {
92
            $nleft     = 0;
93
            $nright    = 0;
94
            $parent_id = 0;
95
        } else {
96
            $nleft     = $node['nleft'];
97
            $nright    = $node['nright'];
98
            $parent_id = $node[$this->fields['id']];
99
        }
100
101
        if ($childrenOnly) {
102
            if ($includeSelf) {
103
                $query = \sprintf('SELECT * FROM `%s` WHERE `%s` = %d OR %s = %d ORDER BY nleft', $this->table, $this->fields['id'], $parent_id, $this->fields['parent'], $parent_id);
104
            } else {
105
                $query = \sprintf('SELECT * FROM `%s` WHERE `%s` = %d ORDER BY nleft', $this->table, $this->fields['parent'], $parent_id);
106
            }
107
        } else {
108
            if ($nleft > 0 && $includeSelf) {
109
                $query = \sprintf('SELECT * FROM `%s` WHERE nleft >= %d AND nright <= %d ORDER BY nleft', $this->table, $nleft, $nright);
110
            } else {
111
                if ($nleft > 0) {
112
                    $query = \sprintf('SELECT * FROM `%s` WHERE nleft > %d AND nright < %d ORDER BY nleft', $this->table, $nleft, $nright);
113
                } else {
114
                    $query = \sprintf('SELECT * FROM `%s` ORDER BY nleft', $this->table);
115
                }
116
            }
117
        }
118
119
        $result = $this->db->query($query);
120
121
        $arr = [];
122
        while (false !== ($row = $this->db->fetchArray($result))) {
123
            $arr[$row[$idField]] = $row;
124
        }
125
126
        return $arr;
127
    }
128
129
    /**
130
     * Fetch the children of a node, or if no node is specified, fetch the
131
     * top level items.
132
     *
133
     * @param int  $id            The ID of the node to fetch child data for.
134
     * @param bool $includeSelf   Whether or not to include the passed node in the
135
     *                            the results.
136
     * @return array The children of the passed node
137
     */
138
    public function getChildren($id = 0, $includeSelf = false)
139
    {
140
        return $this->getDescendants($id, $includeSelf, true);
141
    }
142
143
    /**
144
     * Fetch the path to a node. If an invalid node is passed, an empty array is returned.
145
     * If a top level node is passed, an array containing on that node is included (if
146
     * 'includeSelf' is set to true, otherwise an empty array)
147
     *
148
     * @param int  $id            The ID of the node to fetch child data for.
149
     * @param bool $includeSelf   Whether or not to include the passed node in the
150
     *                            the results.
151
     * @return array An array of each node to passed node
152
     */
153
    public function getPath($id = 0, $includeSelf = false)
154
    {
155
        $node = $this->getNode($id);
156
        if (null === $node) {
157
            return [];
158
        }
159
160
        if ($includeSelf) {
161
            $query = \sprintf('SELECT * FROM `%s` WHERE nleft <= %d AND nright >= %d ORDER BY nlevel', $this->table, $node['nleft'], $node['nright']);
162
        } else {
163
            $query = \sprintf('SELECT * FROM `%s` WHERE nleft < %d AND nright > %d ORDER BY nlevel', $this->table, $node['nleft'], $node['nright']);
164
        }
165
166
        $result = $this->db->query($query);
167
168
        $idField = $this->fields['id'];
169
        $arr     = [];
170
        while (false !== ($row = $this->db->fetchArray($result))) {
171
            $arr[$row[$idField]] = $row;
172
        }
173
174
        return $arr;
175
    }
176
177
    /**
178
     * Check if one node descends from another node. If either node is not
179
     * found, then false is returned.
180
     *
181
     * @param int $descendant_id The node that potentially descends
182
     * @param int $ancestor_id   The node that is potentially descended from
183
     * @return bool True if $descendant_id descends from $ancestor_id, false otherwise
184
     */
185
    public function isDescendantOf($descendant_id, $ancestor_id)
186
    {
187
        $node = $this->getNode($ancestor_id);
188
        if (null === $node) {
189
            return false;
190
        }
191
192
        $query = \sprintf(
193
            'SELECT COUNT(*) AS is_descendant
194
                                      FROM `%s`
195
                                      WHERE `%s` = %d
196
                                      AND nleft > %d
197
                                      AND nright < %d',
198
            $this->table,
199
            $this->fields['id'],
200
            $descendant_id,
201
            $node->nleft,
0 ignored issues
show
Bug introduced by
The property nleft does not seem to exist on XoopsObject.
Loading history...
202
            $node->nright
0 ignored issues
show
Bug introduced by
The property nright does not seem to exist on XoopsObject.
Loading history...
203
        );
204
205
        $result = $this->db->query($query);
206
207
        $row = $this->db->fetchArray($result);
208
        if ($row) {
209
            return $row['is_descendant'] > 0;
210
        }
211
212
        return false;
213
    }
214
215
    /**
216
     * Check if one node is a child of another node. If either node is not
217
     * found, then false is returned.
218
     *
219
     * @param int $child_id  The node that is possibly a child
220
     * @param int $parent_id The node that is possibly a parent
221
     * @return bool True if $child_id is a child of $parent_id, false otherwise
222
     */
223
    public function isChildOf($child_id, $parent_id)
224
    {
225
        $query = \sprintf('SELECT COUNT(*) AS is_child FROM `%s` WHERE `%s` = %d AND %s = %d', $this->table, $this->fields['id'], $child_id, $this->fields['parent'], $parent_id);
226
227
        $result = $this->db->query($query);
228
229
        $row = $this->db->fetchArray($result);
230
        if ($row) {
231
            return $row['is_child'] > 0;
232
        }
233
234
        return false;
235
    }
236
237
    /**
238
     * Find the number of descendants a node has
239
     *
240
     * @param int $id The ID of the node to search for. Pass 0 to count all nodes in the tree.
241
     * @return int The number of descendants the node has, or -1 if the node isn't found.
242
     */
243
    public function numDescendants($id)
244
    {
245
        if (0 == $id) {
246
            $query  = \sprintf('SELECT COUNT(*) AS num_descendants FROM `%s`', $this->table);
247
            $result = $this->db->query($query);
248
            $row    = $this->db->fetchArray($result);
249
            if ($row) {
250
                return $row['num_descendants'];
251
            }
252
        } else {
253
            $node = $this->getNode($id);
254
            if (!null === $node) {
0 ignored issues
show
introduced by
The condition ! null === $node is always false.
Loading history...
255
                return ($node['nright'] - $node['nleft'] - 1) / 2;
256
            }
257
        }
258
259
        return -1;
260
    }
261
262
    /**
263
     * Find the number of children a node has
264
     *
265
     * @param int $id The ID of the node to search for. Pass 0 to count the first level items
266
     * @return int The number of descendants the node has, or -1 if the node isn't found.
267
     */
268
    public function numChildren($id)
269
    {
270
        $query  = \sprintf('SELECT COUNT(*) AS num_children FROM `%s` WHERE `%s` = %d', $this->table, $this->fields['parent'], $id);
271
        $result = $this->db->query($query);
272
        $row    = $this->db->fetchArray($result);
273
        if ($row) {
274
            return $row['num_children'];
275
        }
276
277
        return -1;
278
    }
279
280
    /**
281
     * @param $id
282
     *
283
     * @return int
284
     */
285
    public function numLeef($id)
286
    {
287
        $query = \sprintf('SELECT COUNT(*) AS num_leef FROM `%s` WHERE nright - nleft = 1', $this->table);
288
        if (0 != $id) {
289
            $node  = $this->getNode($id);
290
            $query .= \sprintf(' AND nleft > %d AND nright < %d', $node['nleft'], $node['nright']);
291
        }
292
        $result = $this->db->query($query);
293
        $row    = $this->db->fetchArray($result);
294
        if ($row) {
295
            return $row['num_leef'];
296
        }
297
298
        return -1;
299
    }
300
301
    /**
302
     * Fetch the tree data, nesting within each node references to the node's children
303
     *
304
     * @return array The tree with the node's child data
305
     */
306
    public function getTreeWithChildren()
307
    {
308
        $idField     = $this->fields['id'];
0 ignored issues
show
Unused Code introduced by
The assignment to $idField is dead and can be removed.
Loading history...
309
        $parentField = $this->fields['parent'];
0 ignored issues
show
Unused Code introduced by
The assignment to $parentField is dead and can be removed.
Loading history...
310
311
        $query = \sprintf('SELECT * FROM `%s` ORDER BY %s', $this->table, $this->fields['sort']);
312
313
        $result = $this->db->query($query);
314
315
        // create a root node to hold child data about first level items
316
        $root                      = [];
317
        $root[$this->fields['id']] = 0;
318
        $root['children']          = [];
319
320
        $arr = [
321
            $root,
322
        ];
323
324
        // populate the array and create an empty children array
325
        while (false !== ($row = $this->db->fetchArray($result))) {
326
            $arr[$row[$this->fields['id']]]             = $row;
327
            $arr[$row[$this->fields['id']]]['children'] = [];
328
        }
329
330
        // now process the array and build the child data
331
        foreach ($arr as $id => $row) {
332
            if (isset($row[$this->fields['parent']])) {
333
                $arr[$row[$this->fields['parent']]]['children'][$id] = $id;
334
            }
335
        }
336
337
        return $arr;
338
    }
339
340
    /**
341
     * Rebuilds the tree data and saves it to the database
342
     */
343
    public function rebuild()
344
    {
345
        $data = $this->getTreeWithChildren();
346
347
        $n     = 0; // need a variable to hold the running n tally
348
        $level = 0; // need a variable to hold the running level tally
0 ignored issues
show
Unused Code introduced by
The assignment to $level is dead and can be removed.
Loading history...
349
350
        // invoke the recursive function. Start it processing
351
        // on the fake "root node" generated in getTreeWithChildren().
352
        // because this node doesn't really exist in the database, we
353
        // give it an initial nleft value of 0 and an nlevel of 0.
354
        $this->_generateTreeData($data, 0, 0, $n);
355
        //echo "<pre>";print_r($data);echo "</pre>";
356
        // at this point the the root node will have nleft of 0, nlevel of 0
357
        // and nright of (tree size * 2 + 1)
358
359
        foreach ($data as $id => $row) {
360
            // skip the root node
361
            if (0 == $id) {
362
                continue;
363
            }
364
365
            $query = \sprintf('UPDATE `%s` SET nlevel = %d, nleft = %d, nright = %d WHERE `%s` = %d', $this->table, $row['nlevel'], $row['nleft'], $row['nright'], $this->fields['id'], $id);
366
            $this->db->queryF($query);
367
        }
368
    }
369
370
    /**
371
     * Generate the tree data. A single call to this generates the n-values for
372
     * 1 node in the tree. This function assigns the passed in n value as the
373
     * node's nleft value. It then processes all the node's children (which
374
     * in turn recursively processes that node's children and so on), and when
375
     * it is finally done, it takes the update n-value and assigns it as its
376
     * nright value. Because it is passed as a reference, the subsequent changes
377
     * in subrequests are held over to when control is returned so the nright
378
     * can be assigned.
379
     *
380
     * @param array &$arr   A reference to the data array, since we need to
381
     *                      be able to update the data in it
382
     * @param int    $id    The ID of the current node to process
383
     * @param int    $level The nlevel to assign to the current node
384
     * @param int   &$n     A reference to the running tally for the n-value
385
     */
386
    public function _generateTreeData(&$arr, $id, $level, &$n)
387
    {
388
        $arr[$id]['nlevel'] = $level;
389
        $arr[$id]['nleft']  = ++$n;
390
391
        // loop over the node's children and process their data
392
        // before assigning the nright value
393
        foreach ($arr[$id]['children'] as $child_id) {
394
            $this->_generateTreeData($arr, $child_id, $level + 1, $n);
395
        }
396
        $arr[$id]['nright'] = ++$n;
397
    }
398
}
399