Completed
Push — master ( a98c98...92b268 )
by Bartko
02:00
created

Manipulator::lockTree()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 0
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 119
    public function __construct(Options $options, AdapterInterface $adapter)
18
    {
19 119
        $this->setOptions($options);
20 119
        $this->setAdapter($adapter);
21 119
    }
22
23
    /**
24
     * @param \StefanoTree\NestedSet\Adapter\AdapterInterface $adapter
25
     */
26 119
    private function setAdapter(AdapterInterface $adapter): void
27
    {
28 119
        $this->adapter = $adapter;
29 119
    }
30
31
    /**
32
     * @return AdapterInterface
33
     */
34 116
    public function getAdapter(): AdapterInterface
35
    {
36 116
        return $this->adapter;
37
    }
38
39
    /**
40
     * @param Options $options
41
     */
42 119
    protected function setOptions(Options $options): void
43
    {
44 119
        $this->options = $options;
45 119
    }
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 107
    public function getOptions(): Options
51
    {
52 107
        return $this->options;
53
    }
54
55
    /**
56
     * Data cannot contain keys like idColumnName, levelColumnName, ...
57
     *
58
     * @param array $data
59
     *
60
     * @return array
61
     */
62 5
    protected function cleanData(array $data): array
63
    {
64 5
        $options = $this->getOptions();
65
66
        $disallowedDataKeys = array(
67 5
            $options->getIdColumnName(),
68 5
            $options->getLeftColumnName(),
69 5
            $options->getRightColumnName(),
70 5
            $options->getLevelColumnName(),
71 5
            $options->getParentIdColumnName(),
72
        );
73
74 5
        if (null !== $options->getScopeColumnName()) {
75 2
            $disallowedDataKeys[] = $options->getScopeColumnName();
76
        }
77
78 5
        return array_diff_key($data, array_flip($disallowedDataKeys));
79
    }
80
81
    /**
82
     * @param array $data
83
     *
84
     * @return NodeInfo
85
     */
86 57
    protected function _buildNodeInfoObject(array $data)
87
    {
88 57
        $options = $this->getOptions();
89
90 57
        $id = $data[$options->getIdColumnName()];
91 57
        $parentId = $data[$options->getParentIdColumnName()];
92 57
        $level = (int) $data[$options->getLevelColumnName()];
93 57
        $left = (int) $data[$options->getLeftColumnName()];
94 57
        $right = (int) $data[$options->getRightColumnName()];
95
96 57
        if (isset($data[$options->getScopeColumnName()])) {
97 22
            $scope = $data[$options->getScopeColumnName()];
98
        } else {
99 35
            $scope = null;
100
        }
101
102 57
        return new NodeInfo($id, $parentId, $level, $left, $right, $scope);
103
    }
104
105
    /**
106
     * @return callable
107
     */
108 29
    public function getDbSelectBuilder(): callable
109
    {
110
        return $this->getOptions()->getDbSelectBuilder() ?? function() {
111 25
            return $this->getBlankDbSelect();
112 29
        };
113
    }
114
115
    /**
116
     * @return string
117
     */
118 81
    public function getBlankDbSelect(): string
119
    {
120 81
        return 'SELECT * FROM '.$this->getAdapter()->quoteIdentifier($this->getOptions()->getTableName()).' ';
121
    }
122
123
    /**
124
     * Return default db select.
125
     *
126
     * @return string
127
     */
128 29
    public function getDefaultDbSelect()
