Passed
Pull Request — master (#6239)
by
unknown
08:01
created

NestedTreeRepositoryTrait::recoverNode()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 21
rs 9.7666
1
<?php
2
3
namespace Chamilo\CoreBundle\Traits\Repository\ORM;
4
5
use Gedmo\Exception\RuntimeException;
6
use Gedmo\Tool\Wrapper\EntityWrapper;
7
use Doctrine\ORM\Query;
8
use Gedmo\Tree\Strategy;
9
use Gedmo\Tree\Strategy\ORM\Nested;
10
use Gedmo\Exception\InvalidArgumentException;
11
use Gedmo\Exception\UnexpectedValueException;
12
use Doctrine\ORM\Proxy\Proxy;
13
14
/**
15
 * The NestedTreeRepository trait has some useful functions
16
 * to interact with NestedSet tree. Repository uses
17
 * the strategy used by listener.
18
 *
19
 * @author Gediminas Morkevicius <[email protected]>
20
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
21
 */
22
trait NestedTreeRepositoryTrait
23
{
24
    use TreeRepositoryTrait;
25
26
    /**
27
     * {@inheritDoc}
28
     */
29
    public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
30
    {
31
        $meta = $this->getClassMetadata();
32
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
33
        $qb = $this->getQueryBuilder();
34
        $qb
35
            ->select('node')
36
            ->from($config['useObjectClass'], 'node')
37
            ->where($qb->expr()->isNull('node.'.$config['parent']))
38
        ;
39
40
        if ($sortByField !== null) {
41
            $qb->orderBy('node.'.$sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc');
42
        } else {
43
            $qb->orderBy('node.'.$config['left'], 'ASC');
44
        }
45
46
        return $qb;
47
    }
48
49
    /**
50
     * {@inheritDoc}
51
     */
52
    public function getRootNodesQuery($sortByField = null, $direction = 'asc')
53
    {
54
        return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
55
    }
56
57
    /**
58
     * {@inheritDoc}
59
     */
60
    public function getRootNodes($sortByField = null, $direction = 'asc')
61
    {
62
        return $this->getRootNodesQuery($sortByField, $direction)->getResult();
63
    }
64
65
    /**
66
     * Persists node in given position strategy
67
     */
68
    protected function persistAs($node, $child = null, $position = Nested::FIRST_CHILD)
69
    {
70
        $em = $this->getEntityManager();
71
        $wrapped = new EntityWrapper($node, $em);
72
        $meta = $this->getClassMetadata();
73
        $config = $this->listener->getConfiguration($em, $meta->name);
74
75
        $siblingInPosition = null;
76
        if ($child !== null) {
77
            switch ($position) {
78
                case Nested::PREV_SIBLING:
79
                case Nested::NEXT_SIBLING:
80
                    $sibling = new EntityWrapper($child, $em);
81
                    $newParent = $sibling->getPropertyValue($config['parent']);
82
                    if (null === $newParent && isset($config['root'])) {
83
                        throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible");
84
                    }
85
                    $siblingInPosition = $child;
86
                    $child = $newParent;
87
                    break;
88
            }
89
            $wrapped->setPropertyValue($config['parent'], $child);
90
        }
91
92
        $wrapped->setPropertyValue($config['left'], 0); // simulate changeset
93
        $oid = spl_object_hash($node);
94
        $this->listener->getStrategy($em, $meta->name)->setNodePosition($oid, $position, $siblingInPosition);
95
        $em->persist($node);
96
97
        return $this;
98
    }
99
100
    /**
101
     * Persists given $node as first child of tree
102
     *
103
     * @param $node
104
     * @return self
105
     */
106
    public function persistAsFirstChild($node)
107
    {
108
        return $this->persistAs($node, null, Nested::FIRST_CHILD);
109
    }
110
111
    /**
112
     * Persists given $node as first child of $parent node
113
     *
114
     * @param $node
115
     * @param $parent
116
     * @return self
117
     */
118
    public function persistAsFirstChildOf($node, $parent)
119
    {
120
        return $this->persistAs($node, $parent, Nested::FIRST_CHILD);
121
    }
122
123
    /**
124
     * Persists given $node as last child of tree
125
     *
126
     * @param $node
127
     * @return self
128
     */
129
    public function persistAsLastChild($node)
130
    {
131
        return $this->persistAs($node, null, Nested::LAST_CHILD);
132
    }
133
134
    /**
135
     * Persists given $node as last child of $parent node
136
     *
137
     * @param $node
138
     * @param $parent
139
     * @return self
140
     */
141
    public function persistAsLastChildOf($node, $parent)
142
    {
143
        return $this->persistAs($node, $parent, Nested::LAST_CHILD);
144
    }
145
146
    /**
147
     * Persists given $node next to $sibling node
148
     *
149
     * @param $node
150
     * @param $sibling
151
     * @return self
152
     */
153
    public function persistAsNextSiblingOf($node, $sibling)
154
    {
155
        return $this->persistAs($node, $sibling, Nested::NEXT_SIBLING);
156
    }
157
158
    /**
159
     * Persists given $node previous to $sibling node
160
     *
161
     * @param $node
162
     * @param $sibling
163
     * @return self
164
     */
165
    public function persistAsPrevSiblingOf($node, $sibling)
166
    {
167
        return $this->persistAs($node, $sibling, Nested::PREV_SIBLING);
168
    }
169
170
    /**
171
     * Persists given $node same as first child of it's parent
172
     *
173
     * @param $node
174
     * @return self
175
     */
176
    public function persistAsNextSibling($node)
177
    {
178
        return $this->persistAs($node, null, Nested::NEXT_SIBLING);
179
    }
180
181
    /**
182
     * Persists given $node same as last child of it's parent
183
     *
184
     * @param $node
185
     * @return self
186
     */
187
    public function persistAsPrevSibling($node)
188
    {
189
        return $this->persistAs($node, null, Nested::PREV_SIBLING);
190
    }
191
192
    /**
193
     * Get the Tree path query builder by given $node
194
     *
195
     * @param object $node
196
     *
197
     * @throws InvalidArgumentException - if input is not valid
198
     *
199
     * @return \Doctrine\ORM\QueryBuilder
200
     */
201
    public function getPathQueryBuilder($node)
202
    {
203
        $meta = $this->getClassMetadata();
204
        if (!$node instanceof $meta->name) {
205
            throw new InvalidArgumentException("Node is not related to this repository");
206
        }
207
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
208
        $wrapped = new EntityWrapper($node, $this->getEntityManager());
209
        if (!$wrapped->hasValidIdentifier()) {
210
            throw new InvalidArgumentException("Node is not managed by UnitOfWork");
211
        }
212
        $left = $wrapped->getPropertyValue($config['left']);
213
        $right = $wrapped->getPropertyValue($config['right']);
214
        $qb = $this->getQueryBuilder();
215
        $qb->select('node')
216
            ->from($config['useObjectClass'], 'node')
217
            ->where($qb->expr()->lte('node.'.$config['left'], $left))
218
            ->andWhere($qb->expr()->gte('node.'.$config['right'], $right))
219
            ->orderBy('node.'.$config['left'], 'ASC')
220
        ;
221
        if (isset($config['root'])) {
222
            $root = $wrapped->getPropertyValue($config['root']);
223
            $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
224
            $qb->setParameter('rid', $root);
225
        }
226
227
        return $qb;
228
    }
229
230
    /**
231
     * Get the Tree path query by given $node
232
     *
233
     * @param object $node
234
     *
235
     * @return \Doctrine\ORM\Query
236
     */
237
    public function getPathQuery($node)
238
    {
239
        return $this->getPathQueryBuilder($node)->getQuery();
240
    }
241
242
    /**
243
     * Get the Tree path of Nodes by given $node
244
     *
245
     * @param object $node
246
     *
247
     * @return array - list of Nodes in path
248
     */
249
    public function getPath($node)
250
    {
251
        return $this->getPathQuery($node)->getResult();
252
    }
253
254
    /**
255
     * @see getChildrenQueryBuilder
256
     */
257
    public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
258
    {
259
        $meta = $this->getClassMetadata();
260
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
261
262
        $qb = $this->getQueryBuilder();
263
        $qb->select('node')
264
            ->from($config['useObjectClass'], 'node')
265
        ;
266
        if ($node !== null) {
267
            if ($node instanceof $meta->name) {
268
                $wrapped = new EntityWrapper($node, $this->getEntityManager());
269
                if (!$wrapped->hasValidIdentifier()) {
270
                    throw new InvalidArgumentException("Node is not managed by UnitOfWork");
271
                }
272
                if ($direct) {
273
                    $qb->where($qb->expr()->eq('node.'.$config['parent'], ':pid'));
274
                    $qb->setParameter('pid', $wrapped->getIdentifier());
275
                } else {
276
                    $left = $wrapped->getPropertyValue($config['left']);
277
                    $right = $wrapped->getPropertyValue($config['right']);
278
                    if ($left && $right) {
279
                        $qb->where($qb->expr()->lt('node.'.$config['right'], $right));
280
                        $qb->andWhere($qb->expr()->gt('node.'.$config['left'], $left));
281
                    }
282
                }
283
                if (isset($config['root'])) {
284
                    $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
285
                    $qb->setParameter('rid', $wrapped->getPropertyValue($config['root']));
286
                }
287
                if ($includeNode) {
288
                    $idField = $meta->getSingleIdentifierFieldName();
289
                    $qb->where('('.$qb->getDqlPart('where').') OR node.'.$idField.' = :rootNode');
290
                    $qb->setParameter('rootNode', $node);
291
                }
292
            } else {
293
                throw new \InvalidArgumentException("Node is not related to this repository");
294
            }
295
        } else {
296
            if ($direct) {
297
                $qb->where($qb->expr()->isNull('node.'.$config['parent']));
298
            }
299
        }
300
        if (!$sortByField) {
301
            $qb->orderBy('node.'.$config['left'], 'ASC');
302
        } elseif (is_array($sortByField)) {
303
            $fields = '';
304
            foreach ($sortByField as $field) {
305
                $fields .= 'node.'.$field.',';
306
            }
307
            $fields = rtrim($fields, ',');
308
            $qb->orderBy($fields, $direction);
309
        } else {
310
            if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
311
                $qb->orderBy('node.'.$sortByField, $direction);
312
            } else {
313
                throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
314
            }
315
        }
316
317
        return $qb;
318
    }
319
320
    /**
321
     * @see getChildrenQuery
322
     */
323
    public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
324
    {
325
        return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
326
    }
327
328
    /**
329
     * @see getChildren
330
     */
331
    public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
332
    {
333
        $q = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode);
334
335
        return $q->getResult();
336
    }
337
338
    /**
339
     * {@inheritDoc}
340
     */
341
    public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
342
    {
343
        return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode);
344
    }
