Skip to content

Commit 7881dce

Browse files
authored
Merge pull request #1877 from delboy1978uk/feature/tree-root
Feature/tree root
2 parents ec905a1 + 102faa4 commit 7881dce

File tree

10 files changed

+398
-49
lines changed

10 files changed

+398
-49
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"doctrine/orm": "to use the extensions with the ORM"
5555
},
5656
"autoload": {
57-
"psr-0": { "Gedmo\\": "lib/" }
57+
"psr-4": { "Gedmo\\": "lib/Gedmo" }
5858
},
5959
"config": {
6060
"bin-dir": "bin"

composer7.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"doctrine/orm": "to use the extensions with the ORM"
5959
},
6060
"autoload": {
61-
"psr-0": { "Gedmo\\": "lib/" }
61+
"psr-4": { "Gedmo\\": "lib/Gedmo" }
6262
},
6363
"config": {
6464
"bin-dir": "bin"

doc/tree.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ Thanks for contributions to:
2222
- **[everzet](http://github.com/everzet) Kudryashov Konstantin** for TreeLevel implementation
2323
- **[stof](http://github.com/stof) Christophe Coevoet** for getTreeLeafs function
2424

25+
Update **2018-02-26**
26+
27+
- Nodes with no Parent can now be sorted based on a tree root id being an id from another table. Existing behaviour
28+
is unchanged unless you add properties to the `@TreeRoot` annotation. Example: You have two categories with no parent,
29+
horror and comedy, which are actually categories of 'Movie', which is in another table. Usually calling `moveUp()` or
30+
`moveDown()` would be impossible, but now you can add `@TreeRoot(identifierMethod="getRoot")`, where `getRoot` is the
31+
name of your class method returning the root id/entity.
32+
33+
2534
Update **2017-04-22**
2635

2736
- Added the `TreeObjectHydrator` class for building trees from entities

lib/Gedmo/Mapping/Annotation/Tree.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ final class Tree extends Annotation
2323

2424
/** @var integer */
2525
public $lockingTimeout = 3;
26+
27+
/** @var string $identifierMethod */
28+
public $identifierMethod;
2629
}

lib/Gedmo/Mapping/Annotation/TreeRoot.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515
*/
1616
final class TreeRoot extends Annotation
1717
{
18+
/** @var string $identifierMethod */
19+
public $identifierMethod;
1820
}

lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,6 @@ public function getNextSiblingsQueryBuilder($node, $includeSelf = false)
408408

409409
$config = $this->listener->getConfiguration($this->_em, $meta->name);
410410
$parent = $wrapped->getPropertyValue($config['parent']);
411-
if (isset($config['root']) && !$parent) {
412-
throw new InvalidArgumentException("Cannot get siblings from tree root node");
413-
}
414411

415412
$left = $wrapped->getPropertyValue($config['left']);
416413

@@ -427,6 +424,11 @@ public function getNextSiblingsQueryBuilder($node, $includeSelf = false)
427424
$wrappedParent = new EntityWrapper($parent, $this->_em);
428425
$qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid'));
429426
$qb->setParameter('pid', $wrappedParent->getIdentifier());
427+
} else if (isset($config['root']) && !$parent) {
428+
$qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':root'));
429+
$qb->andWhere($qb->expr()->isNull('node.parent'));
430+
$method = $config['rootIdentifierMethod'];
431+
$qb->setParameter('root', $node->$method());
430432
} else {
431433
$qb->andWhere($qb->expr()->isNull('node.'.$config['parent']));
432434
}
@@ -483,9 +485,6 @@ public function getPrevSiblingsQueryBuilder($node, $includeSelf = false)
483485

484486
$config = $this->listener->getConfiguration($this->_em, $meta->name);
485487
$parent = $wrapped->getPropertyValue($config['parent']);
486-
if (isset($config['root']) && !$parent) {
487-
throw new InvalidArgumentException("Cannot get siblings from tree root node");
488-
}
489488

490489
$left = $wrapped->getPropertyValue($config['left']);
491490

@@ -502,6 +501,11 @@ public function getPrevSiblingsQueryBuilder($node, $includeSelf = false)
502501
$wrappedParent = new EntityWrapper($parent, $this->_em);
503502
$qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid'));
504503
$qb->setParameter('pid', $wrappedParent->getIdentifier());
504+
} else if (isset($config['root']) && !$parent) {
505+
$qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':root'));
506+
$qb->andWhere($qb->expr()->isNull('node.parent'));
507+
$method = $config['rootIdentifierMethod'];
508+
$qb->setParameter('root', $node->$method());
505509
} else {
506510
$qb->andWhere($qb->expr()->isNull('node.'.$config['parent']));
507511
}

