Passed
Push — master ( 6aab01...0294bc )
by Aimeos
09:44
created

DBNestedSet::checkSearchConfig()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 6
nop 1
dl 0
loc 15
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2022
7
 * @package MW
8
 * @subpackage Tree
9
 */
10
11
12
namespace Aimeos\MW\Tree\Manager;
13
14
15
/**
16
 * Tree manager using nested sets stored in a database.
17
 *
18
 * @package MW
19
 * @subpackage Tree
20
 */
21
class DBNestedSet extends \Aimeos\MW\Tree\Manager\Base
22
{
23
	private $searchConfig = [];
24
	private $config;
25
	private $conn;
26
27
28
	/**
29
	 * Initializes the tree manager.
30
	 *
31
	 * The config['search] array must contain these key/array pairs suitable for \Aimeos\Base\Criteria\Attribute\Standard:
32
	 *	[id] => Array describing unique ID codes/types/labels
33
	 *	[label] => Array describing codes/types/labels for descriptive labels
34
	 *	[status] => Array describing codes/types/labels for status values
35
	 *	[parentid] => Array describing codes/types/labels for parentid values
36
	 *	[level] => Array describing codes/types/labels for height levels of tree nodes
37
	 *	[left] => Array describing codes/types/labels for nodes left values
38
	 *	[right] => Array describing codes/types/labels for nodes right values
39
	 *
40
	 * The config['sql] array must contain these statement:
41
	 *	[delete] =>
42
	 *		DELETE FROM treetable WHERE left >= ? AND right <= ?
43
	 *	[get] =>
44
	 *		SELECT node.*
45
	 *		FROM treetable AS parent, treetable AS node
46
	 *		WHERE node.left >= parent.left AND node.left <= parent.right
47
	 *		AND parent.id = ? AND node.level <= parent.level + ? AND :cond
48
	 *		ORDER BY node.left
49
	 *	[insert] =>
50
	 *		INSERT INTO treetable ( label, code, status, parentid, level, left, right ) VALUES ( ?, ?, ?, ?, ? )
51
	 *	[move-left] =>
52
	 *		UPDATE treetable
53
	 *		SET left = left + ?, level = level + ?
54
	 *		WHERE left >= ? AND left <= ?
55
	 *	[move-right] =>
56
	 *		UPDATE treetable
57
	 *		SET right = right + ?
58
	 *		WHERE right >= ? AND right <= ?
59
	 *	[search] =>
60
	 *		SELECT * FROM treetable
61
	 *		WHERE left >= ? AND right <= ? AND :cond
62
	 *		ORDER BY :order
63
	 *	[update] =>
64
	 *		UPDATE treetable SET label = ?, code = ?, status = ? WHERE id = ?
65
	 *	[update-parentid] =>
66
	 *		UPDATE treetable SET parentid = ? WHERE id = ?
67
	 *	[newid] =>
68
	 *		SELECT LAST_INSERT_ID()
69
	 *
70
	 * @param array $config Associative array holding the SQL statements
71
	 * @param \Aimeos\Base\DB\Connection\Iface $resource Database connection
72
	 */
73
	public function __construct( array $config, $resource )
74
	{
75
		if( !( $resource instanceof \Aimeos\Base\DB\Connection\Iface ) ) {
0 ignored issues
show
introduced by
$resource is always a sub-type of Aimeos\Base\DB\Connection\Iface.
Loading history...
76
			throw new \Aimeos\MW\Tree\Exception( 'Given resource isn\'t a database connection object' );
77
		}
78
79
		if( !isset( $config['search'] ) ) {
80
			throw new \Aimeos\MW\Tree\Exception( 'Search config is missing' );
81
		}
82
83
		if( !isset( $config['sql'] ) ) {
84
			throw new \Aimeos\MW\Tree\Exception( 'SQL config is missing' );
85
		}
86
87
		$this->checkSearchConfig( $config['search'] );
88
		$this->checkSqlConfig( $config['sql'] );
89
90
		$this->searchConfig = $config['search'];
91
		$this->config = $config['sql'];
92
		$this->conn = $resource;
93
	}
94
95
96
	/**
97
	 * Returns a list of attributes which can be used in the search method.
98
	 *
99
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] List of search attribute items
100
	 */
101
	public function getSearchAttributes() : array
102
	{
103
		$attributes = [];
104
105
		foreach( $this->searchConfig as $values ) {
106
			$attributes[] = new \Aimeos\Base\Criteria\Attribute\Standard( $values );
107
		}
108
109
		return $attributes;
110
	}
111
112
113
	/**
114
	 * Creates a new search object for storing search criterias.
115
	 *
116
	 * @return \Aimeos\Base\Criteria\Iface Search object instance
117
	 */
118
	public function createSearch() : \Aimeos\Base\Criteria\Iface
119
	{
120
		return new \Aimeos\Base\Criteria\SQL( $this->conn );
121
	}
122
123
124
	/**
125
	 * Creates a new node object.
126
	 *
127
	 * @return \Aimeos\MW\Tree\Node\Iface Empty node object
128
	 */
129
	public function createNode() : \Aimeos\MW\Tree\Node\Iface
130
	{
131
		return $this->createNodeBase();
132
	}
133
134
135
	/**
136
	 * Deletes a node and its descendants from the storage.
137
	 *
138
	 * @param string|null $id Delete the node with the ID and all nodes below
139
	 * @return \Aimeos\MW\Tree\Manager\Iface Manager object for method chaining
140
	 */
141
	public function deleteNode( string $id = null ) : Iface
142
	{
143
		$node = $this->getNode( $id, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
144
145
		$stmt = $this->conn->create( $this->config['delete'] );
146
		$stmt->bind( 1, $node->left, $this->searchConfig['left']['internaltype'] );
147
		$stmt->bind( 2, $node->right, $this->searchConfig['right']['internaltype'] );
148
		$stmt->execute()->finish();
149
150
		$diff = $node->right - $node->left + 1;
151
152
		$stmt = $this->conn->create( $this->config['move-left'] );
153
		$stmt->bind( 1, -$diff, $this->searchConfig['left']['internaltype'] );
154
		$stmt->bind( 2, 0, $this->searchConfig['level']['internaltype'] );
155
		$stmt->bind( 3, $node->right + 1, $this->searchConfig['left']['internaltype'] );
156
		$stmt->bind( 4, 0x7FFFFFFF, $this->searchConfig['left']['internaltype'] );
157
		$stmt->execute()->finish();
158
159
		$stmt = $this->conn->create( $this->config['move-right'] );
160
		$stmt->bind( 1, -$diff, $this->searchConfig['right']['internaltype'] );
161
		$stmt->bind( 2, $node->right + 1, $this->searchConfig['right']['internaltype'] );
162
		$stmt->bind( 3, 0x7FFFFFFF, $this->searchConfig['right']['internaltype'] );
163
		$stmt->execute()->finish();
164
165
		return $this;
166
	}
167
168
169
	/**
170
	 * Returns a node and its descendants depending on the given resource.
171
	 *
172
	 * @param string|null $id Retrieve nodes starting from the given ID
173
	 * @param int $level One of the level constants from \Aimeos\MW\Tree\Manager\Base
174
	 * @param \Aimeos\Base\Criteria\Iface|null $condition Optional criteria object with conditions
175
	 * @return \Aimeos\MW\Tree\Node\Iface Node, maybe with subnodes
176
	 */
177
	public function getNode( string $id = null, int $level = Base::LEVEL_TREE, \Aimeos\Base\Criteria\Iface $condition = null ) : \Aimeos\MW\Tree\Node\Iface
178
	{
179
		if( $id === null )
180
		{
181
			if( ( $node = $this->getRootNode() ) === null ) {
182
				throw new \Aimeos\MW\Tree\Exception( 'No root node available' );
183
			}
184
185
			if( $level === \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE ) {
186
				return $node;
187
			}
188
		}
189
		else
190
		{
191
			$node = $this->getNodeById( $id );
192
193
			if( $level === \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE ) {
194
				return $node;
195
			}
196
		}
197
198
199
		$id = $node->getId();
200
201
		$numlevel = $this->getLevelFromConstant( $level );
202
		$search = $condition ?: $this->createSearch();
203
204
		$types = $this->getSearchTypes( $this->searchConfig );
205
		$funcs = $this->getSearchFunctions( $this->searchConfig );
206
		$translations = $this->getSearchTranslations( $this->searchConfig );
207
		$conditions = $search->getConditionSource( $types, $translations, [], $funcs );
208
209
210
		$stmt = $this->conn->create( str_replace( ':cond', $conditions, $this->config['get'] ) );
211
		$stmt->bind( 1, $id, $this->searchConfig['parentid']['internaltype'] );
212
		$stmt->bind( 2, $numlevel, $this->searchConfig['level']['internaltype'] );
213
		$result = $stmt->execute();
214
215
		if( ( $row = $result->fetch() ) === null ) {
216
			throw new \Aimeos\MW\Tree\Exception( sprintf( 'No node with ID "%1$d" found', $id ) );
217
		}
218
219
		$node = $this->createNodeBase( $row );
220
		$this->createTree( $result, $node );
221
222
		return $node;
223
	}
224
225
226
	/**
227
	 * Inserts a new node before the given reference node to the parent in the storage.
228
	 *
229
	 * @param \Aimeos\MW\Tree\Node\Iface $node New node that should be inserted
230
	 * @param string|null $parentId ID of the parent node where the new node should be inserted below (null for root node)
231
	 * @param string|null $refId ID of the node where the node should be inserted before (null to append)
232
	 * @return \Aimeos\MW\Tree\Node\Iface Updated node item
233
	 */
234
	public function insertNode( \Aimeos\MW\Tree\Node\Iface $node, string $parentId = null, string $refId = null ) : \Aimeos\MW\Tree\Node\Iface
235
	{
236
		$node->parentid = $parentId;
237
238
		if( $refId !== null )
239
		{
240
			$refNode = $this->getNode( $refId, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
241
			$node->left = $refNode->left;
242
			$node->right = $refNode->left + 1;
243
			$node->level = $refNode->level;
0 ignored issues
show
Bug Best Practice introduced by
The property level does not exist on Aimeos\MW\Tree\Node\DBNestedSet. Since you implemented __get, consider adding a @property annotation.
Loading history...
244
		}
245
		else if( $parentId !== null )
246
		{
247
			$parentNode = $this->getNode( $parentId, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
248
			$node->left = $parentNode->right;
249
			$node->right = $parentNode->right + 1;
250
			$node->level = $parentNode->level + 1;
251
		}
252
		else
253
		{
254
			$node->left = 1;
255
			$node->right = 2;
256
			$node->level = 0;
257
			$node->parentid = 0;
258
259
			if( ( $root = $this->getRootNode( '-' ) ) !== null )
260
			{
261
				$node->left = $root->right + 1;
262
				$node->right = $root->right + 2;
263
			}
264
		}
265
266
267
		$stmt = $this->conn->create( $this->config['move-left'] );
268
		$stmt->bind( 1, 2, $this->searchConfig['left']['internaltype'] );
269
		$stmt->bind( 2, 0, $this->searchConfig['level']['internaltype'] );
270
		$stmt->bind( 3, $node->left, $this->searchConfig['left']['internaltype'] );
271
		$stmt->bind( 4, 0x7FFFFFFF, $this->searchConfig['left']['internaltype'] );
272
		$stmt->execute()->finish();
273
274
		$stmt = $this->conn->create( $this->config['move-right'] );
275
		$stmt->bind( 1, 2, $this->searchConfig['right']['internaltype'] );
276
		$stmt->bind( 2, $node->left, $this->searchConfig['right']['internaltype'] );
277
		$stmt->bind( 3, 0x7FFFFFFF, $this->searchConfig['right']['internaltype'] );
278
		$stmt->execute()->finish();
279
280
		$stmt = $this->conn->create( $this->config['insert'] );
281
		$stmt->bind( 1, $node->getLabel(), $this->searchConfig['label']['internaltype'] );
282
		$stmt->bind( 2, $node->getCode(), $this->searchConfig['code']['internaltype'] );
283
		$stmt->bind( 3, $node->getStatus(), $this->searchConfig['status']['internaltype'] );
284
		$stmt->bind( 4, (int) $node->parentid, $this->searchConfig['parentid']['internaltype'] );
285
		$stmt->bind( 5, $node->level, $this->searchConfig['level']['internaltype'] );
286
		$stmt->bind( 6, $node->left, $this->searchConfig['left']['internaltype'] );
287
		$stmt->bind( 7, $node->right, $this->searchConfig['right']['internaltype'] );
288
		$stmt->execute()->finish();
289
290
291
		$result = $this->conn->create( $this->config['newid'] )->execute();
292
293
		if( ( $row = $result->fetch( \Aimeos\Base\DB\Result\Base::FETCH_NUM ) ) === false ) {
294
			throw new \Aimeos\MW\Tree\Exception( sprintf( 'No new record ID available' ) );
295
		}
296
		$result->finish();
297
298
		$node->setId( $row[0] );
299
300
		return $node;
301
	}
302
303
304
	/**
305
	 * Moves an existing node to the new parent in the storage.
306
	 *
307
	 * @param string $id ID of the node that should be moved
308
	 * @param string|null $oldParentId ID of the old parent node which currently contains the node that should be removed
309
	 * @param string|null $newParentId ID of the new parent node where the node should be moved to
310
	 * @param string|null $newRefId ID of the node where the node should be inserted before (null to append)
311
	 * @return \Aimeos\MW\Tree\Manager\Iface Manager object for method chaining
312
	 */
313
	public function moveNode( string $id, string $oldParentId = null, string $newParentId = null, string $newRefId = null ) : Iface
314
	{
315
		$node = $this->getNode( $id, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
316
		$diff = $node->right - $node->left + 1;
317
318
		if( $newRefId !== null )
319
		{
320
			$refNode = $this->getNode( $newRefId, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
321
322
			$leveldiff = $refNode->level - $node->level;
0 ignored issues
show
Bug Best Practice introduced by
The property level does not exist on Aimeos\MW\Tree\Node\DBNestedSet. Since you implemented __get, consider adding a @property annotation.
Loading history...
323
324
			$openNodeLeftBegin = $refNode->left;
325
			$openNodeRightBegin = $refNode->left + 1;
326
327
			if( $refNode->left < $node->left )
328
			{
329
				$moveNodeLeftBegin = $node->left + $diff;
330
				$moveNodeLeftEnd = $node->right + $diff - 1;
331
				$moveNodeRightBegin = $node->left + $diff + 1;
332
				$moveNodeRightEnd = $node->right + $diff;
333
				$movesize = $refNode->left - $node->left - $diff;
334
			}
335
			else
336
			{
337
				$moveNodeLeftBegin = $node->left;
338
				$moveNodeLeftEnd = $node->right - 1;
339
				$moveNodeRightBegin = $node->left + 1;
340
				$moveNodeRightEnd = $node->right;
341
				$movesize = $refNode->left - $node->left;
342
			}
343
344
			$closeNodeLeftBegin = $node->left + $diff;
345
			$closeNodeRightBegin = $node->left + $diff;
346
		}
347
		else
348
		{
349
			$refNode = $this->getNode( $newParentId, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
350
351
			if( $newParentId === null )
352
			{
353
				//make virtual root
354
				if( ( $root = $this->getRootNode( '-' ) ) !== null )
355
				{
356
					$refNode->left = $root->right;
357
					$refNode->right = $root->right + 1;
358
					$refNode->level = -1;
359
				}
360
			}
361
362
			$leveldiff = $refNode->level - $node->level + 1;
363
			$openNodeLeftBegin = $refNode->right + 1;
364
			$openNodeRightBegin = $refNode->right;
365
366
			if( $refNode->right < $node->right )
367
			{
368
				$moveNodeLeftBegin = $node->left + $diff;
369
				$moveNodeLeftEnd = $node->right + $diff - 1;
370
				$moveNodeRightBegin = $node->left + $diff + 1;
371
				$moveNodeRightEnd = $node->right + $diff;
372
				$movesize = $refNode->right - $node->left - $diff;
373
			}
374
			else
375
			{
376
				$moveNodeLeftBegin = $node->left;
377
				$moveNodeLeftEnd = $node->right - 1;
378
				$moveNodeRightBegin = $node->left + 1;
379
				$moveNodeRightEnd = $node->right;
380
				$movesize = $refNode->right - $node->left;
381
			}
382
383
			$closeNodeLeftBegin = $node->left + $diff;
384
			$closeNodeRightBegin = $node->left + $diff;
385
		}
386
387
388
		$stmtLeft = $this->conn->create( $this->config['move-left'] );
389
		$stmtRight = $this->conn->create( $this->config['move-right'] );
390
		$updateParentId = $this->conn->create( $this->config['update-parentid'] );
391
		// open gap for inserting node or subtree
392
393
		$stmtLeft->bind( 1, $diff, $this->searchConfig['left']['internaltype'] );
394
		$stmtLeft->bind( 2, 0, $this->searchConfig['level']['internaltype'] );
395
		$stmtLeft->bind( 3, $openNodeLeftBegin, $this->searchConfig['left']['internaltype'] );
396
		$stmtLeft->bind( 4, 0x7FFFFFFF, $this->searchConfig['left']['internaltype'] );
397
		$stmtLeft->execute()->finish();
398
399
		$stmtRight->bind( 1, $diff, $this->searchConfig['right']['internaltype'] );
400
		$stmtRight->bind( 2, $openNodeRightBegin, $this->searchConfig['right']['internaltype'] );
401
		$stmtRight->bind( 3, 0x7FFFFFFF, $this->searchConfig['right']['internaltype'] );
402
		$stmtRight->execute()->finish();
403
404
		// move node or subtree to the new gap
405
406
		$stmtLeft->bind( 1, $movesize, $this->searchConfig['left']['internaltype'] );
407
		$stmtLeft->bind( 2, $leveldiff, $this->searchConfig['level']['internaltype'] );
408
		$stmtLeft->bind( 3, $moveNodeLeftBegin, $this->searchConfig['left']['internaltype'] );
409
		$stmtLeft->bind( 4, $moveNodeLeftEnd, $this->searchConfig['left']['internaltype'] );
410
		$stmtLeft->execute()->finish();
411
412
		$stmtRight->bind( 1, $movesize, $this->searchConfig['right']['internaltype'] );
413
		$stmtRight->bind( 2, $moveNodeRightBegin, $this->searchConfig['right']['internaltype'] );
414
		$stmtRight->bind( 3, $moveNodeRightEnd, $this->searchConfig['right']['internaltype'] );
415
		$stmtRight->execute()->finish();
416
417
		// close gap opened by moving the node or subtree to the new location
418
419
		$stmtLeft->bind( 1, -$diff, $this->searchConfig['left']['internaltype'] );
420
		$stmtLeft->bind( 2, 0, $this->searchConfig['level']['internaltype'] );
421
		$stmtLeft->bind( 3, $closeNodeLeftBegin, $this->searchConfig['left']['internaltype'] );
422
		$stmtLeft->bind( 4, 0x7FFFFFFF, $this->searchConfig['left']['internaltype'] );
423
		$stmtLeft->execute()->finish();
424
425
		$stmtRight->bind( 1, -$diff, $this->searchConfig['right']['internaltype'] );
426
		$stmtRight->bind( 2, $closeNodeRightBegin, $this->searchConfig['right']['internaltype'] );
427
		$stmtRight->bind( 3, 0x7FFFFFFF, $this->searchConfig['right']['internaltype'] );
428
		$stmtRight->execute()->finish();
429
430
431
		$updateParentId->bind( 1, $newParentId, $this->searchConfig['parentid']['internaltype'] );
432
		$updateParentId->bind( 2, $id, $this->searchConfig['id']['internaltype'] );
433
		$updateParentId->execute()->finish();
434
435
		return $this;
436
	}
437
438
439
	/**
440
	 * Stores the values of the given node to the storage.
441
	 *
442
	 * This method does only store values like the node label but doesn't change
443
	 * the tree layout by adding, moving or deleting nodes.
444
	 *
445
	 * @param \Aimeos\MW\Tree\Node\Iface $node Tree node object
446
	 * @return \Aimeos\MW\Tree\Node\Iface Updated node item
447
	 */
448
	public function saveNode( \Aimeos\MW\Tree\Node\Iface $node ) : \Aimeos\MW\Tree\Node\Iface
449
	{
450
		if( $node->getId() === null ) {
451
			throw new \Aimeos\MW\Tree\Exception( sprintf( 'Unable to save newly created nodes, use insert method instead' ) );
452
		}
453
454
		if( $node->isModified() === false ) {
455
			return $node;
456
		}
457
458
		$stmt = $this->conn->create( $this->config['update'] );
459
		$stmt->bind( 1, $node->getLabel(), $this->searchConfig['label']['internaltype'] );
460
		$stmt->bind( 2, $node->getCode(), $this->searchConfig['code']['internaltype'] );
461
		$stmt->bind( 3, $node->getStatus(), $this->searchConfig['status']['internaltype'] );
462
		$stmt->bind( 4, $node->getId(), $this->searchConfig['id']['internaltype'] );
463
		$stmt->execute()->finish();
464
465
		return $node;
466
	}
467
468
469
	/**
470
	 * Retrieves a list of nodes from the storage matching the given search criteria.
471
	 *
472
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
473
	 * @param string|null $id Search nodes starting at the node with the given ID
474
	 * @return \Aimeos\MW\Tree\Node\Iface[] List of tree nodes
475
	 */
476
	public function searchNodes( \Aimeos\Base\Criteria\Iface $search, string $id = null ) : array
477
	{
478
		$left = 1;
479
		$right = 0x7FFFFFFF;
480
481
		if( $id !== null )
482
		{
483
			$node = $this->getNodeById( $id );
484
485
			$left = $node->left;
486
			$right = $node->right;
487
		}
488
489
		if( $search->getSortations() === [] ) {
490
			$search->setSortations( [$search->sort( '+', $this->searchConfig['left']['code'] )] );
491
		}
492
493
		$types = $this->getSearchTypes( $this->searchConfig );
494
		$funcs = $this->getSearchFunctions( $this->searchConfig );
495
		$translations = $this->getSearchTranslations( $this->searchConfig );
496
		$conditions = $search->getConditionSource( $types, $translations, [], $funcs );
497
		$sortations = $search->getSortationSource( $types, $translations, $funcs );
498
499
		$sql = str_replace(
500
			[':cond', ':order', ':size', ':start'],
501
			[$conditions, $sortations, $search->getLimit(), $search->getOffset()],
502
			$this->config['search']
503
		);
504
505
		$stmt = $this->conn->create( $sql );
506
		$stmt->bind( 1, $left, $this->searchConfig['left']['internaltype'] );
507
		$stmt->bind( 2, $right, $this->searchConfig['right']['internaltype'] );
508
		$result = $stmt->execute();
509
510
		try
511
		{
512
			$nodes = [];
513
			while( ( $row = $result->fetch() ) !== null ) {
514
				$nodes[$row['id']] = $this->createNodeBase( $row );
515
			}
516
		}
517
		catch( \Exception $e )
518
		{
519
			$result->finish();
520
			throw $e;
521
		}
522
523
		return $nodes;
524
	}
525
526
527
	/**
528
	 * Returns a list if node IDs, that are in the path of given node ID.
529
	 *
530
	 * @param string $id ID of node to get the path for
531
	 * @return \Aimeos\MW\Tree\Node\Iface[] List of tree nodes
532
	 */
533
	public function getPath( string $id ) : array
534
	{
535
		$result = [];
536
		$node = $this->getNode( $id, \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE );
537
538
		$search = $this->createSearch();
539
540
		$expr = array(
541
			$search->compare( '<=', $this->searchConfig['left']['code'], $node->left ),
542
			$search->compare( '>=', $this->searchConfig['right']['code'], $node->right ),
543
		);
544
545
		$search->setConditions( $search->and( $expr ) );
546
		$search->setSortations( array( $search->sort( '+', $this->searchConfig['left']['code'] ) ) );
547
548
		$results = $this->searchNodes( $search );
549
550
		foreach( $results as $item ) {
551
			$result[$item->getId()] = $item;
552
		}
553
554
		return $result;
555
	}
556
557
558
	/**
559
	 * Checks if all required search configurations are available.
560
	 *
561
	 * @param array $config Associative list of search configurations
562
	 * @throws \Aimeos\MW\Tree\Exception If one ore more search configurations are missing
563
	 */
564
	protected function checkSearchConfig( array $config )
565
	{
566
		$required = array( 'id', 'label', 'status', 'level', 'left', 'right' );
567
568
		foreach( $required as $key => $entry )
569
		{
570
			if( isset( $config[$entry] ) ) {
571
				unset( $required[$key] );
572
			}
573
		}
574
575
		if( count( $required ) > 0 )
576
		{
577
			$msg = 'Search config in given configuration are missing: "%1$s"';
578
			throw new \Aimeos\MW\Tree\Exception( sprintf( $msg, implode( ', ', $required ) ) );
579
		}
580
	}
581
582
583
	/**
584
	 * Checks if all required SQL statements are available.
585
	 *
586
	 * @param array $config Associative list of SQL statements
587
	 * @throws \Aimeos\MW\Tree\Exception If one ore more SQL statements are missing
588
	 */
589
	protected function checkSqlConfig( array $config )
590
	{
591
		$required = array(
592
			'delete', 'get', 'insert', 'move-left',
593
			'move-right', 'search', 'update', 'newid'
594
		);
595
596
		foreach( $required as $key => $entry )
597
		{
598
			if( isset( $config[$entry] ) ) {
599
				unset( $required[$key] );
600
			}
601
		}
602
603
		if( count( $required ) > 0 )
604
		{
605
			$msg = 'SQL statements in given configuration are missing: "%1$s"';
606
			throw new \Aimeos\MW\Tree\Exception( sprintf( $msg, implode( ', ', $required ) ) );
607
		}
608
	}
609
610
611
	/**
612
	 * Creates a new node object.
613
	 *
614
	 * @param array $values List of attributes that should be stored in the new node
615
	 * @param \Aimeos\MW\Tree\Node\Iface[] $children List of child nodes
616
	 * @return \Aimeos\MW\Tree\Node\Iface Empty node object
617
	 */
618
	protected function createNodeBase( array $values = [], array $children = [] ) : \Aimeos\MW\Tree\Node\Iface
619
	{
620
		return new \Aimeos\MW\Tree\Node\DBNestedSet( $values, $children );
621
	}
622
623
624
	/**
625
	 * Creates a tree from the result set returned by the database.
626
	 *
627
	 * @param \Aimeos\Base\DB\Result\Iface $result Database result
628
	 * @param \Aimeos\MW\Tree\Node\Iface $node Current node to add children to
629
	 */
630
	protected function createTree( \Aimeos\Base\DB\Result\Iface $result, \Aimeos\MW\Tree\Node\Iface $node ) : ?\Aimeos\MW\Tree\Node\Iface
631
	{
632
		while( ( $record = $result->fetch() ) !== null )
633
		{
634
			$newNode = $this->createNodeBase( $record );
635
636
			while( $this->isChild( $newNode, $node ) )
637
			{
638
				$node->addChild( $newNode );
639
640
				if( ( $newNode = $this->createTree( $result, $newNode ) ) === null ) {
641
					return null;
642
				}
643
			}
644
645
			return $newNode;
646
		}
647
648
		return null;
649
	}
650
651
652
	/**
653
	 * Tests if the first node is a child of the second node.
654
	 *
655
	 * @param \Aimeos\MW\Tree\Node\Iface $node Node to test
656
	 * @param \Aimeos\MW\Tree\Node\Iface $parent Parent node
657
	 * @return bool True if not is a child of the second node, false if not
658
	 */
659
	protected function isChild( \Aimeos\MW\Tree\Node\Iface $node, \Aimeos\MW\Tree\Node\Iface $parent ) : bool
660
	{
661
		return $node->__get( 'left' ) > $parent->__get( 'left' ) && $node->__get( 'right' ) < $parent->__get( 'right' );
662
	}
663
664
665
	/**
666
	 * Converts the level constant to the depth of the tree.
667
	 *
668
	 * @param int $level Level constant from \Aimeos\MW\Tree\Manager\Base
669
	 * @return int Number of tree levels
670
	 * @throws \Aimeos\MW\Tree\Exception if level constant is invalid
671
	 */
672
	protected function getLevelFromConstant( int $level ) : int
673
	{
674
		switch( $level )
675
		{
676
			case \Aimeos\MW\Tree\Manager\Base::LEVEL_ONE:
677
				return 0;
678
			case \Aimeos\MW\Tree\Manager\Base::LEVEL_LIST:
679
				return 1;
680
			case \Aimeos\MW\Tree\Manager\Base::LEVEL_TREE:
681
				return 0x3FFF; // max. possible level / 2 to prevent smallint overflow
682
			default:
683
				throw new \Aimeos\MW\Tree\Exception( sprintf( 'Invalid level constant "%1$d"', $level ) );
684
		}
685
	}
686
687
688
	/**
689
	 * Returns a single node identified by its ID.
690
	 *
691
	 * @param string $id Unique ID
692
	 * @return \Aimeos\MW\Tree\Node\Iface Tree node
693
	 * @throws \Aimeos\MW\Tree\Exception If node is not found
694
	 * @throws \Exception If anything unexcepted occurs
695
	 */
696
	protected function getNodeById( string $id ) : \Aimeos\MW\Tree\Node\Iface
697
	{
698
		$stmt = $this->conn->create( str_replace( ':cond', '1=1', $this->config['get'] ) );
699
		$stmt->bind( 1, $id, $this->searchConfig['parentid']['internaltype'] );
700
		$stmt->bind( 2, 0, $this->searchConfig['level']['internaltype'] );
701
		$result = $stmt->execute();
702
703
		if( ( $row = $result->fetch() ) === null ) {
704
			throw new \Aimeos\MW\Tree\Exception( sprintf( 'No node with ID "%1$d" found', $id ) );
705
		}
706
707
		return $this->createNodeBase( $row );
708
	}
709
710
711
	/**
712
	 * Returns the first tree root node depending on the sorting direction.
713
	 *
714
	 * @param string $sort Sort direction, '+' is ascending, '-' is descending
715
	 * @return \Aimeos\MW\Tree\Node\Iface|null Tree root node
716
	 */
717
	protected function getRootNode( string $sort = '+' ) : ?\Aimeos\MW\Tree\Node\Iface
718
	{
719
		$search = $this->createSearch();
720
		$search->setConditions( $search->compare( '==', $this->searchConfig['level']['code'], 0 ) );
721
		$search->setSortations( array( $search->sort( $sort, $this->searchConfig['left']['code'] ) ) );
722
		$nodes = $this->searchNodes( $search );
723
724
		if( ( $node = reset( $nodes ) ) !== false ) {
725
			return $node;
726
		}
727
728
		return null;
729
	}
730
}
731