345
346
    /**
347
     * {@inheritDoc}
348
     */
349
    public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
350
    {
351
        return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode);
352
    }
353
354
    /**
355
     * {@inheritDoc}
356
     */
357
    public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
358
    {
359
        return $this->children($node, $direct, $sortByField, $direction, $includeNode);
360
    }
361
362
    /**
363
     * Get tree leafs query builder
364
     *
365
     * @param object $root        - root node in case of root tree is required
366
     * @param string $sortByField - field name to sort by
367
     * @param string $direction   - sort direction : "ASC" or "DESC"
368
     *
369
     * @throws InvalidArgumentException - if input is not valid
370
     *
371
     * @return \Doctrine\ORM\QueryBuilder
372
     */
373
    public function getLeafsQueryBuilder($root = null, $sortByField = null, $direction = 'ASC')
374
    {
375
        $meta = $this->getClassMetadata();
376
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
377
378
        if (isset($config['root']) && null === $root) {
379
            throw new InvalidArgumentException("If tree has root, getLeafs method requires any node of this tree");
380
        }
381
382
        $qb = $this->getQueryBuilder();
383
        $qb->select('node')
384
            ->from($config['useObjectClass'], 'node')
385
            ->where($qb->expr()->eq('node.'.$config['right'], '1 + node.'.$config['left']))
386
        ;
387
        if (isset($config['root'])) {
388
            if ($root instanceof $meta->name) {
389
                $wrapped = new EntityWrapper($root, $this->getEntityManager());
390
                $rootId = $wrapped->getPropertyValue($config['root']);
391
                if (!$rootId) {
392
                    throw new InvalidArgumentException("Root node must be managed");
393
                }
394
                $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
395
                $qb->setParameter('rid', $rootId);
396
            } else {
397
                throw new InvalidArgumentException("Node is not related to this repository");
398
            }
399
        }
400
        if (!$sortByField) {
401
            if (isset($config['root'])) {
402
                $qb->addOrderBy('node.'.$config['root'], 'ASC');
403
            }
404
            $qb->addOrderBy('node.'.$config['left'], 'ASC');
405
        } else {
406
            if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
407
                $qb->orderBy('node.'.$sortByField, $direction);
408
            } else {
409
                throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
410
            }
411
        }