lib/Gedmo/Tree/Mapping/Driver/Annotation.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ public function readExtendedMetadata($meta, array &$config)
163163
);
164164
}
165165
}
166-
166+
$annotation = $this->reader->getPropertyAnnotation($property, self::ROOT);
167+
$config['rootIdentifierMethod'] = $annotation->identifierMethod;
167168
$config['root'] = $field;
168169
}
169170
// level

lib/Gedmo/Tree/Strategy/ORM/Nested.php

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Gedmo\Tree\Strategy\ORM;
44

5+
use Doctrine\Common\Collections\ArrayCollection;
6+
use Doctrine\Common\Collections\Criteria;
57
use Doctrine\ORM\EntityManagerInterface;
68
use Doctrine\ORM\Mapping\ClassMetadata;
79
use Gedmo\Exception\UnexpectedValueException;
@@ -123,7 +125,9 @@ public function processScheduledInsertion($em, $node, AdapterInterface $ea)
123125
if (isset($config['level'])) {
124126
$meta->getReflectionProperty($config['level'])->setValue($node, 0);
125127
}
126-
if (isset($config['root']) && !$meta->hasAssociation($config['root'])) {
128+
if (isset($config['root']) && !$meta->hasAssociation($config['root']) && !$config['rootIdentifierMethod']) {
129+
$meta->getReflectionProperty($config['root'])->setValue($node, 0);
130+
} else if (isset($config['rootIdentifierMethod']) && is_null($meta->getReflectionProperty($config['root'])->getValue($node))) {
127131
$meta->getReflectionProperty($config['root'])->setValue($node, 0);
128132
}
129133
}
@@ -203,18 +207,17 @@ public function processScheduledDelete($em, $node)
203207
$qb = $em->createQueryBuilder();
204208
$qb->select('node')
205209
->from($config['useObjectClass'], 'node')
206-
->where($qb->expr()->between('node.'.$config['left'], '?1', '?2'))
207-
->setParameters(array(1 => $leftValue, 2 => $rightValue))
208-
;
210+
->where($qb->expr()->between('node.' . $config['left'], '?1', '?2'))
211+
->setParameters(array(1 => $leftValue, 2 => $rightValue));
209212