129
    {
130 29
        return $this->getDbSelectBuilder()();
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 32
    public function lockTree(): void
137
    {
138 32
        $options = $this->getOptions();
139 32
        $adapter = $this->getAdapter();
140
141 32
        $sql = 'SELECT '.$adapter->quoteIdentifier($options->getIdColumnName())
142 32
            .' FROM '.$adapter->quoteIdentifier($options->getTableName())
143 32
            .' FOR UPDATE';
144
145 32
        $adapter->executeSQL($sql);
146 32
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 32
    public function beginTransaction(): void
152
    {
153 32
        $this->getAdapter()
154 32
             ->beginTransaction();
155 32
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 17
    public function commitTransaction(): void
161
    {
162 17
        $this->getAdapter()
163 17
             ->commitTransaction();
164 17
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169 16
    public function rollbackTransaction(): void
170
    {
171 16
        $this->getAdapter()
172 16
             ->rollbackTransaction();
173 16
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 5
    public function update($nodeId, array $data): void
179
    {
180 5
        $options = $this->getOptions();
181 5
        $adapter = $this->getAdapter();
182 5
        $data = $this->cleanData($data);
183
184
        $setPart = array_map(function ($item) use ($adapter) {
185 5
            return $adapter->quoteIdentifier($item).' = :'.$item;
186 5
        }, array_keys($data));
187
188 5
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
189 5
            .' SET '.implode(', ', $setPart)
190 5
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeID';
191
192 5
        $data['__nodeID'] = $nodeId;
193
194 5
        $adapter->executeSQL($sql, $data);
195 5
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 14
    public function insert(NodeInfo $nodeInfo, array $data)
201
    {
202 14
        $options = $this->getOptions();
203
204 14
        $adapter = $this->getAdapter();
205
206 14
        $data[$options->getParentIdColumnName()] = $nodeInfo->getParentId();
207 14
        $data[$options->getLevelColumnName()] = $nodeInfo->getLevel();
208 14
        $data[$options->getLeftColumnName()] = $nodeInfo->getLeft();
209 14
        $data[$options->getRightColumnName()] = $nodeInfo->getRight();
210
211 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...
212 4
            $data[$options->getScopeColumnName()] = $nodeInfo->getScope();
213
        }
214
215
        $columns = array_map(function ($item) use ($adapter) {
216 14
            return $adapter->quoteIdentifier($item);
217 14
        }, array_keys($data));
218
219
        $values = array_map(function ($item) {
220 14
            return ':'.$item;
221 14
        }, array_keys($data));
222
223 14
        $sql = 'INSERT INTO '.$adapter->quoteIdentifier($options->getTableName())
224 14
            .' ('.implode(', ', $columns).')'
225 14
            .' VALUES('.implode(', ', $values).')';
226
227 14
        return $adapter->executeInsertSQL($sql, $data);
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 4
    public function delete($nodeId): void
234
    {
235 4
        $options = $this->getOptions();
236 4
        $adapter = $this->getAdapter();
237
238 4
        $sql = 'DELETE FROM '.$adapter->quoteIdentifier($options->getTableName())
239 4
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeID';
240
241
        $params = array(
242 4
            '__nodeID' => $nodeId,
243
        );
244
245 4
        $adapter->executeSQL($sql, $params);
246 4
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 15
    public function moveLeftIndexes($fromIndex, $shift, $scope = null): void
252
    {
253 15
        $options = $this->getOptions();
254
255 15
        if (0 == $shift) {
256
            return;
257
        }
258
259 15
        $adapter = $this->getAdapter();
260
261
        $params = array(
262 15
            ':shift' => $shift,
263 15
            ':fromIndex' => $fromIndex,
264
        );
265
266 15
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
267 15
            .' SET '
268 15
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' = '
269 15
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' + :shift'
270 15
            .' WHERE '
271 15
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' > :fromIndex';
272
273 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...
274 4
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
275 4
            $params['__scope'] = $scope;
276
        }
277
278 15
        $adapter->executeSQL($sql, $params);
279 15
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284 15
    public function moveRightIndexes($fromIndex, $shift, $scope = null): void
285
    {
286 15
        $options = $this->getOptions();
287
288 15
        if (0 == $shift) {
289
            return;
290
        }
291
292 15
        $adapter = $this->getAdapter();
293
294
        $params = array(
295 15
            ':shift' => $shift,
296 15
            ':fromIndex' => $fromIndex,
297
        );
298
299 15
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
300 15
            .' SET '
301 15
            .$adapter->quoteIdentifier($options->getRightColumnName()).' = '
302 15
            .$adapter->quoteIdentifier($options->getRightColumnName()).' + :shift'
303 15
            .' WHERE '
304 15
            .$adapter->quoteIdentifier($options->getRightColumnName()).' > :fromIndex';
305
306 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...
307 4
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
308 4
            $params['__scope'] = $scope;
309
        }
310
311 15
        $adapter->executeSQL($sql, $params);
312 15
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317 5
    public function updateParentId($nodeId, $newParentId): void
318
    {
319 5
        $options = $this->getOptions();
320
321 5
        $adapter = $this->getAdapter();
322
323 5
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
324 5
            .' SET '.$adapter->quoteIdentifier($options->getParentIdColumnName()).' = :__parentId'
325 5
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeId';
326
327
        $params = array(
328 5
            '__parentId' => $newParentId,
329 5
            '__nodeId' => $nodeId,
330
        );
331
332 5
        $adapter->executeSQL($sql, $params);
333 5
    }
334
335
    /**
336
     * {@inheritdoc}
337
     */
338 6
    public function updateLevels(int $leftIndexFrom, int $rightIndexTo, int $shift, $scope = null): void
339
    {
340 6
        $options = $this->getOptions();
341
342 6
        if (0 == $shift) {
343
            return;
344
        }
345
346 6
        $adapter = $this->getAdapter();
347
348
        $binds = array(
349 6
            ':shift' => $shift,
350 6
            ':leftFrom' => $leftIndexFrom,
351 6
            ':rightTo' => $rightIndexTo,
352
        );
353
354 6
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
355 6
            .' SET '
356 6
            .$adapter->quoteIdentifier($options->getLevelColumnName()).' = '
357 6
            .$adapter->quoteIdentifier($options->getLevelColumnName()).' + :shift'
358 6
            .' WHERE '
359 6
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' >= :leftFrom'
360 6
            .' AND '.$adapter->quoteIdentifier($options->getRightColumnName()).' <= :rightTo';
361
362 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...
363 1
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
364 1
            $binds['__scope'] = $scope;
365
        }
366
367 6
        $adapter->executeSQL($sql, $binds);
368 6
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373 7
    public function moveBranch(int $leftIndexFrom, int $rightIndexTo, int $shift, $scope = null): void
374
    {
375 7
        if (0 == $shift) {
376
            return;
377
        }
378
379 7
        $options = $this->getOptions();
380
381 7
        $adapter = $this->getAdapter();
382
383
        $binds = array(
384 7
            ':shift' => $shift,
385 7
            ':leftFrom' => $leftIndexFrom,
386 7
            ':rightTo' => $rightIndexTo,
387
        );
388
389 7
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
390 7
            .' SET '
391 7
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' = '
392 7
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' + :shift, '
393 7
            .$adapter->quoteIdentifier($options->getRightColumnName()).' = '
394 7
            .$adapter->quoteIdentifier($options->getRightColumnName()).' + :shift'
395 7
            .' WHERE '
396 7
            .$adapter->quoteIdentifier($options->getLeftColumnName()).' >= :leftFrom'
397 7
            .' AND '.$adapter->quoteIdentifier($options->getRightColumnName()).' <= :rightTo';
398
399 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...
400 2
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName()).' = :__scope';
401 2
            $binds['__scope'] = $scope;
402
        }
403
404 7
        $adapter->executeSQL($sql, $binds);
405 7
    }
406
407
    /**
408
     * {@inheritdoc}
409
     */
410 12
    public function getRoots($scope = null): array
411
    {
412 12
        $options = $this->getOptions();
413
414 12
        $adapter = $this->getAdapter();
415
416 12
        $params = array();
417
418 12
        $sql = $this->getBlankDbSelect();
419 12
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getParentIdColumnName(true)).' IS NULL';
420
421 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...
422 3
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName(true)).' = :__scope';
423 3
            $params['__scope'] = $scope;
424
        }
425
426 12
        $sql .= ' ORDER BY '.$adapter->quoteIdentifier($options->getIdColumnName(true)).' ASC';
427
428 12
        return $adapter->executeSelectSQL($sql, $params);
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     */
434 9
    public function getRoot($scope = null): array
435
    {
436 9
        $roots = $this->getRoots($scope);
437
438 9
        return (0 < count($roots)) ? $roots[0] : array();
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444 5
    public function getNode($nodeId): ?array
445
    {
446 5
        $options = $this->getOptions();
447 5
        $nodeId = (int) $nodeId;
448 5
        $adapter = $this->getAdapter();
449
450
        $params = array(
451 5
            '__nodeID' => $nodeId,
452
        );
453
454 5
        $sql = $this->getDefaultDbSelect();
455 5
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName(true)).' = :__nodeID';
456
457 5
        $result = $adapter->executeSelectSQL($sql, $params);
458
459 5
        return (0 < count($result)) ? $result[0] : null;
460
    }
461
462
    /**
463
     * {@inheritdoc}
464
     */
465 64
    public function getNodeInfo($nodeId): ?NodeInfo
466
    {
467 64
        $options = $this->getOptions();
468 64
        $adapter = $this->getAdapter();
469
470
        $params = array(
471 64
            '__nodeID' => $nodeId,
472
        );
473
474 64
        $sql = $this->getBlankDbSelect();
475 64
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName(true)).' = :__nodeID';
476
477 64
        $array = $adapter->executeSelectSQL($sql, $params);
478
479 64
        $result = ($array) ? $this->_buildNodeInfoObject($array[0]) : null;
480
481 64
        return $result;
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487 7
    public function getChildrenNodeInfo($parentNodeId): array
488
    {
489 7
        $adapter = $this->getAdapter();
490 7
        $options = $this->getOptions();
491
492
        $params = array(
493 7
            '__parentID' => $parentNodeId,
494
        );
495
496
        $sql = 'SELECT *'
497 7
            .' FROM '.$adapter->quoteIdentifier($this->getOptions()->getTableName())
498 7
            .' WHERE '.$adapter->quoteIdentifier($options->getParentIdColumnName(true)).' = :__parentID'
499 7
            .' ORDER BY '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' ASC';
500
501 7
        $data = $adapter->executeSelectSQL($sql, $params);
502
503 7
        $result = array();
504 7
        foreach ($data as $nodeData) {
505 6
            $result[] = $this->_buildNodeInfoObject($nodeData);
506
        }
507
508 7
        return $result;
509
    }
510
511
    /**
512
     * {@inheritdoc}
513
     */
514 3
    public function updateNodeMetadata(NodeInfo $nodeInfo): void
515
    {
516 3
        $adapter = $this->getAdapter();
517 3
        $options = $this->getOptions();
518
519
        $data = array(
520 3
            $options->getRightColumnName() => $nodeInfo->getRight(),
521 3
            $options->getLeftColumnName() => $nodeInfo->getLeft(),
522 3
            $options->getLevelColumnName() => $nodeInfo->getLevel(),
523
        );
524
525
        $setPart = array_map(function ($item) use ($adapter) {
526 3
            return $adapter->quoteIdentifier($item).' = :'.$item;
527 3
        }, array_keys($data));
528
529 3
        $sql = 'UPDATE '.$adapter->quoteIdentifier($options->getTableName())
530 3
            .' SET '.implode(', ', $setPart)
531 3
            .' WHERE '.$adapter->quoteIdentifier($options->getIdColumnName()).' = :__nodeID';
532
533 3
        $data['__nodeID'] = $nodeInfo->getId();
534
535 3
        $adapter->executeSQL($sql, $data);
536 3
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541 11
    public function getAncestors($nodeId, int $startLevel = 0, int $excludeLastNLevels = 0): array
542
    {
543 11
        $options = $this->getOptions();
544
545
        // node does not exist
546 11
        $nodeInfo = $this->getNodeInfo($nodeId);
547 11
        if (!$nodeInfo) {
548 2
            return array();
549
        }
550
551 9
        $adapter = $this->getAdapter();
552
553
        $params = array(
554 9
            '__leftIndex' => $nodeInfo->getLeft(),
555 9
            '__rightIndex' => $nodeInfo->getRight(),
556
        );
557
558 9
        $sql = $this->getDefaultDbSelect();
559
560 9
        $sql .= ' WHERE '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' <= :__leftIndex'
561 9
            .' AND '.$adapter->quoteIdentifier($options->getRightColumnName(true)).' >= :__rightIndex';
562
563 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...
564 3
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getScopeColumnName(true)).' = :__scope';
565 3
            $params['__scope'] = $nodeInfo->getScope();
566
        }
567
568 9
        if (0 < $startLevel) {
569 3
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getLevelColumnName(true)).' >= :__startLevel';
570 3
            $params['__startLevel'] = $startLevel;
571
        }
572
573 9
        if (0 < $excludeLastNLevels) {
574 4
            $sql .= ' AND '.$adapter->quoteIdentifier($options->getLevelColumnName(true)).' <= :__excludeLastNLevels';
575 4
            $params['__excludeLastNLevels'] = $nodeInfo->getLevel() - $excludeLastNLevels;
576
        }
577
578 9
        $sql .= ' ORDER BY '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' ASC';
579
580 9
        return $adapter->executeSelectSQL($sql, $params);
581
    }
582
583
    /**
584
     * {@inheritdoc}
585
     */
586 18
    public function getDescendants($nodeId, int $startLevel = 0, ?int $levels = null, $excludeBranch = null): array
587
    {
588 18
        $options = $this->getOptions();
589
590 18
        if (!$nodeInfo = $this->getNodeInfo($nodeId)) {
591 3
            return array();
592
        }
593
594 15
        $adapter = $this->getAdapter();
595 15
        $sql = $this->getDefaultDbSelect();
596
597 15
        $params = array();
598 15
        $wherePart = array();
599
600 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...
601 5
            $wherePart[] = $adapter->quoteIdentifier($options->getScopeColumnName(true)).' = :__scope';
602 5
            $params['__scope'] = $nodeInfo->getScope();
603
        }
604
605 15
        if (0 != $startLevel) {
606 7
            $wherePart[] = $adapter->quoteIdentifier($options->getLevelColumnName(true)).' >= :__level';
607 7
            $params['__level'] = $nodeInfo->getLevel() + $startLevel;
608
        }
609
610 15
        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...
611 6
            $wherePart[] = $adapter->quoteIdentifier($options->getLevelColumnName(true)).' < :__endLevel';
612 6
            $params['__endLevel'] = $nodeInfo->getLevel() + $startLevel + abs($levels);
613
        }
