Completed
Push — v4-develop ( 1f585a...56f600 )
by Bartko
04:57
created

Manipulator::updateParentId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 10
cts 10
cp 1
rs 9.7
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace StefanoTree\NestedSet\Manipulator;
6
7
use StefanoTree\NestedSet\Adapter\AdapterInterface;
8
use StefanoTree\NestedSet\NodeInfo;
9
use StefanoTree\NestedSet\Options;
10
11
class Manipulator implements ManipulatorInterface
12
{
13
    private $adapter;
14
15
    private $options;
16
17
    private $dbSelectBuilder;
18
19 118
    public function __construct(Options $options, AdapterInterface $adapter)
20
    {
21 118
        $this->setOptions($options);
22 118
        $this->setAdapter($adapter);
23 118
    }
24
25
    /**
26
     * @param \StefanoTree\NestedSet\Adapter\AdapterInterface $adapter
27
     */
28 118
    private function setAdapter(AdapterInterface $adapter): void
29
    {
30 118
        $this->adapter = $adapter;
31 118
    }
32
33
    /**
34
     * @return AdapterInterface
35
     */
36 115
    public function getAdapter(): AdapterInterface
37
    {
38 115
        return $this->adapter;
39
    }
40
41
    /**
42
     * @param Options $options
43
     */
44 118
    protected function setOptions(Options $options): void
45
    {
46 118
        $this->options = $options;
47 118
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52 106
    public function getOptions(): Options
53
    {
54 106
        return $this->options;
55
    }
56
57
    /**
58
     * Data cannot contain keys like idColumnName, levelColumnName, ...
59
     *
60
     * @param array $data
61
     *
62
     * @return array
63
     */
64 5
    protected function cleanData(array $data): array
65
    {
66 5
        $options = $this->getOptions();
67
68
        $disallowedDataKeys = array(
69 5
            $options->getIdColumnName(),
70 5
            $options->getLeftColumnName(),
71 5
            $options->getRightColumnName(),
72 5
            $options->getLevelColumnName(),
73 5
            $options->getParentIdColumnName(),
74
        );
75
76 5
        if (null !== $options->getScopeColumnName()) {
77 2
            $disallowedDataKeys[] = $options->getScopeColumnName();
78
        }
79
80 5
        return array_diff_key($data, array_flip($disallowedDataKeys));
81
    }
82
83
    /**
84
     * @param array $data
85
     *
86
     * @return NodeInfo
87
     */
88 56
    protected function _buildNodeInfoObject(array $data)
89
    {
90 56
        $options = $this->getOptions();
91
92 56
        $id = $data[$options->getIdColumnName()];
93 56
        $parentId = $data[$options->getParentIdColumnName()];
94 56
        $level = (int) $data[$options->getLevelColumnName()];
95 56
        $left = (int) $data[$options->getLeftColumnName()];
96 56
        $right = (int) $data[$options->getRightColumnName()];
97
98 56
        if (isset($data[$options->getScopeColumnName()])) {
99 21
            $scope = $data[$options->getScopeColumnName()];
100
        } else {
101 35
            $scope = null;
102
        }
103
104 56
        return new NodeInfo($id, $parentId, $level, $left, $right, $scope);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 5
    public function setDbSelectBuilder(callable $selectBuilder): void
111
    {
112 5
        $this->dbSelectBuilder = $selectBuilder;
113 5
    }
114
115
    /**
116
     * @return callable
117
     */
118 28
    public function getDbSelectBuilder(): callable
119
    {
120 28
        if (null === $this->dbSelectBuilder) {
121
            $this->dbSelectBuilder = function () {
122 25
                return $this->getBlankDbSelect();
123
            };
124
        }
125
126 28
        return $this->dbSelectBuilder;
127
    }
128
129
    /**
130
     * @return string
131
     */
132 80
    public function getBlankDbSelect(): string
133
    {
134 80
        return 'SELECT * FROM '.$this->getAdapter()->quoteIdentifier($this->getOptions()->getTableName()).' ';
135
    }
136
137
    /**
138
     * Return default db select.
139
     *
140
     * @return string
141
     */
142 28
    public function getDefaultDbSelect()
143
    {
144 28
        return $this->getDbSelectBuilder()();
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 32
    public function lockTree(): void
151
    {
152 32
        $options = $this->getOptions();
153 32
        $adapter = $this->getAdapter();
154
155 32
        $sql = 'SELECT '.$adapter->quoteIdentifier($options->getIdColumnName())
156 32
            .' FROM '.$adapter->quoteIdentifier($options->getTableName())
157 32
            .' FOR UPDATE';
158
159 32
        $adapter->executeSQL($sql);
160 32
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 32
    public function beginTransaction(): void
166
    {
167 32
        $this->getAdapter()
168 32
             ->beginTransaction();
169 32
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174 17
    public function commitTransaction(): void
175
    {
176 17
        $this->getAdapter()
177 17
             ->commitTransaction();
178 17
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 16
    public function rollbackTransaction(): void
184
    {
185 16
        $this->getAdapter()
186 16
             ->rollbackTransaction();
187 16
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 5
    public function update($nodeId, array $data): void
193
    {
194 5
        $options = $this->getOptions();
195 5
        $adapter = $this->getAdapter();
196 5
        $data = $this->cleanData($data);
197
198
        $setPart = array_map(function ($item) use ($adapter) {
199 5
            return $adapter->quoteIdentifier($item).' = :'.$item;
200 5
        }, array_keys($data));
201
202 5
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
203 5
            .' SET '.implode(', ', $setPart)
204 5
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeID';
205
206 5
        $data['__nodeID'] = $nodeId;
207
208 5
        $adapter->executeSQL($sql, $data);
209 5
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 14
    public function insert(NodeInfo $nodeInfo, array $data)
215
    {
216 14
        $options = $this->getOptions();
217
218 14
        $adapter = $this->getAdapter();
219
220 14
        $data[$options->getParentIdColumnName()] = $nodeInfo->getParentId();
221 14
        $data[$options->getLevelColumnName()] = $nodeInfo->getLevel();
222 14
        $data[$options->getLeftColumnName()] = $nodeInfo->getLeft();
223 14
        $data[$options->getRightColumnName()] = $nodeInfo->getRight();
224
225 14
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
226 4
            $data[$options->getScopeColumnName()] = $nodeInfo->getScope();
227
        }
228
229
        $columns = array_map(function ($item) use ($adapter) {
230 14
            return $adapter->quoteIdentifier($item);
231 14
        }, array_keys($data));
232
233
        $values = array_map(function ($item) {
234 14
            return ':'.$item;
235 14
        }, array_keys($data));
236
237 14
        $sql = 'INSERT INTO '.$adapter->quoteIdentifier($options->getTableName())
238 14
            .' ('.implode(', ', $columns).')'
239 14
            .' VALUES('.implode(', ', $values).')';
240
241 14
        return $adapter->executeInsertSQL($sql, $data);
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247 4
    public function delete($nodeId): void
248
    {
249 4
        $options = $this->getOptions();
250 4
        $adapter = $this->getAdapter();
251
252 4
        $sql = 'DELETE FROM '.$adapter->quoteIdentifier($options->getTableName())
253 4
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeID';
254
255
        $params = array(
256 4
            '__nodeID' => $nodeId,
257
        );
258
259 4
        $adapter->executeSQL($sql, $params);
260 4
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265 15
    public function moveLeftIndexes($fromIndex, $shift, $scope = null): void
266
    {
267 15
        $options = $this->getOptions();
268
269 15
        if (0 == $shift) {
270
            return;
271
        }
272
273 15
        $adapter = $this->getAdapter();
274
275
        $params = array(
276 15
            ':shift' => $shift,
277 15
            ':fromIndex' => $fromIndex,
278
        );
279
280 15
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
281 15
            .' SET '
282 15
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' = '
283 15
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' + :shift'
284 15
            .' WHERE '
285 15
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' > :fromIndex';
286
287 15
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
288 4
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
289 4
            $params['__scope'] = $scope;
290
        }
291
292 15
        $adapter->executeSQL($sql, $params);
293 15
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 15
    public function moveRightIndexes($fromIndex, $shift, $scope = null): void
299
    {
300 15
        $options = $this->getOptions();
301
302 15
        if (0 == $shift) {
303
            return;
304
        }
305
306 15
        $adapter = $this->getAdapter();
307
308
        $params = array(
309 15
            ':shift' => $shift,
310 15
            ':fromIndex' => $fromIndex,
311
        );
312
313 15
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
314 15
            .' SET '
315 15
            .$adapter->quoteIdentifier($options->getRightColumnName()).' = '
316 15
            .$adapter->quoteIdentifier($options->getRightColumnName()).' + :shift'
317 15
            .' WHERE '
318 15
            .$adapter->quoteIdentifier($options->getRightColumnName()).' > :fromIndex';
319
320 15
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
321 4
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
322 4
            $params['__scope'] = $scope;
323
        }
324
325 15
        $adapter->executeSQL($sql, $params);
326 15
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331 5
    public function updateParentId($nodeId, $newParentId): void
332
    {
333 5
        $options = $this->getOptions();
334
335 5
        $adapter = $this->getAdapter();
336
337 5
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
338 5
            .' SET '.$adapter->quoteIdentifier($options->getParentIdColumnName()).' = :__parentId'
339 5
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeId';
340
341
        $params = array(
342 5
            '__parentId' => $newParentId,
343 5
            '__nodeId' => $nodeId,
344
        );
345
346 5
        $adapter->executeSQL($sql, $params);
347 5
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352 6
    public function updateLevels(int $leftIndexFrom, int $rightIndexTo, int $shift, $scope = null): void
353
    {
354 6
        $options = $this->getOptions();
355
356 6
        if (0 == $shift) {
357
            return;
358
        }
359
360 6
        $adapter = $this->getAdapter();
361
362
        $binds = array(
363 6
            ':shift' => $shift,
364 6
            ':leftFrom' => $leftIndexFrom,
365 6
            ':rightTo' => $rightIndexTo,
366
        );
367
368 6
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
369 6
            .' SET '
370 6
            .$adapter->quoteIdentifier($options->getLevelColumnName()).' = '
371 6
            .$adapter->quoteIdentifier($options->getLevelColumnName()).' + :shift'
372 6
            .' WHERE '
373 6
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' >= :leftFrom'
374 6
            .' AND '.$adapter->quoteIdentifier($options->getRightColumnName()).' <= :rightTo';
375
376 6
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
377 1
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
378 1
            $binds['__scope'] = $scope;
379
        }
380
381 6
        $adapter->executeSQL($sql, $binds);
382 6
    }
383
384
    /**
385
     * {@inheritdoc}
386
     */
387 7
    public function moveBranch(int $leftIndexFrom, int $rightIndexTo, int $shift, $scope = null): void
388
    {
389 7
        if (0 == $shift) {
390
            return;
391
        }
392
393 7
        $options = $this->getOptions();
394
395 7
        $adapter = $this->getAdapter();
396
397
        $binds = array(
398 7
            ':shift' => $shift,
399 7
            ':leftFrom' => $leftIndexFrom,
400 7
            ':rightTo' => $rightIndexTo,
401
        );
402
403 7
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
404 7
            .' SET '
405 7
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' = '
406 7
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' + :shift, '
407 7
            .$adapter->quoteIdentifier($options->getRightColumnName()).' = '
408 7
            .$adapter->quoteIdentifier($options->getRightColumnName()).' + :shift'
409 7
            .' WHERE '
410 7
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' >= :leftFrom'
411 7
            .' AND '.$adapter->quoteIdentifier($options->getRightColumnName()).' <= :rightTo';
412
413 7
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
414 2
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
415 2
            $binds['__scope'] = $scope;
416
        }
417
418 7
        $adapter->executeSQL($sql, $binds);
419 7
    }
420
421
    /**
422
     * {@inheritdoc}
423
     */
424 12
    public function getRoots($scope = null): array
425
    {
426 12
        $options = $this->getOptions();
427
428 12
        $adapter = $this->getAdapter();
429
430 12
        $params = array();
431
432 12
        $sql = $this->getBlankDbSelect();
433 12
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getParentIdColumnName(true)).' IS NULL';
434
435 12
        if (null != $scope && $options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
436 3
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName(true)).' = :__scope';
437 3
            $params['__scope'] = $scope;
438
        }
439
440 12
        $sql .= ' ORDER BY '.$adapter->quoteIdentifier($options->getIdColumnName(true)).' ASC';
441
442 12
        return $adapter->executeSelectSQL($sql, $params);
443
    }
444
445
    /**
446
     * {@inheritdoc}
447
     */
448 9
    public function getRoot($scope = null): array
449
    {
450 9
        $roots = $this->getRoots($scope);
451
452 9
        return (0 < count($roots)) ? $roots[0] : array();
453
    }
454
455
    /**
456
     * {@inheritdoc}
457
     */
458 5
    public function getNode($nodeId): ?array
459
    {
460 5
        $options = $this->getOptions();
461 5
        $nodeId = (int) $nodeId;
462 5
        $adapter = $this->getAdapter();
463
464
        $params = array(
465 5
            '__nodeID' => $nodeId,
466
        );
467
468 5
        $sql = $this->getDefaultDbSelect();
469 5
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName(true)).' = :__nodeID';
470
471 5
        $result = $adapter->executeSelectSQL($sql, $params);
472
473 5
        return (0 < count($result)) ? $result[0] : null;
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479 63
    public function getNodeInfo($nodeId): ?NodeInfo
480
    {
481 63
        $options = $this->getOptions();
482 63
        $adapter = $this->getAdapter();
483
484
        $params = array(
485 63
            '__nodeID' => $nodeId,
486
        );
487
488 63
        $sql = $this->getBlankDbSelect();
489 63
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName(true)).' = :__nodeID';
490
491 63
        $array = $adapter->executeSelectSQL($sql, $params);
492
493 63
        $result = ($array) ? $this->_buildNodeInfoObject($array[0]) : null;
494
495 63
        return $result;
496
    }
497
498
    /**
499
     * {@inheritdoc}
500
     */
501 7
    public function getChildrenNodeInfo($parentNodeId): array
502
    {
503 7
        $adapter = $this->getAdapter();
504 7
        $options = $this->getOptions();
505
506
        $params = array(
507 7
            '__parentID' => $parentNodeId,
508
        );
509
510
        $sql = 'SELECT *'
511 7
            .' FROM '.$adapter->quoteIdentifier($this->getOptions()->getTableName())
512 7
            .' WHERE '.$adapter->quoteIdentifier($options->getParentIdColumnName(true)).' = :__parentID'
513 7
            .' ORDER BY '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' ASC';
514
515 7
        $data = $adapter->executeSelectSQL($sql, $params);
516
517 7
        $result = array();
518 7
        foreach ($data as $nodeData) {
519 6
            $result[] = $this->_buildNodeInfoObject($nodeData);
520
        }
521
522 7
        return $result;
523
    }
524
525
    /**
526
     * {@inheritdoc}
527
     */
528 3
    public function updateNodeMetadata(NodeInfo $nodeInfo): void
529
    {
530 3
        $adapter = $this->getAdapter();
531 3
        $options = $this->getOptions();
532
533
        $data = array(
534 3
            $options->getRightColumnName() => $nodeInfo->getRight(),
535 3
            $options->getLeftColumnName() => $nodeInfo->getLeft(),
536 3
            $options->getLevelColumnName() => $nodeInfo->getLevel(),
537
        );
538
539
        $setPart = array_map(function ($item) use ($adapter) {
540 3
            return $adapter->quoteIdentifier($item).' = :'.$item;
541 3
        }, array_keys($data));
542
543 3
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
544 3
            .' SET '.implode(', ', $setPart)
545 3
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeID';
546
547 3
        $data['__nodeID'] = $nodeInfo->getId();
548
549 3
        $adapter->executeSQL($sql, $data);
550 3
    }
551
552
    /**
553
     * {@inheritdoc}
554
     */
555 11
    public function getAncestors($nodeId, int $startLevel = 0, int $excludeLastNLevels = 0): array
556
    {
557 11
        $options = $this->getOptions();
558
559
        // node does not exist
560 11
        $nodeInfo = $this->getNodeInfo($nodeId);
561 11
        if (!$nodeInfo) {
562 2
            return array();
563
        }
564
565 9
        $adapter = $this->getAdapter();
566
567
        $params = array(
568 9
            '__leftIndex' => $nodeInfo->getLeft(),
569 9
            '__rightIndex' => $nodeInfo->getRight(),
570
        );
571
572 9
        $sql = $this->getDefaultDbSelect();
573
574 9
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' <= :__leftIndex'
575 9
            .' AND '.$adapter->quoteIdentifier($options->getRightColumnName(true)).' >= :__rightIndex';
576
577 9
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
578 3
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName(true)).' = :__scope';
579 3
            $params['__scope'] = $nodeInfo->getScope();
580
        }
581
582 9
        if (0 < $startLevel) {
583 3
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getLevelColumnName(true)).' >= :__startLevel';
584 3
            $params['__startLevel'] = $startLevel;
585
        }
586
587 9
        if (0 < $excludeLastNLevels) {
588 4
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getLevelColumnName(true)).' <= :__excludeLastNLevels';
589 4
            $params['__excludeLastNLevels'] = $nodeInfo->getLevel() - $excludeLastNLevels;
590
        }
591
592 9
        $sql .= ' ORDER BY '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' ASC';
593
594 9
        return $adapter->executeSelectSQL($sql, $params);
595
    }
596
597
    /**
598
     * {@inheritdoc}
599
     */
600 17
    public function getDescendants($nodeId, int $startLevel = 0, ?int $levels = null, $excludeBranch = null): array
601
    {
602 17
        $options = $this->getOptions();
603
604 17
        if (!$nodeInfo = $this->getNodeInfo($nodeId)) {
605 3
            return array();
606
        }
607
608 14
        $adapter = $this->getAdapter();
609 14
        $sql = $this->getDefaultDbSelect();
610
611 14
        $params = array();
612 14
        $wherePart = array();
613
614 14
        if ($options->getScopeColumnName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->getScopeColumnName() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
615 4
            $wherePart[] = $adapter->quoteIdentifier($options->getScopeColumnName(true)).' = :__scope';
616 4
            $params['__scope'] = $nodeInfo->getScope();
617
        }
618
619 14
        if (0 != $startLevel) {
620 7
            $wherePart[] = $adapter->quoteIdentifier($options->getLevelColumnName(true)).' >= :__level';
621 7
            $params['__level'] = $nodeInfo->getLevel() + $startLevel;
622
        }
623
624 14
        if (null != $levels) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $levels of type null|integer against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
625 5
            $wherePart[] = $adapter->quoteIdentifier($options->getLevelColumnName(true)).' < :__endLevel';
626 5
            $params['__endLevel'] = $nodeInfo->getLevel() + $startLevel + abs($levels);
627
        }
628
629 14
        if (null != $excludeBranch && null != ($excludeNodeInfo = $this->getNodeInfo($excludeBranch))) {
630 3
            $wherePart[] = '( '
631 3
                .$adapter->quoteIdentifier($options->getLeftColumnName(true)).' BETWEEN :__l1 AND :__p1'
632 3
                .' OR '
633 3
                .$adapter->quoteIdentifier($options->getLeftColumnName(true)).' BETWEEN :__l2 AND :__p2'
634 3
                .') AND ('
635 3
                .$adapter->quoteIdentifier($options->getRightColumnName(true)).' BETWEEN :__l3 AND :__p3'
636 3
                .' OR '
637 3
                .$adapter->quoteIdentifier($options->getRightColumnName(true)).' BETWEEN :__l4 AND :__p4'
638 3
                .')';
639
640 3
            $params['__l1'] = $nodeInfo->getLeft();
641 3
            $params['__p1'] = $excludeNodeInfo->getLeft() - 1;
642 3
            $params['__l2'] = $excludeNodeInfo->getRight() + 1;
643 3
            $params['__p2'] = $nodeInfo->getRight();
644 3
            $params['__l3'] = $excludeNodeInfo->getRight() + 1;
645 3
            $params['__p3'] = $nodeInfo->getRight();
646 3
            $params['__l4'] = $nodeInfo->getLeft();
647 3
            $params['__p4'] = $excludeNodeInfo->getLeft() - 1;
648
        } else {
649 12
            $wherePart[] = $adapter->quoteIdentifier($options->getLeftColumnName(true)).' >= :__left'
650 12
                .' AND '.$adapter->quoteIdentifier($options->getRightColumnName(true)).' <= :__right';
651
652 12
            $params['__left'] = $nodeInfo->getLeft();
653 12
            $params['__right'] = $nodeInfo->getRight();
654
        }
655
656 14
        $sql .= ' WHERE '.implode(' AND ', $wherePart);
657 14
        $sql .= ' ORDER BY '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' ASC';
658
659 14
        $result = $adapter->executeSelectSQL($sql, $params);
660
661 14
        return (0 < count($result)) ? $result : array();
662
    }
663
}
664