412
413
        return $qb;
414
    }
415
416
    /**
417
     * Get tree leafs query
418
     *
419
     * @param object $root        - root node in case of root tree is required
420
     * @param string $sortByField - field name to sort by
421
     * @param string $direction   - sort direction : "ASC" or "DESC"
422
     *
423
     * @return \Doctrine\ORM\Query
424
     */
425
    public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC')
426
    {
427
        return $this->getLeafsQueryBuilder($root, $sortByField, $direction)->getQuery();
428
    }
429
430
    /**
431
     * Get list of leaf nodes of the tree
432
     *
433
     * @param object $root        - root node in case of root tree is required
434
     * @param string $sortByField - field name to sort by
435
     * @param string $direction   - sort direction : "ASC" or "DESC"
436
     *
437
     * @return array
438
     */
439
    public function getLeafs($root = null, $sortByField = null, $direction = 'ASC')
440
    {
441
        return $this->getLeafsQuery($root, $sortByField, $direction)->getResult();
442
    }
443
444
    /**
445
     * Get the query builder for next siblings of the given $node
446
     *
447
     * @param object $node
448
     * @param bool   $includeSelf - include the node itself
449
     *
450
     * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
451
     *
452
     * @return \Doctrine\ORM\QueryBuilder
453
     */
454
    public function getNextSiblingsQueryBuilder($node, $includeSelf = false)