210213
if (isset($config['root'])) {
211-
$qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
214+
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
212215
$qb->setParameter('rid', $rootId);
213216
}
214217
$q = $qb->getQuery();
215218
// get nodes for deletion
216219
$nodes = $q->getResult();
217-
foreach ((array) $nodes as $removalNode) {
220+
foreach ((array)$nodes as $removalNode) {
218221
$uow->scheduleForDelete($removalNode);
219222
}
220223
}
@@ -311,7 +314,7 @@ public function updateNode(EntityManagerInterface $em, $node, $parent, $position
311314
$level = 0;
312315
$treeSize = $right - $left + 1;
313316
$newRoot = null;
314-
if ($parent) {
317+
if ($parent) { // || (!$parent && isset($config['rootIdentifierMethod']))
315318
$wrappedParent = AbstractWrapper::wrap($parent, $em);
316319

317320
$parentRoot = isset($config['root']) ? $wrappedParent->getPropertyValue($config['root']) : null;
@@ -342,10 +345,15 @@ public function updateNode(EntityManagerInterface $em, $node, $parent, $position
342345
$level++;
343346
} else {
344347
$newParent = $wrappedParent->getPropertyValue($config['parent']);
345-
if (is_null($newParent) && (isset($config['root']) || $isNewNode)) {
348+
349+
if (is_null($newParent) && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) {
346350
throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible");
351+
} else if (is_null($newParent) && (isset($config['root']) || $isNewNode)) {
352+
// root is a different column from parent (pointing to another table?), do nothing
353+
} else {
354+
$wrapped->setPropertyValue($config['parent'], $newParent);
347355
}
348-
$wrapped->setPropertyValue($config['parent'], $newParent);
356+
349357
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node);
350358
$start = $parentLeft;
351359
}
@@ -358,10 +366,14 @@ public function updateNode(EntityManagerInterface $em, $node, $parent, $position
358366
$level++;
359367
} else {
360368
$newParent = $wrappedParent->getPropertyValue($config['parent']);
361-
if (is_null($newParent) && (isset($config['root']) || $isNewNode)) {
369+
if (is_null($newParent) && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) {
362370
throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible");
371+
} else if (is_null($newParent) && (isset($config['root']) || $isNewNode)) {
372+
// root is a different column from parent (pointing to another table?), do nothing
373+
} else {
374+
$wrapped->setPropertyValue($config['parent'], $newParent);
363375
}
364-
$wrapped->setPropertyValue($config['parent'], $newParent);
376+
365377
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node);
366378
$start = $parentRight + 1;
367379
}
@@ -445,8 +457,23 @@ public function updateNode(EntityManagerInterface $em, $node, $parent, $position
445457
}
446458
} else {
447459
$start = 1;
448-
449-
if ($meta->isSingleValuedAssociation($config['root'])) {
460+
if (isset($config['rootIdentifierMethod'])) {
461+
$method = $config['rootIdentifierMethod'];
462+
$newRoot = $node->$method();
463+
$repo = $em->getRepository($config['useObjectClass']);
464+
465+
$criteria = new Criteria();
466+
$criteria->andWhere(Criteria::expr()->notIn($wrapped->getMetadata()->identifier[0], [$wrapped->getIdentifier()]));
467+
$criteria->andWhere(Criteria::expr()->eq($config['root'], $node->$method()));
468+
$criteria->andWhere(Criteria::expr()->isNull($config['parent']));
469+
$criteria->andWhere(Criteria::expr()->eq($config['level'], 0));
470+
$criteria->orderBy([$config['right'] => Criteria::ASC]);
471+
$roots = $repo->matching($criteria)->toArray();
472+
$last = array_pop($roots);
473+
474+
$start = ($last) ? $meta->getFieldValue($last, $config['right']) + 1 : 1;
475+
476+
} else if ($meta->isSingleValuedAssociation($config['root'])) {
450477
$newRoot = $node;
451478
} else {
452479
$newRoot = $wrapped->getIdentifier();
@@ -472,28 +499,28 @@ public function updateNode(EntityManagerInterface $em, $node, $parent, $position
472499
$qb = $em->createQueryBuilder();
473500
$qb->update($config['useObjectClass'], 'node');
474501
if (isset($config['root'])) {
475-
$qb->set('node.'.$config['root'], ':rid');
502+
$qb->set('node.' . $config['root'], ':rid');
476503
$qb->setParameter('rid', $newRoot);
477504
$wrapped->setPropertyValue($config['root'], $newRoot);
478505
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['root'], $newRoot);
479506
}
480507
if (isset($config['level'])) {
481-
$qb->set('node.'.$config['level'], $level);
508+
$qb->set('node.' . $config['level'], $level);
482509
$wrapped->setPropertyValue($config['level'], $level);
483510
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['level'], $level);
484511
}
485512
if (isset($newParent)) {
486513
$wrappedNewParent = AbstractWrapper::wrap($newParent, $em);
487514
$newParentId = $wrappedNewParent->getIdentifier();
488-
$qb->set('node.'.$config['parent'], ':pid');
515+
$qb->set('node.' . $config['parent'], ':pid');
489516
$qb->setParameter('pid', $newParentId);
490517
$wrapped->setPropertyValue($config['parent'], $newParent);
491518
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $newParent);
492519
}
493-
$qb->set('node.'.$config['left'], $left + $diff);
494-
$qb->set('node.'.$config['right'], $right + $diff);
520+
$qb->set('node.' . $config['left'], $left + $diff);
521+
$qb->set('node.' . $config['right'], $right + $diff);
495522
// node id cannot be null
496-
$qb->where($qb->expr()->eq('node.'.$identifierField, ':id'));
523+
$qb->where($qb->expr()->eq('node.' . $identifierField, ':id'));
497524
$qb->setParameter('id', $nodeId);
498525
$qb->getQuery()->getSingleScalarResult();
499526
$wrapped->setPropertyValue($config['left'], $left + $diff);
@@ -522,12 +549,11 @@ public function max(EntityManagerInterface $em, $class, $rootId = 0)
522549
$meta = $em->getClassMetadata($class);
523550
$config = $this->listener->getConfiguration($em, $meta->name);
524551
$qb = $em->createQueryBuilder();
525-
$qb->select($qb->expr()->max('node.'.$config['right']))
526-
->from($config['useObjectClass'], 'node')
527-
;
552+
$qb->select($qb->expr()->max('node.' . $config['right']))
553+
->from($config['useObjectClass'], 'node');
528554

529555
if (isset($config['root']) && $rootId) {
530-
$qb->where($qb->expr()->eq('node.'.$config['root'], ':rid'));
556+
$qb->where($qb->expr()->eq('node.' . $config['root'], ':rid'));
531557
$qb->setParameter('rid', $rootId);
532558
}
533559
$query = $qb->getQuery();
@@ -539,6 +565,10 @@ public function max(EntityManagerInterface $em, $class, $rootId = 0)
539565
/**
540566
* Shift tree left and right values by delta
541567
*
568+
* @param EntityManager $em
569+
* @param string $class
570+
* @param integer $first
571+
* @param integer $delta
542572
* @param EntityManagerInterface $em
543573
* @param string $class
544574
* @param integer $first
@@ -554,22 +584,20 @@ public function shiftRL(EntityManagerInterface $em, $class, $first, $delta, $roo
554584
$absDelta = abs($delta);
555585
$qb = $em->createQueryBuilder();
556586
$qb->update($config['useObjectClass'], 'node')
557-
->set('node.'.$config['left'], "node.{$config['left']} {$sign} {$absDelta}")
558-
->where($qb->expr()->gte('node.'.$config['left'], $first))
559-
;
587+
->set('node.' . $config['left'], "node.{$config['left']} {$sign} {$absDelta}")
588+
->where($qb->expr()->gte('node.' . $config['left'], $first));
560589
if (isset($config['root'])) {
561-
$qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
590+
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
562591
$qb->setParameter('rid', $root);
563592
}
564593
$qb->getQuery()->getSingleScalarResult();
565594

566595
$qb = $em->createQueryBuilder();
567596
$qb->update($config['useObjectClass'], 'node')
568-
->set('node.'.$config['right'], "node.{$config['right']} {$sign} {$absDelta}")
569-
->where($qb->expr()->gte('node.'.$config['right'], $first))
570-
;
597+
->set('node.' . $config['right'], "node.{$config['right']} {$sign} {$absDelta}")
598+
->where($qb->expr()->gte('node.' . $config['right'], $first));
571599
if (isset($config['root'])) {
572-
$qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
600+
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
573601
$qb->setParameter('rid', $root);
574602
}
575603

@@ -618,7 +646,7 @@ public function shiftRL(EntityManagerInterface $em, $class, $first, $delta, $roo
618646
* @param integer $delta
619647
* @param integer|string $root
620648
* @param integer|string $destRoot
621-
* @param integer $levelDelta
649+
* @param integer $levelDelta
622650
*/
623651
public function shiftRangeRL(EntityManagerInterface $em, $class, $first, $last, $delta, $root = null, $destRoot = null, $levelDelta = null)
624652
{
@@ -632,19 +660,18 @@ public function shiftRangeRL(EntityManagerInterface $em, $class, $first, $last,
632660

633661
$qb = $em->createQueryBuilder();
634662
$qb->update($config['useObjectClass'], 'node')
635-
->set('node.'.$config['left'], "node.{$config['left']} {$sign} {$absDelta}")
636-
->set('node.'.$config['right'], "node.{$config['right']} {$sign} {$absDelta}")
637-
->where($qb->expr()->gte('node.'.$config['left'], $first))
638-
->andWhere($qb->expr()->lte('node.'.$config['right'], $last))
639-
;
663+
->set('node.' . $config['left'], "node.{$config['left']} {$sign} {$absDelta}")
664+
->set('node.' . $config['right'], "node.{$config['right']} {$sign} {$absDelta}")
665+
->where($qb->expr()->gte('node.' . $config['left'], $first))
666+
->andWhere($qb->expr()->lte('node.' . $config['right'], $last));
640667
if (isset($config['root'])) {
641-
$qb->set('node.'.$config['root'], ':drid');
668+
$qb->set('node.' . $config['root'], ':drid');
642669
$qb->setParameter('drid', $destRoot);
643-
$qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid'));
670+
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
644671
$qb->setParameter('rid', $root);
645672
}
646673
if (isset($config['level'])) {
647-
$qb->set('node.'.$config['level'], "node.{$config['level']} {$levelSign} {$absLevelDelta}");
674+
$qb->set('node.' . $config['level'], "node.{$config['level']} {$levelSign} {$absLevelDelta}");
648675
}
649676
$qb->getQuery()->getSingleScalarResult();
650677
// update in memory nodes increases performance, saves some IO

0 commit comments

Comments
 (0)