1 <?php
2
3 4 5 6 7 8
9 class Hierarchy extends DataObjectDecorator {
10
11 protected $markedNodes;
12 protected $markingFilter;
13 14 15
16 protected $_cache_numChildren;
17
18 function augmentSQL(SQLQuery &$query) {
19
20 }
21
22 function augmentDatabase() {
23
24 }
25
26 function augmentWrite(&$manipulation) {
27
28 }
29
30 31 32 33 34 35 36 37 38 39 40 41
42 public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $child->Title', $extraArg = null, $limitToMarked = false, $childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren", $rootCall = true, $minNodeCount = 30) {
43 if ($limitToMarked && $rootCall) {
44 $this->markingFinished($numChildrenMethod);
45 }
46
47 if ($this->owner->hasMethod($childrenMethod)) {
48 $children = $this->owner->$childrenMethod($extraArg);
49 } else {
50 user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
51 $childrenMethod, get_class($this->owner)), E_USER_ERROR);
52 }
53
54 if ($children) {
55 if ($attributes) {
56 $attributes = " $attributes";
57 }
58
59 $output = "<ul$attributes>\n";
60
61 foreach ($children as $child) {
62 if (!$limitToMarked || $child->isMarked()) {
63 $foundAChild = true;
64 $output .= eval("return $titleEval;") . "\n" .
65 $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, $childrenMethod, $numChildrenMethod, false, $minNodeCount) . "</li>\n";
66 }
67 }
68
69 $output .= "</ul>\n";
70 }
71
72 if (isset($foundAChild) && $foundAChild) {
73 return $output;
74 }
75 }
76
77 78 79 80 81 82 83 84 85 86 87
88 public function markPartialTree($minNodeCount = 30, $context = null, $childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
89 if (!is_numeric($minNodeCount))
90 $minNodeCount = 30;
91
92 $this->markedNodes = array($this->owner->ID => $this->owner);
93 $this->owner->markUnexpanded();
94
95
96 while (list($id, $node) = each($this->markedNodes)) {
97 $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
98 if ($minNodeCount && sizeof($this->markedNodes) >= $minNodeCount) {
99 break;
100 }
101 }
102 return sizeof($this->markedNodes);
103 }
104
105 106 107 108 109
110 public function setMarkingFilter($parameterName, $parameterValue) {
111 $this->markingFilter = array(
112 "parameter" => $parameterName,
113 "value" => $parameterValue
114 );
115 }
116
117 118 119 120 121
122 public function setMarkingFilterFunction($funcName) {
123 $this->markingFilter = array(
124 "func" => $funcName,
125 );
126 }
127
128 129 130 131 132
133 public function markingFilterMatches($node) {
134 if (!$this->markingFilter) {
135 return true;
136 }
137
138 if (isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
139 if (is_array($this->markingFilter['value'])) {
140 $ret = false;
141 foreach ($this->markingFilter['value'] as $value) {
142 $ret = $ret || $node->$parameterName == $value;
143 if ($ret == true) {
144 break;
145 }
146 }
147 return $ret;
148 } else {
149 return ($node->$parameterName == $this->markingFilter['value']);
150 }
151 } else if ($func = $this->markingFilter['func']) {
152 return call_user_func($func, $node);
153 }
154 }
155
156 157 158 159
160 public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
161 if ($node->hasMethod($childrenMethod)) {
162 $children = $node->$childrenMethod($context);
163 } else {
164 user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
165 $childrenMethod, get_class($this->owner)), E_USER_ERROR);
166 }
167
168 $node->markExpanded();
169 if ($children) {
170 foreach ($children as $child) {
171 if (!$this->markingFilter || $this->markingFilterMatches($child)) {
172 if ($child->$numChildrenMethod()) {
173 $child->markUnexpanded();
174 } else {
175 $child->markExpanded();
176 }
177 $this->markedNodes[$child->ID] = $child;
178 }
179 }
180 }
181 }
182
183 184 185 186
187 protected function markingFinished($numChildrenMethod = "numChildren") {
188
189 if ($this->markedNodes) {
190 foreach ($this->markedNodes as $id => $node) {
191 if (!$node->isExpanded() && !$node->$numChildrenMethod()) {
192 $node->markExpanded();
193 }
194 }
195 }
196 }
197
198 199 200 201
202 public function markingClasses() {
203 $classes = '';
204 if (!$this->isExpanded()) {
205 $classes .= " unexpanded";
206 }
207 if (!$this->isTreeOpened()) {
208 $classes .= " closed";
209 }
210 return $classes;
211 }
212
213 214 215 216 217
218 public function markById($id, $open = false) {
219 if (isset($this->markedNodes[$id])) {
220 $this->markChildren($this->markedNodes[$id]);
221 if ($open) {
222 $this->markedNodes[$id]->markOpened();
223 }
224 return true;
225 } else {
226 return false;
227 }
228 }
229
230 231 232 233
234 public function markToExpose($childObj) {
235 if (is_object($childObj)) {
236 $stack = array_reverse($childObj->parentStack());
237 foreach ($stack as $stackItem) {
238 $this->markById($stackItem->ID, true);
239 }
240 }
241 }
242
243 244 245
246 public function markedNodeIDs() {
247 return array_keys($this->markedNodes);
248 }
249
250 251 252 253
254 public function parentStack() {
255 $p = $this->owner;
256
257 while ($p) {
258 $stack[] = $p;
259 $p = $p->ParentID ? $p->Parent() : null;
260 }
261
262 return $stack;
263 }
264
265 266 267 268
269 protected static $marked = array();
270 271 272 273
274 protected static $expanded = array();
275 276 277 278
279 protected static $treeOpened = array();
280
281 282 283
284 public function markExpanded() {
285 self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
286 self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
287 }
288
289 290 291
292 public function markUnexpanded() {
293 self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
294 self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false;
295 }
296
297 298 299
300 public function markOpened() {
301 self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
302 self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
303 }
304
305 306 307 308
309 public function isMarked() {
310 $baseClass = ClassInfo::baseDataClass($this->owner->class);
311 $id = $this->owner->ID;
312 return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
313 }
314
315 316 317 318
319 public function isExpanded() {
320 $baseClass = ClassInfo::baseDataClass($this->owner->class);
321 $id = $this->owner->ID;
322 return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
323 }
324
325 326 327
328 public function isTreeOpened() {
329 $baseClass = ClassInfo::baseDataClass($this->owner->class);
330 $id = $this->owner->ID;
331 return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
332 }
333
334 335 336
337 public function partialTreeAsUL($minCount = 50) {
338 $children = $this->owner->AllChildren();
339 if ($children) {
340 if ($attributes)
341 $attributes = " $attributes";
342 $output = "<ul$attributes>\n";
343
344 foreach ($children as $child) {
345 $output .= eval("return $titleEval;") . "\n" .
346 $child->getChildrenAsUL("", $titleEval, $extraArg) . "</li>\n";
347 }
348 $output .= "</ul>\n";
349 }
350 return $output;
351 }
352
353 354 355 356
357 public function getDescendantIDList() {
358 $idList = array();
359 $this->loadDescendantIDListInto($idList);
360 return $idList;
361 }
362
363 364 365 366
367 public function loadDescendantIDListInto(&$idList) {
368 if ($children = $this->AllChildren()) {
369 foreach ($children as $child) {
370 if (in_array($child->ID, $idList)) {
371 continue;
372 }
373 $idList[] = $child->ID;
374 $ext = $child->getExtensionInstance('Hierarchy');
375 $ext->setOwner($child);
376 $ext->loadDescendantIDListInto($idList);
377 $ext->clearOwner();
378 }
379 }
380 }
381
382 383 384 385
386 public function Children() {
387 if (!(isset($this->_cache_children) && $this->_cache_children)) {
388 $result = $this->owner->stageChildren(false);
389 if (isset($result)) {
390 $this->_cache_children = new DataObjectSet();
391 foreach ($result as $child) {
392 if ($child->canView()) {
393 $this->_cache_children->push($child);
394 }
395 }
396 }
397 }
398 return $this->_cache_children;
399 }
400
401 402 403 404
405 public function AllChildren($st=1) {
406 return $this->owner->stageChildren($st);
407 }
408
409 410 411 412 413 414 415 416
417 public function AllChildrenIncludingDeleted($context = null) {
418 return $this->doAllChildrenIncludingDeleted($context);
419 }
420
421 422 423 424 425 426
427 public function doAllChildrenIncludingDeleted($context = null) {
428 if (!$this->owner)
429 user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
430
431 $idxStageChildren = array();
432 $idxLiveChildren = array();
433
434 $baseClass = ClassInfo::baseDataClass($this->owner->class);
435 if ($baseClass) {
436 $stageChildren = $this->owner->stageChildren(true);
437
438
439 if ($this->owner->hasExtension('Versioned')) {
440
441 $liveChildren = $this->owner->liveChildren(true, true);
442 if ($liveChildren) {
443 foreach ($liveChildren as $child) {
444 $stageChildren->push($child);
445 }
446 }
447 }
448
449 $this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
450 } else {
451 user_error("Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'", E_USER_ERROR);
452 }
453
454 return $stageChildren;
455 }
456
457 458 459 460
461 public function AllHistoricalChildren() {
462 $baseClass = ClassInfo::baseDataClass($this->owner->class);
463 return Versioned::get_including_deleted($baseClass,
464 "\"ParentID\" = " . (int) $this->owner->ID, "\"$baseClass\".\"ID\" ASC");
465 }
466
467 468 469
470 public function numHistoricalChildren() {
471 $query = Versioned::get_including_deleted_query(ClassInfo::baseDataClass($this->owner->class),
472 "\"ParentID\" = " . (int) $this->owner->ID);
473
474 return $query->unlimitedRowCount();
475 }
476
477 478 479 480 481 482 483 484
485 public function numChildren($cache = true) {
486 $baseClass = ClassInfo::baseDataClass($this->owner->class);
487
488
489 if (!$cache || !is_numeric($this->_cache_numChildren)) {
490
491 $query = new SQLQuery(
492 "COUNT(*)",
493 "\"$baseClass\"",
494 sprintf('"ParentID" = %d', $this->owner->ID)
495 );
496 $this->owner->extend('augmentSQL', $query);
497 $this->owner->extend('augmentNumChildrenCountQuery', $query);
498
499 $this->_cache_numChildren = (int) $query->execute()->value();
500 }
501
502
503 return $this->_cache_numChildren;
504 }
505
506 507 508 509 510 511 512
513 public function stageChildren($showAll = false) {
514 if ($this->owner->db('ShowInMenus')) {
515 $extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1";
516 } else {
517 $extraFilter = '';
518 }
519 if (!isset($_REQUEST['filter']) || $_REQUEST['filter'] != 'CMSSiteTreeFilter_Search') {
520 if ((($showAll === false) || ($showAll == 1)) && $this->owner->db('ShowOnlyInTab')) {
521 $extraFilter .= " AND \"ShowOnlyInTab\"=0";
522 }
523 }
524
525 $baseClass = ClassInfo::baseDataClass($this->owner->class);
526
527 if (is_a($this->owner, 'File') && $this->owner->hasDatabaseField('Hidden')) {
528 $extraFilter .= " AND \"Hidden\"=0";
529 }
530
531 $limit = '';
532 $sort = '';
533
534
535 $cms = preg_match("/admin\/?$/", $_SERVER['REQUEST_URI']) || preg_match("/admin\/getsubtree/", $_SERVER['REQUEST_URI']);
536
537
538 if (preg_match("/admin\/filterSiteTree/", $_SERVER['REQUEST_URI']))
539 $cms = false;
540
541
542 $limit = $this->owner->NumberCMSChildren;
543
544 if ($cms && $limit)
545 $sort = "Created DESC";
546 else
547 $limit = '';
548
549 $staged = DataObject::get($baseClass, "\"{$baseClass}\".\"ParentID\" = "
550 . (int) $this->owner->ID . " AND \"{$baseClass}\".\"ID\" != " . (int) $this->owner->ID
551 . $extraFilter, $sort, "", $limit);
552
553 if (!$staged)
554 $staged = new DataObjectSet();
555 $this->owner->extend("augmentStageChildren", $staged, $showAll);
556 return $staged;
557 }
558
559 560 561 562 563 564 565 566
567 public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
568 if ($this->owner->db('ShowInMenus')) {
569 $extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1";
570 } else {
571 $extraFilter = '';
572 }
573 if (!isset($_REQUEST['filter']) || $_REQUEST['filter'] != 'CMSSiteTreeFilter_Search') {
574 if ($this->owner->db('ShowOnlyInTab')) {
575 $extraFilter .= " AND \"ShowOnlyInTab\"=0";
576 }
577 }
578 $join = "";
579
580 $baseClass = ClassInfo::baseDataClass($this->owner->class);
581
582 $filter = "\"{$baseClass}\".\"ParentID\" = " . (int) $this->owner->ID
583 . " AND \"{$baseClass}\".\"ID\" != " . (int) $this->owner->ID
584 . $extraFilter;
585
586 if ($onlyDeletedFromStage) {
587
588
589 $join = "LEFT JOIN {$baseClass} ON {$baseClass}.\"ID\" = \"{$baseClass}\".\"ID\"";
590 $filter .= " AND {$baseClass}.\"ID\" IS NULL";
591 }
592
593 $oldStage = Versioned::current_stage();
594 Versioned::reading_stage('Live');
595
596
597
598 $query = singleton($baseClass)->extendedSQL($filter, null, null, $join);
599
600
601
602 $correctedSQL = str_replace(array("LEFT JOIN {$baseClass}", "{$baseClass}.\"ID\""),
603 array("LEFT JOIN \"{$baseClass}\"", "\"{$baseClass}\".\"ID\""), $query->sql());
604
605 $result = $this->owner->buildDataObjectSet(DB::query($correctedSQL));
606
607 Versioned::reading_stage($oldStage);
608
609 return $result;
610 }
611
612 613 614 615
616 public function getParent($filter = '') {
617 if ($p = $this->owner->__get("ParentID")) {
618 $tableClasses = ClassInfo::dataClassesFor($this->owner->class);
619 $baseClass = array_shift($tableClasses);
620 $filter .= ( $filter) ? " AND " : "" . "\"$baseClass\".\"ID\" = $p";
621 return DataObject::get_one($this->owner->class, $filter);
622 }
623 }
624
625 626 627 628 629
630 public function getAncestors() {
631 $ancestors = new DataObjectSet();
632 $object = $this->owner;
633
634 while ($object = $object->getParent()) {
635 $ancestors->push($object);
636 }
637
638 return $ancestors;
639 }
640
641 642 643 644 645 646
647 public function naturalPrev($className, $afterNode = null) {
648 return null;
649 }
650
651 652 653 654 655 656 657 658
659 public function naturalNext($className = null, $root = 0, $afterNode = null) {
660
661
662 if ($afterNode && $afterNode->ID != $this->owner->ID) {
663 if (!$className || ($className && $this->owner->class == $className)) {
664 return $this->owner;
665 }
666 }
667
668 $nextNode = null;
669 $baseClass = ClassInfo::baseDataClass($this->owner->class);
670
671 $children = DataObject::get(ClassInfo::baseDataClass($this->owner->class), "\"$baseClass\".\"ParentID\"={$this->owner->ID}" . ( ( $afterNode ) ? " AND \"Sort\" > " . sprintf('%d', $afterNode->Sort) : "" ), '"Sort" ASC');
672
673
674 675
676
677 if ($children) {
678 foreach ($children as $node) {
679 if ($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
680 break;
681 }
682 }
683
684 if ($nextNode) {
685 return $nextNode;
686 }
687 }
688
689
690 if (!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class) && ($parent = $this->owner->Parent())) {
691 return $parent->naturalNext($className, $root, $this->owner);
692 }
693
694 return null;
695 }
696
697 function flushCache() {
698 $this->_cache_children = null;
699 $this->_cache_numChildren = null;
700 }
701
702 }
703 ?>
704
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-