455
    {
456
        $meta = $this->getClassMetadata();
457
        if (!$node instanceof $meta->name) {
458
            throw new InvalidArgumentException("Node is not related to this repository");
459
        }
460
        $wrapped = new EntityWrapper($node, $this->getEntityManager());
461
        if (!$wrapped->hasValidIdentifier()) {
462
            throw new InvalidArgumentException("Node is not managed by UnitOfWork");
463
        }
464
465
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
466
        $parent = $wrapped->getPropertyValue($config['parent']);
467
        if (isset($config['root']) && !$parent) {
468
            throw new InvalidArgumentException("Cannot get siblings from tree root node");
469
        }
470
471
        $left = $wrapped->getPropertyValue($config['left']);
472
473
        $qb = $this->getQueryBuilder();
474
        $qb->select('node')
475
            ->from($config['useObjectClass'], 'node')
476
            ->where(
477
                $includeSelf ?
478
                $qb->expr()->gte('node.'.$config['left'], $left) :
479
                $qb->expr()->gt('node.'.$config['left'], $left)
480
            )
481
            ->orderBy("node.{$config['left']}", 'ASC')
482
        ;
483
        if ($parent) {
484
            $wrappedParent = new EntityWrapper($parent, $this->getEntityManager());
485
            $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid'));
486
            $qb->setParameter('pid', $wrappedParent->getIdentifier());
487
        } else {
488
            $qb->andWhere($qb->expr()->isNull('node.'.$config['parent']));
489
        }
490
491
        return $qb;
492
    }
493
494
    /**
495
     * Get the query for next siblings of the given $node
496
     *
497
     * @param object $node
498
     * @param bool   $includeSelf - include the node itself
499
     *
500
     * @return \Doctrine\ORM\Query
501
     */
502
    public function getNextSiblingsQuery($node, $includeSelf = false)
503
    {
504
        return $this->getNextSiblingsQueryBuilder($node, $includeSelf)->getQuery();
505
    }
506
507
    /**
508
     * Find the next siblings of the given $node
509
     *
510
     * @param object $node
511
     * @param bool   $includeSelf - include the node itself
512
     *
513
     * @return array
514
     */
515
    public function getNextSiblings($node, $includeSelf = false)
516
    {
517
        return $this->getNextSiblingsQuery($node, $includeSelf)->getResult();
518
    }
519
520
    /**
521
     * Get query builder for previous siblings of the given $node
522
     *
523
     * @param object $node
524
     * @param bool   $includeSelf - include the node itself
525
     *
526
     * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
527
     *
528
     * @return \Doctrine\ORM\QueryBuilder
529
     */
530
    public function getPrevSiblingsQueryBuilder($node, $includeSelf = false)
531
    {
532
        $meta = $this->getClassMetadata();
533
        if (!$node instanceof $meta->name) {
534
            throw new InvalidArgumentException("Node is not related to this repository");
535
        }
536
        $wrapped = new EntityWrapper($node, $this->getEntityManager());
537
        if (!$wrapped->hasValidIdentifier()) {
538
            throw new InvalidArgumentException("Node is not managed by UnitOfWork");
539
        }
540
541
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
542
        $parent = $wrapped->getPropertyValue($config['parent']);
543
        if (isset($config['root']) && !$parent) {
544
            throw new InvalidArgumentException("Cannot get siblings from tree root node");
545
        }
546
547
        $left = $wrapped->getPropertyValue($config['left']);
548
549
        $qb = $this->getQueryBuilder();
550
        $qb->select('node')
551
            ->from($config['useObjectClass'], 'node')
552
            ->where(
553
                $includeSelf ?
554
                $qb->expr()->lte('node.'.$config['left'], $left) :
555
                $qb->expr()->lt('node.'.$config['left'], $left)
556
            )
557
            ->orderBy("node.{$config['left']}", 'ASC')
558
        ;
559
        if ($parent) {
560
            $wrappedParent = new EntityWrapper($parent, $this->getEntityManager());
561
            $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid'));
562
            $qb->setParameter('pid', $wrappedParent->getIdentifier());
563
        } else {
564
            $qb->andWhere($qb->expr()->isNull('node.'.$config['parent']));
565
        }
566
567
        return $qb;
568
    }
569
570
    /**
571
     * Get query for previous siblings of the given $node
572
     *
573
     * @param object $node
574
     * @param bool   $includeSelf - include the node itself
575
     *
576
     * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid
577
     *
578
     * @return \Doctrine\ORM\Query
579
     */
580
    public function getPrevSiblingsQuery($node, $includeSelf = false)
581
    {
582
        return $this->getPrevSiblingsQueryBuilder($node, $includeSelf)->getQuery();
583
    }
584
585
    /**
586
     * Find the previous siblings of the given $node
587
     *
588
     * @param object $node
589
     * @param bool   $includeSelf - include the node itself
590
     *
591
     * @return array
592
     */
593
    public function getPrevSiblings($node, $includeSelf = false)
594
    {
595
        return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult();
596
    }
597
598
    /**
599
     * Move the node down in the same level
600
     *
601
     * @param object   $node
602
     * @param int|bool $number integer - number of positions to shift
603
     *                         boolean - if "true" - shift till last position
604
     *
605
     * @throws \RuntimeException - if something fails in transaction
606
     *
607
     * @return boolean - true if shifted
608
     */
609
    public function moveDown($node, $number = 1)
