DatabaseTree   B
last analyzed

Complexity

Total Complexity 53

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 53
c 2
b 0
f 1
lcom 1
cbo 1
dl 0
loc 384
rs 7.4757

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A countAncestors() 0 13 2
A getAncestors() 0 14 2
A countSiblings() 0 3 1
A getSiblings() 0 5 1
A countChildren() 0 3 1
A getChildren() 0 5 1
A countDescendants() 0 3 1
C getDescendants() 0 44 7
B insertNode() 0 50 6
B removeNode() 0 35 6
B moveNode() 0 63 7
B getTree() 0 60 8
A visualise() 0 19 3
A rebuild() 0 16 2
A rebuildNode() 0 18 3
A findNodeById() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DatabaseTree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DatabaseTree, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of Yolk - Gamer Network's PHP Framework.
4
 *
5
 * Copyright (c) 2013 Gamer Network Ltd.
6
 *
7
 * Distributed under the MIT License, a copy of which is available in the
8
 * LICENSE file that was bundled with this package, or online at:
9
 * https://github.com/gamernetwork/yolk
10
 */
11
12
namespace yolk\database\support;
13
14
use yolk\contracts\database\DatabaseConnection;
15
use yolk\contracts\support\Tree;
16
17
// only manipulates tree fields (lft, rgt), doesn't insert or delete database records
18
class DatabaseTree implements Tree {
19
20
	protected $db;
21
22
	protected $table_name;
23
	protected $name_field;
24
25
	public function __construct( DatabaseConnection $db, $table_name, $name_field = 'name' ) {
26
		$this->db         = $db;
27
		$this->table_name = $table_name;
28
		$this->name_field = $name_field;
29
	}
30
31
	public function countAncestors( $id ) {
32
33
		$node = $this->findNodeById($id);
34
35
		return $node ? (int) $this->db->getOne(
36
			"SELECT COUNT(*)
37
			  FROM {$this->table_name}
38
			 WHERE lft < :lft
39
			   AND rgt > :rgt",
40
			$node
41
		) : 0;
42
43
	}
44
45
	public function getAncestors( $id ) {
46
47
		$node = $this->findNodeById($id);
48
49
		return $node ? $this->db->getAssoc(
50
			"SELECT id, {$this->name_field}
51
			   FROM {$this->table_name}
52
			  WHERE lft < :lft
53
			    AND rgt > :rgt
54
		   ORDER BY lft ASC",
55
			$node
56
		) : array();
57
58
	}
59
60
	public function countSiblings( $id ) {
61
		return (int) $this->db->getOne("SELECT COUNT(*) FROM {$this->table_name} WHERE parent_id = (SELECT parent_id FROM {$this->table_name} WHERE id = ?)", $id) - 1;
62
	}
63
64
	public function getSiblings( $id ) {
65
		$siblings = $this->db->getAssoc("SELECT id, {$this->name_field} FROM {$this->table_name} WHERE parent_id = (SELECT parent_id FROM {$this->table_name} WHERE id = ?)", $id);
66
		asort($siblings);
67
		return $siblings;
68
	}
69
70
	public function countChildren( $id ) {
71
		return (int) $this->db->getOne("SELECT COUNT(*) FROM {$this->table_name} WHERE parent_id = ", $id);
72
	}
73
74
	public function getChildren( $id ) {
75
		$children = $this->db->getAssoc("SELECT id, {$this->name_field} FROM {$this->table_name} WHERE parent_id = ?", $id);
76
		asort($children);
77
		return $children;
78
	}
79
80
	public function countDescendants( $id ) {
81
		return (int) $this->db->getOne("SELECT (rgt - lft - 1) / 2 FROM {$this->table_name} WHERE id = ?", $id);
82
	}
83
84
	public function getDescendants( $id, $absolute_depth = false ) {
85
86
		$node = $this->findNodeById($id);
87
88
		if( !$node )
0 ignored issues
show
Bug Best Practice introduced by
The expression $node of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
89
			return array();
90
91
		$descendants = $this->db->getAssoc(
92
			"SELECT id,
93
				   {$this->name_field},
94
				   lft,
95
				   rgt
96
			  FROM {$this->table_name}
97
			 WHERE lft BETWEEN :lft + 1 AND :rgt - 1
98
		  ORDER BY lft ASC",
99
			$node
100
		);
101
102
		$offset = $absolute_depth ? $this->countAncestors($id) : 0;
103
104
		$stack = array();
105
		foreach( $descendants as &$child ) {
106
107
			$child['lft'] = (int) $child['lft'];
108
			$child['rgt'] = (int) $child['rgt'];
109
110
			// if current left > right on top of stack we've gone down a level so pop the stack
111
			while( $stack && $child['lft'] > $stack[count($stack) - 1] )
0 ignored issues
show
Bug Best Practice introduced by
The expression $stack of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
112
				array_pop($stack);
113
114
			$child['depth'] = $offset + count($stack) + 1;
115
116
			// node has children so push current right value to stack
117
			if( $child['rgt'] - $child['lft'] > 1 )
118
				$stack[] = $child['rgt'];
119
120
			unset($child['lft']);
121
			unset($child['rgt']);
122
123
		}
124
125
		return $descendants;
126
127
	}
128
129
	public function insertNode( $id, $parent_id = 0 ) {
130
131
		// determine if a database transaction is currently active
132
		// if not we'll start one
133
		$transaction = $this->db->inTransaction();
134
135
		try {
136
137
			if( !$transaction )
138
				$this->db->begin();
139
140
			if( $parent_id ) {
141
142
				$parent = $this->findNodeById($parent_id);
143
144
				// shift everything up to make room for the new node
145
				$this->db->query("UPDATE {$this->table_name} SET rgt = rgt + 2 WHERE rgt >= ?", $parent['rgt']);
146
				$this->db->query("UPDATE {$this->table_name} SET lft = lft + 2 WHERE lft >= ?", $parent['rgt']);
147
148
				// single query - http://www.slideshare.net/billkarwin/models-for-hierarchical-data (slide 36)
149
				/*update table set
150
				lft = case when left >= parent_rgt then lft + 2 else lft end
151
				rgt = rgt + 2
152
				where rgt > parent_lft*/
153
154
			}
155
			else {
156
				$parent = array(
157
					'rgt' => $this->db->getOne("SELECT MAX(rgt) + 1 FROM {$this->table_name}")
158
				);
159
			}
160
161
			$this->db->execute(
162
				"UPDATE {$this->table_name} SET lft = ?, rgt = ? WHERE id = ?",
163
				array($parent['rgt'], $parent['rgt'] + 1, $id)
164
			);
165
166
			if( !$transaction )
167
				$this->db->commit();
168
169
		}
170
		catch( \Exception $e ) {
171
			if( !$transaction )
172
				$this->db->rollback();
173
			throw $e;
174
		}
175
176
		return $this;
177
178
	}
179
180
	public function removeNode( $id ) {
181
182
		if( !$node = $this->findNodeById($id) )
183
			return $this;
184
185
		// determine if a database transaction is currently active
186
		// if not we'll start one
187
		$transaction = $this->db->inTransaction();
188
189
		try {
190
191
			if( !$transaction )
192
				$this->db->begin();
193
194
			$diff = (int) $node['rgt'] - (int) $node['lft'] + 1;
195
196
			// remove the item from the tree by blanking the left and right indexes
197
			$this->db->execute("UPDATE {$this->table_name} SET lft = 0, rgt = 0 WHERE lft BETWEEN ? AND ?", array($node['lft'], $node['rgt']));
198
			
199
			$this->db->execute("UPDATE {$this->table_name} SET lft = lft - ? WHERE lft >= ?", array($diff, $node['lft']));
200
			$this->db->execute("UPDATE {$this->table_name} SET rgt = rgt - ? WHERE rgt >= ?", array($diff, $node['rgt']));
201
202
			if( !$transaction )
203
				$this->db->commit();
204
205
		}
206
		catch( \Exception $e ) {
207
			if( !$transaction )
208
				$this->db->rollback();
209
			throw $e;
210
		}
211
212
		return $this;
213
214
	}
215
216
	public function moveNode( $id, $parent_id ) {
217
218
		$node   = $this->findNodeById($id);
219
		$parent = $this->findNodeById($parent_id);
220
221
		// both source and parent must exist
222
		if( !$node || !$parent )
0 ignored issues
show
Bug Best Practice introduced by
The expression $node of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $parent of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
223
			return $this;
224
225
		// determine if a database transaction is currently active
226
		// if not we'll start one
227
		$transaction = $this->db->inTransaction();
228
229
		try {
230
231
			if( !$transaction )
232
				$this->db->begin();
233
234
			$diff = $node['rgt'] - $node['lft'] + 1;
235
236
			// remove the node and it's descendents from the tree whilst keeping it's structure, by converting the left and right values to negative numbers
237
			$this->db->execute(
238
				"UPDATE {$this->table_name} SET lft = -(lft - :lft + 1), rgt = -(rgt -:lft + 1) WHERE lft >= :lft AND rgt <= :rgt",
239
				array(
240
					'lft'  => $node['lft'],
241
					'rgt'  => $node['rgt']
242
				)
243
			);
244
245
			// collapse the gap we just created
246
			$this->db->execute("UPDATE {$this->table_name} SET lft = lft - ? WHERE lft > ?", array($diff, $node['lft']));
247
			$this->db->execute("UPDATE {$this->table_name} SET rgt = rgt - ? WHERE rgt > ?", array($diff, $node['rgt']));
248
249
			// refresh parent state
250
			$parent = $this->findNodeById($parent_id);
251
252
			// create new gap
253
			$this->db->execute("UPDATE {$this->table_name} SET lft = lft + ? WHERE lft > ?", array($diff, $parent['rgt']));
254
			$this->db->execute("UPDATE {$this->table_name} SET rgt = rgt + ? WHERE rgt >= ?", array($diff, $parent['rgt']));
255
256
			// refresh parent state
257
			$parent = $this->findNodeById($parent_id);
258
259
			// fill the gap with the original node, updating the left and right values
260
			$this->db->execute("UPDATE {$this->table_name} SET lft = ? - ? - lft - 1 WHERE lft < 0", array($parent['rgt'], $diff));
261
			$this->db->execute("UPDATE {$this->table_name} SET rgt = ? - ? - rgt - 1 WHERE rgt < 0", array($parent['rgt'], $diff));
262
263
			// ensure the correct parent is assigned - may have already by done by calling function
264
			$this->db->execute("UPDATE {$this->table_name} SET parent_id = ? WHERE id = ?", array($parent_id, $id));
265
266
			if( !$transaction )
267
				$this->db->commit();
268
269
		}
270
		catch( \Exception $e ) {
271
			if( !$transaction )
272
				$this->db->rollback();
273
			throw $e;
274
		}
275
276
		return $this;
277
278
	}
279
280
	public function getTree( $id, $max_depth = 0, $sort = false ) {
281
282
		$node = $this->findNodeById($id);
283
284
		if( !$node )
0 ignored issues
show
Bug Best Practice introduced by
The expression $node of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
285
			return array();
286
287
		// fetch all the nodes in this sub-tree and calculate their depth
288
		$data = $this->db->getAssoc(
289
			"SELECT n.id, n.{$this->name_field}, COUNT(p.id) - 1 as depth
290
			   FROM {$this->table_name} AS n,
291
			        {$this->table_name} AS p
292
			  WHERE n.lft BETWEEN p.lft AND p.rgt
293
			    AND n.lft BETWEEN ? AND ?
294
		   GROUP BY n.id
295
		   ORDER BY n.lft",
296
			array(
297
				$node['lft'],
298
				$node['rgt']
299
			)
300
		);
301
302
		// as depth is relative to the requested node we need to take into account any ancestors
303
		$max_depth += $this->countAncestors($id);
304
305
		$tree = array();
306
		$path = array();
307
		$prev = 0;
308
309
		foreach( $data as $id => $item ) {
310
311
			// too deep - not interested
312
			if( $max_depth && ($item['depth'] > $max_depth) )
313
				continue;
314
			
315
			// back up a level so remove last too items
316
			if( $item['depth'] < $prev ) {
317
				$path = array_slice($path, 0, -2);
318
			}
319
			// same level so remove previous item
320
			elseif( $item['depth'] == $prev ) {
321
				array_pop($path);
322
			}
323
324
			$path[]       = $item['name'];
325
			$item['path'] = implode('.', $path);
326
			$tree[$id]    = $item;
327
			$prev         = $item['depth'];
328
329
		}
330
331
		if( $sort ) {
332
			uasort($tree, function( $a, $b ) {
333
				return strcmp($a['path'], $b['path']);
334
			});
335
		}
336
337
		return $tree;
338
339
	}
340
341
	public function visualise( $id, $max_depth = 0, $sort = false ) {
342
343
		$tree = array();
344
		$data = $this->getTree($id, $max_depth, $sort);
345
346
		if( $data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
347
348
			$offset = reset($data);
349
			$offset = (int) $offset['depth'];
350
351
			foreach( $data as $id => $item ) {
352
				$tree[$id] = str_repeat('|-- ', (int) $item['depth'] - $offset). $item['name'];
353
			}
354
355
		}
356
357
		return $tree;
358
359
	}
360
361
	public function rebuild( $sort = false ) {
362
363
		try {
364
			$this->db->begin();
365
			$this->db->execute("UPDATE {$this->table_name} SET lft = 0, rgt = 0");
366
			$this->rebuildNode($sort, 0);
367
			$this->db->commit();
368
		}
369
		catch( \Exception $e ) {
370
			$this->db->rollback();
371
			throw $e;
372
		}
373
374
		return $this;
375
376
	}
377
378
	protected function rebuildNode( $sort = false, $id = 0, $lft = 0 ) {
379
380
		$rgt = $lft + 1;
381
382
		$order_by = $sort ? "ORDER BY {$this->name_field} ASC" : '';
383
384
		// fetch a list of children of this node
385
		$nodes = $this->db->getCol("SELECT id FROM {$this->table_name} WHERE parent_id = ? {$order_by}", $id);
386
387
		foreach( $nodes as $child ) {
388
			$rgt = $this->rebuildNode($sort, $child, $rgt);
389
		}
390
391
		$this->db->execute("UPDATE {$this->table_name} SET lft = ?, rgt = ? WHERE id = ?", array($lft, $rgt, $id));
392
393
	    return $rgt + 1;
394
395
	}
396
397
	protected function findNodeById( $id ) {
398
		return $this->db->getRow("SELECT lft, rgt FROM {$this->table_name} WHERE id = ?", $id);
399
	}
400
401
}
402
403
// EOF