Passed
Push — master ( c76aa7...6c061b )
by Julito
08:15 queued 11s
created

NestedTreeRepositoryTrait::moveDown()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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