610
    {
611
        $result = false;
612
        $meta = $this->getClassMetadata();
613
        if ($node instanceof $meta->name) {
614
            $nextSiblings = $this->getNextSiblings($node);
615
            if ($numSiblings = count($nextSiblings)) {
616
                $result = true;
617
                if ($number === true) {
618
                    $number = $numSiblings;
619
                } elseif ($number > $numSiblings) {
620
                    $number = $numSiblings;
621
                }
622
                $this->listener
623
                    ->getStrategy($this->getEntityManager(), $meta->name)
624
                    ->updateNode($this->getEntityManager(), $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING);
625
            }
626
        } else {
627
            throw new InvalidArgumentException("Node is not related to this repository");
628
        }
629
630
        return $result;
631
    }
632
633
    /**
634
     * Move the node up in the same level
635
     *
636
     * @param object   $node
637
     * @param int|bool $number integer - number of positions to shift
638
     *                         boolean - true shift till first position
639
     *
640
     * @throws \RuntimeException - if something fails in transaction
641
     *
642
     * @return boolean - true if shifted
643
     */
644
    public function moveUp($node, $number = 1)
645
    {
646
        $result = false;
647
        $meta = $this->getClassMetadata();
648
        if ($node instanceof $meta->name) {
649
            $prevSiblings = array_reverse($this->getPrevSiblings($node));
650
            if ($numSiblings = count($prevSiblings)) {
651
                $result = true;
652
                if ($number === true) {
653
                    $number = $numSiblings;
654
                } elseif ($number > $numSiblings) {
655
                    $number = $numSiblings;
656
                }
657
                $this->listener
658
                    ->getStrategy($this->getEntityManager(), $meta->name)
659
                    ->updateNode($this->getEntityManager(), $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING);
660
            }
661
        } else {
662
            throw new InvalidArgumentException("Node is not related to this repository");
663
        }
664
665
        return $result;
666
    }
667
668
    /**
669
     * UNSAFE: be sure to backup before running this method when necessary
670
     *
671
     * Removes given $node from the tree and reparents its descendants
672
     */
673
    public function removeFromTree(object $node): void
674
    {
675
        $meta = $this->getClassMetadata();
676
        $em = $this->getEntityManager();
677
678
        if ($node instanceof $meta->name) {
679
            $wrapped = new EntityWrapper($node, $em);
680
            $config = $this->listener->getConfiguration($em, $meta->name);
681
            $right = $wrapped->getPropertyValue($config['right']);
682
            $left = $wrapped->getPropertyValue($config['left']);
683
            $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
684
685
            if (!is_numeric($left) || !is_numeric($right)) {
686
                $this->removeSingle($wrapped);
687
                return;
688
            }
689
690
            if ($right == $left + 1) {
691
                $this->removeSingle($wrapped);
692
                $this->listener
693
                    ->getStrategy($em, $meta->name)
694
                    ->shiftRL($em, $config['useObjectClass'], $right, -2, $rootId);
695
696
                return; // node was a leaf
697
            }
698
            // process updates in transaction
699
            $em->getConnection()->beginTransaction();
700
            try {
701
                $parent = $wrapped->getPropertyValue($config['parent']);
702
                $parentId = null;
703
                if ($parent) {
704
                    $wrappedParent = new EntityWrapper($parent, $em);
705
                    $parentId = $wrappedParent->getIdentifier();
706
                }
707
                $pk = $meta->getSingleIdentifierFieldName();
708
                $nodeId = $wrapped->getIdentifier();
709
                $shift = -1;
710
711
                // in case if root node is removed, children become roots
712
                if (isset($config['root']) && !$parent) {
713
                    $qb = $this->getQueryBuilder();
714
                    $qb->select('node.'.$pk, 'node.'.$config['left'], 'node.'.$config['right'])
715
                        ->from($config['useObjectClass'], 'node');
716
717
                    $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid'));
718
                    $qb->setParameter('pid', $nodeId);
719
                    $nodes = $qb->getQuery()->getArrayResult();
720
721
                    foreach ($nodes as $newRoot) {
722
                        $left = $newRoot[$config['left']];
723
                        $right = $newRoot[$config['right']];
724
                        $rootId = $newRoot[$pk];
725
                        $shift = -($left - 1);
726
727
                        $qb = $this->getQueryBuilder();
728
                        $qb->update($config['useObjectClass'], 'node');
729
                        $qb->set('node.'.$config['root'], ':rid');
730
                        $qb->setParameter('rid', $rootId);
731
                        $qb->where($qb->expr()->eq('node.'.$config['root'], ':rpid'));
732
                        $qb->setParameter('rpid', $nodeId);
733
                        $qb->andWhere($qb->expr()->gte('node.'.$config['left'], $left));
734
                        $qb->andWhere($qb->expr()->lte('node.'.$config['right'], $right));
735
                        $qb->getQuery()->getSingleScalarResult();
736
737
                        $qb = $this->getQueryBuilder();
738
                        $qb->update($config['useObjectClass'], 'node');
739
                        $qb->set('node.'.$config['parent'], ':pid');
740
                        $qb->setParameter('pid', $parentId);
741
                        $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid'));
742
                        $qb->setParameter('rpid', $nodeId);
743
                        $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
744
                        $qb->setParameter('rid', $rootId);
745
                        $qb->getQuery()->getSingleScalarResult();
746
747
                        $this->listener
748
                            ->getStrategy($em, $meta->name)
749
                            ->shiftRangeRL($em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
750
                        $this->listener
751
                            ->getStrategy($em, $meta->name)
752
                            ->shiftRL($em, $config['useObjectClass'], $right, -2, $rootId);
753
                    }
754
                } else {
755
                    $qb = $this->getQueryBuilder();
756
                    $qb->update($config['useObjectClass'], 'node');
757
                    $qb->set('node.'.$config['parent'], ':pid');
758
                    $qb->setParameter('pid', $parentId);
759
                    $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid'));
760
                    $qb->setParameter('rpid', $nodeId);
761
                    if (isset($config['root'])) {
762
                        $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
763
                        $qb->setParameter('rid', $rootId);
764
                    }
765
                    $qb->getQuery()->getSingleScalarResult();
766
767
                    $this->listener
768
                        ->getStrategy($em, $meta->name)
769
                        ->shiftRangeRL($em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1);
770
771
                    $this->listener
772
                        ->getStrategy($em, $meta->name)
773
                        ->shiftRL($em, $config['useObjectClass'], $right, -2, $rootId);
774
                }
775
                $this->removeSingle($wrapped);
776
                $em->getConnection()->commit();
777
            } catch (\Exception $e) {
778
                $em->close();
779
                $em->getConnection()->rollback();
780
                throw new RuntimeException('Transaction failed', null, $e);
781
            }
782
        } else {
783
            throw new InvalidArgumentException("Node is not related to this repository");
784
        }
785
    }
786
787
    /**
788
     * Reorders $node's sibling nodes and child nodes,
789
     * according to the $sortByField and $direction specified
790
     *
791
     * @param object|null $node        - node from which to start reordering the tree; null will reorder everything
792
     * @param string      $sortByField - field name to sort by
793
     * @param string      $direction   - sort direction : "ASC" or "DESC"
794
     * @param boolean     $verify      - true to verify tree first
795
     *
796
     * @return bool|null
797
     */
798
    public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true)
