1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\ORM\Behavior;
16:
17: use Cake\Database\Expression\IdentifierExpression;
18: use Cake\Datasource\EntityInterface;
19: use Cake\Datasource\Exception\RecordNotFoundException;
20: use Cake\Event\Event;
21: use Cake\ORM\Behavior;
22: use Cake\ORM\Query;
23: use InvalidArgumentException;
24: use RuntimeException;
25:
26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:
38: class TreeBehavior extends Behavior
39: {
40: 41: 42: 43: 44:
45: protected $_primaryKey;
46:
47: 48: 49: 50: 51: 52: 53:
54: protected $_defaultConfig = [
55: 'implementedFinders' => [
56: 'path' => 'findPath',
57: 'children' => 'findChildren',
58: 'treeList' => 'findTreeList',
59: ],
60: 'implementedMethods' => [
61: 'childCount' => 'childCount',
62: 'moveUp' => 'moveUp',
63: 'moveDown' => 'moveDown',
64: 'recover' => 'recover',
65: 'removeFromTree' => 'removeFromTree',
66: 'getLevel' => 'getLevel',
67: 'formatTreeList' => 'formatTreeList',
68: ],
69: 'parent' => 'parent_id',
70: 'left' => 'lft',
71: 'right' => 'rght',
72: 'scope' => null,
73: 'level' => null,
74: 'recoverOrder' => null,
75: ];
76:
77: 78: 79:
80: public function initialize(array $config)
81: {
82: $this->_config['leftField'] = new IdentifierExpression($this->_config['left']);
83: $this->_config['rightField'] = new IdentifierExpression($this->_config['right']);
84: }
85:
86: 87: 88: 89: 90: 91: 92: 93: 94: 95:
96: public function beforeSave(Event $event, EntityInterface $entity)
97: {
98: $isNew = $entity->isNew();
99: $config = $this->getConfig();
100: $parent = $entity->get($config['parent']);
101: $primaryKey = $this->_getPrimaryKey();
102: $dirty = $entity->isDirty($config['parent']);
103: $level = $config['level'];
104:
105: if ($parent && $entity->get($primaryKey) == $parent) {
106: throw new RuntimeException("Cannot set a node's parent as itself");
107: }
108:
109: if ($isNew && $parent) {
110: $parentNode = $this->_getNode($parent);
111: $edge = $parentNode->get($config['right']);
112: $entity->set($config['left'], $edge);
113: $entity->set($config['right'], $edge + 1);
114: $this->_sync(2, '+', ">= {$edge}");
115:
116: if ($level) {
117: $entity->set($level, $parentNode[$level] + 1);
118: }
119:
120: return;
121: }
122:
123: if ($isNew && !$parent) {
124: $edge = $this->_getMax();
125: $entity->set($config['left'], $edge + 1);
126: $entity->set($config['right'], $edge + 2);
127:
128: if ($level) {
129: $entity->set($level, 0);
130: }
131:
132: return;
133: }
134:
135: if (!$isNew && $dirty && $parent) {
136: $this->_setParent($entity, $parent);
137:
138: if ($level) {
139: $parentNode = $this->_getNode($parent);
140: $entity->set($level, $parentNode[$level] + 1);
141: }
142:
143: return;
144: }
145:
146: if (!$isNew && $dirty && !$parent) {
147: $this->_setAsRoot($entity);
148:
149: if ($level) {
150: $entity->set($level, 0);
151: }
152: }
153: }
154:
155: 156: 157: 158: 159: 160: 161: 162: 163:
164: public function afterSave(Event $event, EntityInterface $entity)
165: {
166: if (!$this->_config['level'] || $entity->isNew()) {
167: return;
168: }
169:
170: $this->_setChildrenLevel($entity);
171: }
172:
173: 174: 175: 176: 177: 178:
179: protected function _setChildrenLevel($entity)
180: {
181: $config = $this->getConfig();
182:
183: if ($entity->get($config['left']) + 1 === $entity->get($config['right'])) {
184: return;
185: }
186:
187: $primaryKey = $this->_getPrimaryKey();
188: $primaryKeyValue = $entity->get($primaryKey);
189: $depths = [$primaryKeyValue => $entity->get($config['level'])];
190:
191: $children = $this->_table->find('children', [
192: 'for' => $primaryKeyValue,
193: 'fields' => [$this->_getPrimaryKey(), $config['parent'], $config['level']],
194: 'order' => $config['left'],
195: ]);
196:
197:
198: foreach ($children as $node) {
199: $parentIdValue = $node->get($config['parent']);
200: $depth = $depths[$parentIdValue] + 1;
201: $depths[$node->get($primaryKey)] = $depth;
202:
203: $this->_table->updateAll(
204: [$config['level'] => $depth],
205: [$primaryKey => $node->get($primaryKey)]
206: );
207: }
208: }
209:
210: 211: 212: 213: 214: 215: 216:
217: public function beforeDelete(Event $event, EntityInterface $entity)
218: {
219: $config = $this->getConfig();
220: $this->_ensureFields($entity);
221: $left = $entity->get($config['left']);
222: $right = $entity->get($config['right']);
223: $diff = $right - $left + 1;
224:
225: if ($diff > 2) {
226: $query = $this->_scope($this->_table->query())
227: ->delete()
228: ->where(function ($exp) use ($config, $left, $right) {
229:
230: return $exp
231: ->gte($config['leftField'], $left + 1)
232: ->lte($config['leftField'], $right - 1);
233: });
234: $statement = $query->execute();
235: $statement->closeCursor();
236: }
237:
238: $this->_sync($diff, '-', "> {$right}");
239: }
240:
241: 242: 243: 244: 245: 246: 247: 248: 249: 250:
251: protected function _setParent($entity, $parent)
252: {
253: $config = $this->getConfig();
254: $parentNode = $this->_getNode($parent);
255: $this->_ensureFields($entity);
256: $parentLeft = $parentNode->get($config['left']);
257: $parentRight = $parentNode->get($config['right']);
258: $right = $entity->get($config['right']);
259: $left = $entity->get($config['left']);
260:
261: if ($parentLeft > $left && $parentLeft < $right) {
262: throw new RuntimeException(sprintf(
263: 'Cannot use node "%s" as parent for entity "%s"',
264: $parent,
265: $entity->get($this->_getPrimaryKey())
266: ));
267: }
268:
269:
270: $diff = $right - $left + 1;
271: $targetLeft = $parentRight;
272: $targetRight = $diff + $parentRight - 1;
273: $min = $parentRight;
274: $max = $left - 1;
275:
276: if ($left < $targetLeft) {
277:
278: $targetLeft = $parentRight - $diff;
279: $targetRight = $parentRight - 1;
280: $min = $right + 1;
281: $max = $parentRight - 1;
282: $diff *= -1;
283: }
284:
285: if ($right - $left > 1) {
286:
287: $internalLeft = $left + 1;
288: $internalRight = $right - 1;
289: $this->_sync($targetLeft - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}", true);
290: }
291:
292: $this->_sync($diff, '+', "BETWEEN {$min} AND {$max}");
293:
294: if ($right - $left > 1) {
295: $this->_unmarkInternalTree();
296: }
297:
298:
299: $entity->set($config['left'], $targetLeft);
300: $entity->set($config['right'], $targetRight);
301: }
302:
303: 304: 305: 306: 307: 308: 309: 310:
311: protected function _setAsRoot($entity)
312: {
313: $config = $this->getConfig();
314: $edge = $this->_getMax();
315: $this->_ensureFields($entity);
316: $right = $entity->get($config['right']);
317: $left = $entity->get($config['left']);
318: $diff = $right - $left;
319:
320: if ($right - $left > 1) {
321:
322: $internalLeft = $left + 1;
323: $internalRight = $right - 1;
324: $this->_sync($edge - $diff - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}", true);
325: }
326:
327: $this->_sync($diff + 1, '-', "BETWEEN {$right} AND {$edge}");
328:
329: if ($right - $left > 1) {
330: $this->_unmarkInternalTree();
331: }
332:
333: $entity->set($config['left'], $edge - $diff);
334: $entity->set($config['right'], $edge);
335: }
336:
337: 338: 339: 340: 341: 342: 343:
344: protected function _unmarkInternalTree()
345: {
346: $config = $this->getConfig();
347: $this->_table->updateAll(
348: function ($exp) use ($config) {
349:
350: $leftInverse = clone $exp;
351: $leftInverse->setConjunction('*')->add('-1');
352: $rightInverse = clone $leftInverse;
353:
354: return $exp
355: ->eq($config['leftField'], $leftInverse->add($config['leftField']))
356: ->eq($config['rightField'], $rightInverse->add($config['rightField']));
357: },
358: function ($exp) use ($config) {
359:
360: return $exp->lt($config['leftField'], 0);
361: }
362: );
363: }
364:
365: 366: 367: 368: 369: 370: 371: 372: 373: 374:
375: public function findPath(Query $query, array $options)
376: {
377: if (empty($options['for'])) {
378: throw new InvalidArgumentException("The 'for' key is required for find('path')");
379: }
380:
381: $config = $this->getConfig();
382: list($left, $right) = array_map(
383: function ($field) {
384: return $this->_table->aliasField($field);
385: },
386: [$config['left'], $config['right']]
387: );
388:
389: $node = $this->_table->get($options['for'], ['fields' => [$left, $right]]);
390:
391: return $this->_scope($query)
392: ->where([
393: "$left <=" => $node->get($config['left']),
394: "$right >=" => $node->get($config['right']),
395: ])
396: ->order([$left => 'ASC']);
397: }
398:
399: 400: 401: 402: 403: 404: 405: 406:
407: public function childCount(EntityInterface $node, $direct = false)
408: {
409: $config = $this->getConfig();
410: $parent = $this->_table->aliasField($config['parent']);
411:
412: if ($direct) {
413: return $this->_scope($this->_table->find())
414: ->where([$parent => $node->get($this->_getPrimaryKey())])
415: ->count();
416: }
417:
418: $this->_ensureFields($node);
419:
420: return ($node->get($config['right']) - $node->get($config['left']) - 1) / 2;
421: }
422:
423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438:
439: public function findChildren(Query $query, array $options)
440: {
441: $config = $this->getConfig();
442: $options += ['for' => null, 'direct' => false];
443: list($parent, $left, $right) = array_map(
444: function ($field) {
445: return $this->_table->aliasField($field);
446: },
447: [$config['parent'], $config['left'], $config['right']]
448: );
449:
450: list($for, $direct) = [$options['for'], $options['direct']];
451:
452: if (empty($for)) {
453: throw new InvalidArgumentException("The 'for' key is required for find('children')");
454: }
455:
456: if ($query->clause('order') === null) {
457: $query->order([$left => 'ASC']);
458: }
459:
460: if ($direct) {
461: return $this->_scope($query)->where([$parent => $for]);
462: }
463:
464: $node = $this->_getNode($for);
465:
466: return $this->_scope($query)
467: ->where([
468: "{$right} <" => $node->get($config['right']),
469: "{$left} >" => $node->get($config['left']),
470: ]);
471: }
472:
473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489:
490: public function findTreeList(Query $query, array $options)
491: {
492: $left = $this->_table->aliasField($this->getConfig('left'));
493:
494: $results = $this->_scope($query)
495: ->find('threaded', [
496: 'parentField' => $this->getConfig('parent'),
497: 'order' => [$left => 'ASC'],
498: ]);
499:
500: return $this->formatTreeList($results, $options);
501: }
502:
503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519:
520: public function formatTreeList(Query $query, array $options = [])
521: {
522: return $query->formatResults(function ($results) use ($options) {
523:
524: $options += [
525: 'keyPath' => $this->_getPrimaryKey(),
526: 'valuePath' => $this->_table->getDisplayField(),
527: 'spacer' => '_',
528: ];
529:
530: return $results
531: ->listNested()
532: ->printer($options['valuePath'], $options['keyPath'], $options['spacer']);
533: });
534: }
535:
536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546:
547: public function removeFromTree(EntityInterface $node)
548: {
549: return $this->_table->getConnection()->transactional(function () use ($node) {
550: $this->_ensureFields($node);
551:
552: return $this->_removeFromTree($node);
553: });
554: }
555:
556: 557: 558: 559: 560: 561: 562:
563: protected function _removeFromTree($node)
564: {
565: $config = $this->getConfig();
566: $left = $node->get($config['left']);
567: $right = $node->get($config['right']);
568: $parent = $node->get($config['parent']);
569:
570: $node->set($config['parent'], null);
571:
572: if ($right - $left == 1) {
573: return $this->_table->save($node);
574: }
575:
576: $primary = $this->_getPrimaryKey();
577: $this->_table->updateAll(
578: [$config['parent'] => $parent],
579: [$config['parent'] => $node->get($primary)]
580: );
581: $this->_sync(1, '-', 'BETWEEN ' . ($left + 1) . ' AND ' . ($right - 1));
582: $this->_sync(2, '-', "> {$right}");
583: $edge = $this->_getMax();
584: $node->set($config['left'], $edge + 1);
585: $node->set($config['right'], $edge + 2);
586: $fields = [$config['parent'], $config['left'], $config['right']];
587:
588: $this->_table->updateAll($node->extract($fields), [$primary => $node->get($primary)]);
589:
590: foreach ($fields as $field) {
591: $node->setDirty($field, false);
592: }
593:
594: return $node;
595: }
596:
597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607:
608: public function moveUp(EntityInterface $node, $number = 1)
609: {
610: if ($number < 1) {
611: return false;
612: }
613:
614: return $this->_table->getConnection()->transactional(function () use ($node, $number) {
615: $this->_ensureFields($node);
616:
617: return $this->_moveUp($node, $number);
618: });
619: }
620:
621: 622: 623: 624: 625: 626: 627: 628:
629: protected function _moveUp($node, $number)
630: {
631: $config = $this->getConfig();
632: list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']];
633: list($nodeParent, $nodeLeft, $nodeRight) = array_values($node->extract([$parent, $left, $right]));
634:
635: $targetNode = null;
636: if ($number !== true) {
637: $targetNode = $this->_scope($this->_table->find())
638: ->select([$left, $right])
639: ->where(["$parent IS" => $nodeParent])
640: ->where(function ($exp) use ($config, $nodeLeft) {
641:
642: return $exp->lt($config['rightField'], $nodeLeft);
643: })
644: ->orderDesc($config['leftField'])
645: ->offset($number - 1)
646: ->limit(1)
647: ->first();
648: }
649: if (!$targetNode) {
650: $targetNode = $this->_scope($this->_table->find())
651: ->select([$left, $right])
652: ->where(["$parent IS" => $nodeParent])
653: ->where(function ($exp) use ($config, $nodeLeft) {
654:
655: return $exp->lt($config['rightField'], $nodeLeft);
656: })
657: ->orderAsc($config['leftField'])
658: ->limit(1)
659: ->first();
660:
661: if (!$targetNode) {
662: return $node;
663: }
664: }
665:
666: list($targetLeft) = array_values($targetNode->extract([$left, $right]));
667: $edge = $this->_getMax();
668: $leftBoundary = $targetLeft;
669: $rightBoundary = $nodeLeft - 1;
670:
671: $nodeToEdge = $edge - $nodeLeft + 1;
672: $shift = $nodeRight - $nodeLeft + 1;
673: $nodeToHole = $edge - $leftBoundary + 1;
674: $this->_sync($nodeToEdge, '+', "BETWEEN {$nodeLeft} AND {$nodeRight}");
675: $this->_sync($shift, '+', "BETWEEN {$leftBoundary} AND {$rightBoundary}");
676: $this->_sync($nodeToHole, '-', "> {$edge}");
677:
678: $node->set($left, $targetLeft);
679: $node->set($right, $targetLeft + ($nodeRight - $nodeLeft));
680:
681: $node->setDirty($left, false);
682: $node->setDirty($right, false);
683:
684: return $node;
685: }
686:
687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697:
698: public function moveDown(EntityInterface $node, $number = 1)
699: {
700: if ($number < 1) {
701: return false;
702: }
703:
704: return $this->_table->getConnection()->transactional(function () use ($node, $number) {
705: $this->_ensureFields($node);
706:
707: return $this->_moveDown($node, $number);
708: });
709: }
710:
711: 712: 713: 714: 715: 716: 717: 718:
719: protected function _moveDown($node, $number)
720: {
721: $config = $this->getConfig();
722: list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']];
723: list($nodeParent, $nodeLeft, $nodeRight) = array_values($node->extract([$parent, $left, $right]));
724:
725: $targetNode = null;
726: if ($number !== true) {
727: $targetNode = $this->_scope($this->_table->find())
728: ->select([$left, $right])
729: ->where(["$parent IS" => $nodeParent])
730: ->where(function ($exp) use ($config, $nodeRight) {
731:
732: return $exp->gt($config['leftField'], $nodeRight);
733: })
734: ->orderAsc($config['leftField'])
735: ->offset($number - 1)
736: ->limit(1)
737: ->first();
738: }
739: if (!$targetNode) {
740: $targetNode = $this->_scope($this->_table->find())
741: ->select([$left, $right])
742: ->where(["$parent IS" => $nodeParent])
743: ->where(function ($exp) use ($config, $nodeRight) {
744:
745: return $exp->gt($config['leftField'], $nodeRight);
746: })
747: ->orderDesc($config['leftField'])
748: ->limit(1)
749: ->first();
750:
751: if (!$targetNode) {
752: return $node;
753: }
754: }
755:
756: list(, $targetRight) = array_values($targetNode->extract([$left, $right]));
757: $edge = $this->_getMax();
758: $leftBoundary = $nodeRight + 1;
759: $rightBoundary = $targetRight;
760:
761: $nodeToEdge = $edge - $nodeLeft + 1;
762: $shift = $nodeRight - $nodeLeft + 1;
763: $nodeToHole = $edge - $rightBoundary + $shift;
764: $this->_sync($nodeToEdge, '+', "BETWEEN {$nodeLeft} AND {$nodeRight}");
765: $this->_sync($shift, '-', "BETWEEN {$leftBoundary} AND {$rightBoundary}");
766: $this->_sync($nodeToHole, '-', "> {$edge}");
767:
768: $node->set($left, $targetRight - ($nodeRight - $nodeLeft));
769: $node->set($right, $targetRight);
770:
771: $node->setDirty($left, false);
772: $node->setDirty($right, false);
773:
774: return $node;
775: }
776:
777: 778: 779: 780: 781: 782: 783:
784: protected function _getNode($id)
785: {
786: $config = $this->getConfig();
787: list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']];
788: $primaryKey = $this->_getPrimaryKey();
789: $fields = [$parent, $left, $right];
790: if ($config['level']) {
791: $fields[] = $config['level'];
792: }
793:
794: $node = $this->_scope($this->_table->find())
795: ->select($fields)
796: ->where([$this->_table->aliasField($primaryKey) => $id])
797: ->first();
798:
799: if (!$node) {
800: throw new RecordNotFoundException("Node \"{$id}\" was not found in the tree.");
801: }
802:
803: return $node;
804: }
805:
806: 807: 808: 809: 810: 811:
812: public function recover()
813: {
814: $this->_table->getConnection()->transactional(function () {
815: $this->_recoverTree();
816: });
817: }
818:
819: 820: 821: 822: 823: 824: 825: 826:
827: protected function _recoverTree($counter = 0, $parentId = null, $level = -1)
828: {
829: $config = $this->getConfig();
830: list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']];
831: $primaryKey = $this->_getPrimaryKey();
832: $aliasedPrimaryKey = $this->_table->aliasField($primaryKey);
833: $order = $config['recoverOrder'] ?: $aliasedPrimaryKey;
834:
835: $query = $this->_scope($this->_table->query())
836: ->select([$aliasedPrimaryKey])
837: ->where([$this->_table->aliasField($parent) . ' IS' => $parentId])
838: ->order($order)
839: ->disableHydration();
840:
841: $leftCounter = $counter;
842: $nextLevel = $level + 1;
843: foreach ($query as $row) {
844: $counter++;
845: $counter = $this->_recoverTree($counter, $row[$primaryKey], $nextLevel);
846: }
847:
848: if ($parentId === null) {
849: return $counter;
850: }
851:
852: $fields = [$left => $leftCounter, $right => $counter + 1];
853: if ($config['level']) {
854: $fields[$config['level']] = $level;
855: }
856:
857: $this->_table->updateAll(
858: $fields,
859: [$primaryKey => $parentId]
860: );
861:
862: return $counter + 1;
863: }
864:
865: 866: 867: 868: 869:
870: protected function _getMax()
871: {
872: $field = $this->_config['right'];
873: $rightField = $this->_config['rightField'];
874: $edge = $this->_scope($this->_table->find())
875: ->select([$field])
876: ->orderDesc($rightField)
877: ->first();
878:
879: if (empty($edge->{$field})) {
880: return 0;
881: }
882:
883: return $edge->{$field};
884: }
885:
886: 887: 888: 889: 890: 891: 892: 893: 894: 895: 896: 897:
898: protected function _sync($shift, $dir, $conditions, $mark = false)
899: {
900: $config = $this->_config;
901:
902: foreach ([$config['leftField'], $config['rightField']] as $field) {
903: $query = $this->_scope($this->_table->query());
904: $exp = $query->newExpr();
905:
906: $movement = clone $exp;
907: $movement->add($field)->add((string)$shift)->setConjunction($dir);
908:
909: $inverse = clone $exp;
910: $movement = $mark ?
911: $inverse->add($movement)->setConjunction('*')->add('-1') :
912: $movement;
913:
914: $where = clone $exp;
915: $where->add($field)->add($conditions)->setConjunction('');
916:
917: $query->update()
918: ->set($exp->eq($field, $movement))
919: ->where($where);
920:
921: $query->execute()->closeCursor();
922: }
923: }
924:
925: 926: 927: 928: 929: 930: 931:
932: protected function _scope($query)
933: {
934: $scope = $this->getConfig('scope');
935:
936: if (is_array($scope)) {
937: return $query->where($scope);
938: }
939: if (is_callable($scope)) {
940: return $scope($query);
941: }
942:
943: return $query;
944: }
945:
946: 947: 948: 949: 950: 951: 952:
953: protected function _ensureFields($entity)
954: {
955: $config = $this->getConfig();
956: $fields = [$config['left'], $config['right']];
957: $values = array_filter($entity->extract($fields));
958: if (count($values) === count($fields)) {
959: return;
960: }
961:
962: $fresh = $this->_table->get($entity->get($this->_getPrimaryKey()), $fields);
963: $entity->set($fresh->extract($fields), ['guard' => false]);
964:
965: foreach ($fields as $field) {
966: $entity->setDirty($field, false);
967: }
968: }
969:
970: 971: 972: 973: 974:
975: protected function _getPrimaryKey()
976: {
977: if (!$this->_primaryKey) {
978: $primaryKey = (array)$this->_table->getPrimaryKey();
979: $this->_primaryKey = $primaryKey[0];
980: }
981:
982: return $this->_primaryKey;
983: }
984:
985: 986: 987: 988: 989: 990:
991: public function getLevel($entity)
992: {
993: $primaryKey = $this->_getPrimaryKey();
994: $id = $entity;
995: if ($entity instanceof EntityInterface) {
996: $id = $entity->get($primaryKey);
997: }
998: $config = $this->getConfig();
999: $entity = $this->_table->find('all')
1000: ->select([$config['left'], $config['right']])
1001: ->where([$primaryKey => $id])
1002: ->first();
1003:
1004: if ($entity === null) {
1005: return false;
1006: }
1007:
1008: $query = $this->_table->find('all')->where([
1009: $config['left'] . ' <' => $entity[$config['left']],
1010: $config['right'] . ' >' => $entity[$config['right']],
1011: ]);
1012:
1013: return $this->_scope($query)->count();
1014: }
1015: }
1016: