Component::lister()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
ccs 11
cts 11
cp 1
rs 9.2
cc 4
eloc 10
nc 4
nop 2
crap 4
1
<?php
2
3
namespace BootPress\Hierarchy;
4
5
use BootPress\Database\Component as Database;
6
7
// http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/
8
// http://www.evanpetersen.com/item/nested-sets.html
9
// http://stackoverflow.com/questions/4048151/what-are-the-options-for-storing-hierarchical-data-in-a-relational-database
10
// http://vadimtropashko.wordpress.com/2008/08/09/one-more-nested-intervals-vs-adjacency-list-comparison/
11
// http://www.slideshare.net/billkarwin/models-for-hierarchical-data
12
// http://troels.arvin.dk/db/rdbms/links/#hierarchical
13
14
class Component
15
{
16
    private $db;
17
    private $id;
18
    private $table;
19
    private $count;
20
    private $parents;
21
    public $update = array();
22
23
    /*
24
    The $db $table must have the following fields array(
25
        $id => 'INTEGER PRIMARY KEY',
26
        'parent' => 'INTEGER NOT NULL DEFAULT 0',
27
        'level' => 'INTEGER NOT NULL DEFAULT 0',
28
        'lft' => 'INTEGER NOT NULL DEFAULT 0',
29
        'rgt' => 'INTEGER NOT NULL DEFAULT 0'
30
    )
31
    All you need to worry about is the 'id' and 'parent'.  This class will take care of the rest.
32
    */
33
34 12
    public function __construct(Database $db, $table, $id = 'id')
35
    {
36 12
        $this->db = $db;
37 12
        $this->id = $id;
38 12
        $this->table = $table;
39 12
    }
40
41
    // this should be called any time you insert into or delete from your hierarchical table
42
    // http://stackoverflow.com/questions/4664517/how-do-you-convert-a-parent-child-adjacency-table-to-a-nested-set-using-php-an
43
    // http://gen5.info/q/2008/11/04/nested-sets-php-verb-objects-and-noun-objects/
44 6
    public function refresh($order = null)
45
    {
46 6
        if (is_null($order)) {
47 2
            $order = $this->id;
48 2
        }
49 6
        $this->count = 0;
50 6
        $this->parents = array();
51 6
        $this->update = array();
52 6
        if ($stmt = $this->db->query('SELECT '.$this->id.', parent FROM '.$this->table.' ORDER BY '.$order)) {
53 6
            while (list($id, $parent) = $this->db->fetch($stmt)) {
54 6
                if (!isset($this->parents[$parent])) {
55 6
                    $this->parents[$parent] = array();
56 6
                }
57 6
                $this->parents[$parent][] = $id;
58 6
            }
59 6
            $this->db->close($stmt);
60 6
        }
61 6
        $this->traverse(0);
62 6
        unset($this->update[0]);
63 6
        ksort($this->update);
64 6
        if ($stmt = $this->db->update($this->table, $this->id, array('level', 'lft', 'rgt'))) {
65 6
            foreach ($this->update as $id => $fields) {
66 6
                $this->db->update($stmt, $id, $fields);
67 6
                unset($this->update[$id]['level']);
68 6
            }
69 6
            $this->db->close($stmt);
70 6
        }
71 6
    }
72
73 1
    public function delete($id)
74
    {
75 1
        if ($ids = $this->db->ids(array(
76 1
            'SELECT node.id',
77 1
            'FROM '.$this->table.' AS node, '.$this->table.' AS parent',
78 1
            'WHERE node.lft BETWEEN parent.lft AND parent.rgt AND parent.'.$this->id.' = ?',
79 1
            'ORDER BY node.lft',
80 1
        ), $id)) {
81 1
            $this->db->exec('DELETE FROM '.$this->table.' WHERE '.$this->id.' IN('.implode(', ', $ids).')');
82
83 1
            return $ids;
84
        }
85
86 1
        return false;
87
    }
88
89 1
    public function id($field, array $values)
90
    {
91 1
        $from = array();
92 1
        $where = array();
93 1
        foreach (range(1, count($values)) as $level => $num) {
94 1
            $from[] = (isset($previous)) ? "{$this->table} AS t{$num} ON t{$num}.parent = t{$previous}.{$this->id}" : "{$this->table} AS t{$num}";
95 1
            $where[] = "t{$num}.level = {$level} AND t{$num}.{$field} = ?";
96 1
            $previous = $num;
97 1
        }
98
99 1
        return $this->db->value("SELECT t{$num}.{$this->id} \nFROM ".implode("\n  LEFT JOIN ", $from)."\nWHERE ".implode("\n  AND ", $where), $values);
0 ignored issues
show
Bug introduced by
The variable $num seems to be defined by a foreach iteration on line 93. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
100
    }
101
102 4
    public function path(array $fields, array $options)
103
    {
104 4
        if (!isset($options['where'])) {
105 1
            return array();
106
        }
107 4
        list($field, $id) = array_map('trim', explode('=', $options['where'])); // eg. 'id = 1'
108 4
        $path = array();
109 4
        if ($stmt = $this->db->query(array(
110 4
            'SELECT parent.'.$this->id.', '.$this->fields('parent', $fields),
111 4
            'FROM '.$this->table.' AS node',
112 4
            'INNER JOIN '.$this->table.' AS parent',
113 4
            'WHERE node.lft BETWEEN parent.lft AND parent.rgt AND node.'.$field.' = ?',
114 4
            'ORDER BY node.lft',
115 4
        ), $id, 'assoc')) {
116 4
            while ($row = $this->db->fetch($stmt)) {
117 4
                $path[array_shift($row)] = $row;
118 4
            }
119 4
            $this->db->close($stmt);
120 4
        }
121
122 4
        return $path;
123
    }
124
125 1
    public function children($id, $fields) // The immediate subordinates of a node ie. no grand children
126
    {
127 1
        $single = (!is_array($fields)) ? $fields : false;
128 1
        $children = array();
129 1
        if ($stmt = $this->db->query('SELECT '.$this->id.', '.implode(', ', (array) $fields).' FROM '.$this->table.' WHERE parent = ? ORDER BY lft', $id, 'assoc')) {
130 1
            while ($row = $this->db->fetch($stmt)) {
131 1
                $children[array_shift($row)] = ($single) ? $row[$single] : $row;
132 1
            }
133 1
            $this->db->close($stmt);
134 1
        }
135
136 1
        return $children;
137
    }
138
139 1
    public function level($depth, $fields)
140
    {
141 1
        $single = (!is_array($fields)) ? $fields : false;
142 1
        $level = array();
143 1
        if ($stmt = $this->db->query('SELECT '.$this->id.', '.implode(', ', (array) $fields).' FROM '.$this->table.' WHERE level = ? ORDER BY lft', $depth, 'assoc')) {
144 1
            while ($row = $this->db->fetch($stmt)) {
145 1
                $level[array_shift($row)] = ($single) ? $row[$single] : $row;
146 1
            }
147 1
            $this->db->close($stmt);
148 1
        }
149
150 1
        return $level;
151
    }
152
153 5
    public function counts($table, $match, $id = null)
154
    {
155 5
        if (!is_null($id)) {
156 1
            return (int) $this->db->value(array(
157 1
                'SELECT COUNT('.$table.'.'.$match.') AS count',
158 1
                'FROM '.$this->table.' AS node',
159 1
                'INNER JOIN '.$this->table.' AS parent',
160 1
                'INNER JOIN '.$table,
161 1
                'WHERE parent.'.$this->id.' = '.(int) $id.' AND node.lft BETWEEN parent.lft AND parent.rgt AND node.'.$this->id.' = '.$table.'.'.$match,
162 1
                'GROUP BY parent.'.$this->id,
163 1
                'ORDER BY parent.lft',
164 1
            ));
165
        } else {
166 5
            $counts = array();
167 5
            if ($stmt = $this->db->query(array(
168 5
                'SELECT parent.'.$this->id.', COUNT('.$table.'.'.$match.') AS count',
169 5
                'FROM '.$this->table.' AS node',
170 5
                'INNER JOIN '.$this->table.' AS parent',
171 5
                'INNER JOIN '.$table,
172 5
                'WHERE node.lft BETWEEN parent.lft AND parent.rgt AND node.'.$this->id.' = '.$table.'.'.$match,
173 5
                'GROUP BY parent.'.$this->id,
174 5
                'ORDER BY parent.lft',
175 5
            ))) {
176 5
                while (list($id, $count) = $this->db->fetch($stmt)) {
177 5
                    $counts[$id] = (int) $count;
178 5
                }
179 5
                $this->db->close($stmt);
180 5
            }
181
182 5
            return $counts;
183
        }
184
    }
185
186 11
    public function tree(array $fields, array $options = array())
187
    {
188 11
        $tree = array();
189 11
        $having = (isset($options['having'])) ? ' HAVING '.$options['having'] : null; // eg. 'depth <= 1'
190 11
        if (isset($options['where'])) {
191 6
            list($field, $id) = array_map('trim', explode('=', $options['where'])); // eg. 'id = 1'
192 6
            $stmt = $this->db->query(array(
193 6
                'SELECT node.'.$this->id.', '.$this->fields('node', (array) $fields).', node.parent, (COUNT(parent.'.$this->id.') - (sub_tree.sub_depth + 1)) AS depth',
194 6
                'FROM '.$this->table.' AS node',
195 6
                'INNER JOIN '.$this->table.' AS parent',
196 6
                'INNER JOIN '.$this->table.' AS sub_parent',
197 6
                'INNER JOIN (',
198 6
                '  SELECT node.'.$this->id.', (COUNT(parent.'.$this->id.') - 1) AS sub_depth',
199 6
                '  FROM '.$this->table.' AS node',
200 6
                '  INNER JOIN '.$this->table.' AS parent',
201 6
                '  WHERE node.lft BETWEEN parent.lft AND parent.rgt',
202 6
                '  AND node.'.$field.' = ?',
203 6
                '  GROUP BY node.'.$this->id,
204 6
                '  ORDER BY node.lft',
205 6
                ') AS sub_tree',
206 6
                'WHERE node.lft BETWEEN parent.lft AND parent.rgt',
207 6
                '  AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt',
208 6
                '  AND sub_parent.'.$this->id.' = sub_tree.'.$this->id,
209 6
                'GROUP BY node.'.$this->id.$having,
210 6
                'ORDER BY node.lft',
211 6
            ), $id, 'assoc');
212 6
        } else {
213 6
            $stmt = $this->db->query(array(
214 6
                'SELECT node.'.$this->id.', '.$this->fields('node', (array) $fields).', node.parent, (COUNT(parent.'.$this->id.') - 1) AS depth',
215 6
                'FROM '.$this->table.' AS node',
216 6
                'INNER JOIN '.$this->table.' AS parent',
217 6
                'WHERE node.lft BETWEEN parent.lft AND parent.rgt',
218 6
                'GROUP BY node.'.$this->id.$having,
219 6
                'ORDER BY node.lft',
220 6
            ), '', 'assoc');
0 ignored issues
show
Documentation introduced by
'' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
221
        }
222 11
        while ($row = $this->db->fetch($stmt)) {
223 10
            $tree[array_shift($row)] = $row;
224 10
        }
225 11
        $this->db->close($stmt);
226
227 11
        return $tree;
228
    }
229
230 1
    public function lister(array $tree, array $nest = null)
231
    {
232 1
        if (is_null($nest)) {
233 1
            return $this->lister($tree, $this->nestify($tree));
234
        }
235 1
        $list = array();
236 1
        foreach ($nest as $id => $values) {
237 1
            if (!empty($values)) {
238 1
                $list[array_shift($tree[$id])] = $this->lister($tree, $values);
239 1
            } else {
240 1
                $list[] = array_shift($tree[$id]);
241
            }
242 1
        }
243
244 1
        return $list;
245
    }
246
247
    // http://semlabs.co.uk/journal/converting-nested-set-model-data-in-to-multi-dimensional-arrays-in-php
248 7
    public function nestify(array $tree)
249
    {
250 7
        $nested = array();
251 7
        $children = array();
252 7
        foreach ($tree as $id => $fields) {
253 7
            if ($fields['depth'] == 0) {
254 7
                $nested[$id] = array(); // $fields;
255 7
                $children[$fields['depth'] + 1] = $id;
256 7
            } else {
257 5
                $parent = &$nested;
258 5
                for ($i = 1; $i <= $fields['depth']; ++$i) {
259 5
                    if (isset($children[$i])) {
260 5
                        $parent = &$parent[$children[$i]];
261 5
                    }
262 5
                }
263 5
                $parent[$id] = array(); // $fields;
264 5
                $children[$fields['depth'] + 1] = $id;
265
            }
266 7
        }
267
268 7
        return $nested;
269
    }
270
271
    // http://stackoverflow.com/questions/16999530/how-do-i-format-nested-set-model-data-into-an-array
272 3
    public function flatten(array $nest, array $related = array())
273
    {
274 3
        $children = array();
275 3
        foreach ($nest as $id => $values) {
276 3
            $parents = $related;
277 3
            $parents[] = $id;
278 3
            if (!empty($values)) {
279 2
                foreach ($this->flatten($values, $parents) as $nest) {
280 2
                    $children[] = $nest;
281 2
                }
282 2
            } else {
283 3
                $children[] = $parents;
284
            }
285 3
        }
286
287 3
        return $children;
288
    }
289
290 6
    private function traverse($id, $level = -1)
291
    {
292 6
        $lft = $this->count;
293 6
        ++$this->count;
294 6
        if (isset($this->parents[$id])) {
295 6
            foreach ($this->parents[$id] as $child) {
296 6
                $this->traverse($child, $level + 1);
297 6
            }
298 6
            unset($this->parents[$id]);
299 6
        }
300 6
        $rgt = $this->count;
301 6
        ++$this->count;
302 6
        $this->update[$id] = array('level' => $level, 'lft' => $lft, 'rgt' => $rgt);
303 6
    }
304
305 12
    private function fields($prefix, array $fields)
306
    {
307 12
        return $prefix.'.'.implode(', '.$prefix.'.', $fields);
308
    }
309
}
310