799
    {
800
        $meta = $this->getClassMetadata();
801
        if ($node instanceof $meta->name || $node === null) {
802
            $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
803
            if ($verify && is_array($this->verify())) {
804
                return false;
805
            }
806
807
            $nodes = $this->children($node, true, $sortByField, $direction);
808
            foreach ($nodes as $node) {
0 ignored issues
show
introduced by
$node is overwriting one of the parameters of this function.
Loading history...
809
                $wrapped = new EntityWrapper($node, $this->getEntityManager());
810
                $right = $wrapped->getPropertyValue($config['right']);
811
                $left = $wrapped->getPropertyValue($config['left']);
812
                $this->moveDown($node, true);
813
                if ($left != ($right - 1)) {
814
                    $this->reorder($node, $sortByField, $direction, false);
815
                }
816
            }
817
        } else {
818
            throw new InvalidArgumentException("Node is not related to this repository");
819
        }
820
    }
821
822
    /**
823
     * Reorders all nodes in the tree according to the $sortByField and $direction specified.
824
     *
825
     * @param string  $sortByField - field name to sort by
826
     * @param string  $direction   - sort direction : "ASC" or "DESC"
827
     * @param boolean $verify      - true to verify tree first
828
     */
829
    public function reorderAll($sortByField = null, $direction = 'ASC', $verify = true)
830
    {
831
        $this->reorder(null, $sortByField, $direction, $verify);
832
    }
833
834
    /**
835
     * Verifies that current tree is valid.
836
     * If any error is detected it will return an array
837
     * with a list of errors found on tree
838
     *
839
     * @return array|bool - true on success,error list on failure
840
     */
841
    public function verify()
842
    {
843
        if (!$this->childCount()) {
844
            return true; // tree is empty
845
        }
846
847
        $errors = array();
848
        $meta = $this->getClassMetadata();
849
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
850
        if (isset($config['root'])) {
851
            $trees = $this->getRootNodes();
852
            foreach ($trees as $tree) {
853
                $this->verifyTree($errors, $tree);
854
            }
855
        } else {
856
            $this->verifyTree($errors);
857
        }
858
859
        return $errors ?: true;
860
    }
861
862
    /**
863
     * NOTE: flush your entity manager after
864
     *
865
     * Tries to recover the tree
866
     *
867
     * @return void
868
     */
869
    public function recover()
870
    {
871
        if ($this->verify() === true) {
872
            return;
873
        }
874
875
        $meta = $this->getClassMetadata();
876
        $em = $this->getEntityManager();
877
        $config = $this->listener->getConfiguration($em, $meta->name);
878
        $self = $this;
879
880
        $doRecover = function ($root, &$count, $level = 0) use ($meta, $config, $self, $em, &$doRecover) {
881
            $lft = $count++;
882
            foreach ($self->getChildren($root, true) as $child) {
883
                $doRecover($child, $count, $level + 1);
884
            }
885
            $rgt = $count++;
886
            $meta->getReflectionProperty($config['left'])->setValue($root, $lft);
887
            $meta->getReflectionProperty($config['right'])->setValue($root, $rgt);
888
            if (isset($config['level'])) {
889
                $meta->getReflectionProperty($config['level'])->setValue($root, $level);
890
            }
891
            $em->persist($root);
892
        };
893
894
        if (isset($config['root'])) {
895
            foreach ($this->getRootNodes() as $root) {
896
                $count = 1; // reset on every root node
897
                $doRecover($root, $count);
898
            }
899
        } else {
900
            $count = 1;
901
            foreach ($this->getChildren(null, true) as $root) {
902
                $doRecover($root, $count);
903
            }
904
        }
905
    }