614
615 15
        if (null != $excludeBranch && null != ($excludeNodeInfo = $this->getNodeInfo($excludeBranch))) {
616 3
            $wherePart[] = '( '
617 3
                .$adapter->quoteIdentifier($options->getLeftColumnName(true)).' BETWEEN :__l1 AND :__p1'
618 3
                .' OR '
619 3
                .$adapter->quoteIdentifier($options->getLeftColumnName(true)).' BETWEEN :__l2 AND :__p2'
620 3
                .') AND ('
621 3
                .$adapter->quoteIdentifier($options->getRightColumnName(true)).' BETWEEN :__l3 AND :__p3'
622 3
                .' OR '
623 3
                .$adapter->quoteIdentifier($options->getRightColumnName(true)).' BETWEEN :__l4 AND :__p4'
624 3
                .')';
625
626 3
            $params['__l1'] = $nodeInfo->getLeft();
627 3
            $params['__p1'] = $excludeNodeInfo->getLeft() - 1;
628 3
            $params['__l2'] = $excludeNodeInfo->getRight() + 1;
629 3
            $params['__p2'] = $nodeInfo->getRight();
630 3
            $params['__l3'] = $excludeNodeInfo->getRight() + 1;
631 3
            $params['__p3'] = $nodeInfo->getRight();
632 3
            $params['__l4'] = $nodeInfo->getLeft();
633 3
            $params['__p4'] = $excludeNodeInfo->getLeft() - 1;
634
        } else {
635 13
            $wherePart[] = $adapter->quoteIdentifier($options->getLeftColumnName(true)).' >= :__left'
636 13
                .' AND '.$adapter->quoteIdentifier($options->getRightColumnName(true)).' <= :__right';
637
638 13
            $params['__left'] = $nodeInfo->getLeft();
639 13
            $params['__right'] = $nodeInfo->getRight();
640
        }
641
642 15
        $sql .= ' WHERE '.implode(' AND ', $wherePart);
643 15
        $sql .= ' ORDER BY '.$adapter->quoteIdentifier($options->getLeftColumnName(true)).' ASC';
644
645 15
        $result = $adapter->executeSelectSQL($sql, $params);
646
647 15
        return (0 < count($result)) ? $result : array();
648
    }
649
}
650