906
907
    /**
908
     * Added in Chamilo.
909
     */
910
    public function recoverNode($node, $sortByField = null)
911
    {
912
        $meta = $this->getClassMetadata();
913
        $em = $this->getEntityManager();
914
        $config = $this->listener->getConfiguration($em, $meta->name);
915
        $doRecover = function ($root, &$count, $level = 0) use ($meta, $config, $node, $em, $sortByField, &$doRecover) {
0 ignored issues
show
Unused Code introduced by
The import $node is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
916
            $lft = $count++;
917
            foreach ($this->getChildren($root, true, $sortByField) as $child) {
918
                $doRecover($child, $count, $level + 1);
919
            }
920
            $rgt = $count++;
921
            $meta->getReflectionProperty($config['left'])->setValue($root, $lft);
922
            $meta->getReflectionProperty($config['right'])->setValue($root, $rgt);
923
            if (isset($config['level'])) {
924
                $meta->getReflectionProperty($config['level'])->setValue($root, $level);
925
            }
926
            $em->persist($root);
927
        };
928
929
        $count = 1;
930
        $doRecover($node, $count);
931
    }
932
933
    public function verifyNode($node)
934
    {
935
        if (!$node->childCount()) {
936
            return true; // tree is empty
937
        }
938
939
        $errors = array();
940
        $this->verifyTree($errors, $node);
941
942
        return $errors ?: true;
943
    }
944
945
    /**
946
     * {@inheritDoc}
947
     */
948
    public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false)
949
    {
950
        $meta = $this->getClassMetadata();
951
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
952
953
        return $this->childrenQueryBuilder(
954
            $node,
955
            $direct,
956
            isset($config['root']) ? array($config['root'], $config['left']) : $config['left'],
957
            'ASC',
958
            $includeNode
959
        );
960
    }
961
962
    /**
963
     * {@inheritDoc}
964
     */
965
    public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false)
966
    {
967
        return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery();
968
    }
969
970
    /**
971
     * {@inheritdoc}
972
     */
973
    public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
974
    {
975
        return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult();
976
    }
977
978
    /**
979
     * {@inheritdoc}
980
     */
981
    protected function validate()
982
    {
983
        return $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName() === Strategy::NESTED;
984
    }
985
986
    /**
987
     * Collect errors on given tree if
988
     * where are any
989
     *
990
     * @param array  $errors
991
     * @param object $root
992
     */
993
    private function verifyTree(&$errors, $root = null)
994
    {
995
        $meta = $this->getClassMetadata();
996
        $em = $this->getEntityManager();
997
        $config = $this->listener->getConfiguration($em, $meta->name);
998
999
        $identifier = $meta->getSingleIdentifierFieldName();
1000
        $rootId = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($root) : null;
1001
        $qb = $this->getQueryBuilder();
1002
        $qb->select($qb->expr()->min('node.'.$config['left']))
1003
            ->from($config['useObjectClass'], 'node')
1004
        ;
1005
        if (isset($config['root'])) {
1006
            $qb->where($qb->expr()->eq('node.'.$config['root'], ':rid'));
1007
            $qb->setParameter('rid', $rootId);
1008
        }
1009
        $min = intval($qb->getQuery()->getSingleScalarResult());
1010
        $edge = $this->listener->getStrategy($em, $meta->name)->max($em, $config['useObjectClass'], $rootId);
1011
        // check duplicate right and left values
1012
        for ($i = $min; $i <= $edge; $i++) {
1013
            $qb = $this->getQueryBuilder();
1014
            $qb->select($qb->expr()->count('node.'.$identifier))
1015
                ->from($config['useObjectClass'], 'node')
1016
                ->where($qb->expr()->orX(
1017
                    $qb->expr()->eq('node.'.$config['left'], $i),
1018
                    $qb->expr()->eq('node.'.$config['right'], $i)
1019
                ))
1020
            ;
1021
            if (isset($config['root'])) {
1022
                $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
1023
                $qb->setParameter('rid', $rootId);
1024
            }
1025
            $count = intval($qb->getQuery()->getSingleScalarResult());
1026
            if ($count !== 1) {
1027
                if ($count === 0) {
1028
                    $errors[] = "index [{$i}], missing".($root ? ' on tree root: '.$rootId : '');
1029
                } else {
1030
                    $errors[] = "index [{$i}], duplicate".($root ? ' on tree root: '.$rootId : '');
1031
                }
1032
            }
1033
        }
1034
        // check for missing parents
1035
        $qb = $this->getQueryBuilder();
1036
        $qb->select('node')
1037
            ->from($config['useObjectClass'], 'node')
1038
            ->leftJoin('node.'.$config['parent'], 'parent')
1039
            ->where($qb->expr()->isNotNull('node.'.$config['parent']))
1040
            ->andWhere($qb->expr()->isNull('parent.'.$identifier))
1041
        ;
1042
        if (isset($config['root'])) {
1043
            $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
1044
            $qb->setParameter('rid', $rootId);
1045
        }
1046
        $nodes = $qb->getQuery()->getArrayResult();
1047
        if (count($nodes)) {
1048
            foreach ($nodes as $node) {
1049
                $errors[] = "node [{$node[$identifier]}] has missing parent".($root ? ' on tree root: '.$rootId : '');
1050
            }
1051
1052
            return; // loading broken relation can cause infinite loop
1053
        }
1054
1055
        $qb = $this->getQueryBuilder();
1056
        $qb->select('node')
1057
            ->from($config['useObjectClass'], 'node')
1058
            ->where($qb->expr()->lt('node.'.$config['right'], 'node.'.$config['left']))
1059
        ;
1060
        if (isset($config['root'])) {
1061
            $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
1062
            $qb->setParameter('rid', $rootId);
1063
        }
1064
        $result = $qb->getQuery()
1065
            ->setMaxResults(1)
1066
            ->getResult(Query::HYDRATE_ARRAY);
1067
        $node = count($result) ? array_shift($result) : null;
1068
1069
        if ($node) {
1070
            $id = $node[$identifier];
1071
            $errors[] = "node [{$id}], left is greater than right".($root ? ' on tree root: '.$rootId : '');
1072
        }
1073
1074
        $qb = $this->getQueryBuilder();
1075
        $qb->select('node')
1076
            ->from($config['useObjectClass'], 'node')
1077
        ;
1078
        if (isset($config['root'])) {
1079
            $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
1080
            $qb->setParameter('rid', $rootId);
1081
        }
1082
        $nodes = $qb->getQuery()->getResult(Query::HYDRATE_OBJECT);
1083
1084
        foreach ($nodes as $node) {
1085
            $right = $meta->getReflectionProperty($config['right'])->getValue($node);
1086
            $left = $meta->getReflectionProperty($config['left'])->getValue($node);
1087
            $id = $meta->getReflectionProperty($identifier)->getValue($node);
1088
            $parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
1089
            if (!$right || !$left) {
1090
                $errors[] = "node [{$id}] has invalid left or right values";
1091
            } elseif ($right == $left) {
1092
                $errors[] = "node [{$id}] has identical left and right values";
1093
            } elseif ($parent) {
1094
                if ($parent instanceof Proxy && !$parent->__isInitialized__) {
1095
                    $em->refresh($parent);
1096
                }
1097
                $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent);
1098
                $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent);
1099
                $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
1100
                if ($left < $parentLeft) {
1101
                    $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value";
1102
                } elseif ($right > $parentRight) {
1103
                    $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value";
1104
                }
1105
            } else {
1106
                $qb = $this->getQueryBuilder();
1107
                $qb->select($qb->expr()->count('node.'.$identifier))
1108
                    ->from($config['useObjectClass'], 'node')
1109
                    ->where($qb->expr()->lt('node.'.$config['left'], $left))
1110
                    ->andWhere($qb->expr()->gt('node.'.$config['right'], $right))
1111
                ;
1112
                if (isset($config['root'])) {
1113
                    $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
1114
                    $qb->setParameter('rid', $rootId);
1115
                }
1116
                if ($count = intval($qb->getQuery()->getSingleScalarResult())) {
1117
                    $errors[] = "node [{$id}] parent field is blank, but it has a parent";
1118
                }
1119
            }
1120
        }
1121
    }
1122
1123
    /**
1124
     * Removes single node without touching children
1125
     *
1126
     * @internal
1127
     *
1128
     * @param EntityWrapper $wrapped
1129
     */
1130
    private function removeSingle(EntityWrapper $wrapped)
1131
    {
1132
        $meta = $this->getClassMetadata();
1133
        $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
1134
1135
        $pk = $meta->getSingleIdentifierFieldName();
1136
        $nodeId = $wrapped->getIdentifier();
1137
        // prevent from deleting whole branch
1138
        $qb = $this->getQueryBuilder();
1139
        $qb->update($config['useObjectClass'], 'node')
1140
            ->set('node.'.$config['left'], 0)
1141
            ->set('node.'.$config['right'], 0);
1142
1143
        $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id'));
1144
        $qb->setParameter('id', $nodeId);
1145
        $qb->getQuery()->getSingleScalarResult();
1146
1147
        // remove the node from database
1148
        $qb = $this->getQueryBuilder();
1149
        $qb->delete($config['useObjectClass'], 'node');
1150
        $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id'));
1151
        $qb->setParameter('id', $nodeId);
1152
        $qb->getQuery()->getSingleScalarResult();
1153
1154
        // remove from identity map
1155
        $this->getEntityManager()->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject());
1156
    }
1157
}
1158