Completed
Push — master ( deca00...5388ff )
by Sam
24s
created

Hierarchy::markingClasses()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 1
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Hierarchy;
4
5
use SilverStripe\Admin\LeftAndMain;
6
use SilverStripe\CMS\Model\SiteTree;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Core\Resettable;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\ValidationResult;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\DataExtension;
15
use SilverStripe\ORM\Versioning\Versioned;
16
use Exception;
17
18
/**
19
 * DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents. The most
20
 * obvious example of this is SiteTree.
21
 *
22
 * @property int $ParentID
23
 * @property DataObject $owner
24
 * @method DataObject Parent()
25
 */
26
class Hierarchy extends DataExtension implements Resettable
27
{
28
29
    protected $markedNodes;
30
31
    protected $markingFilter;
32
33
    /** @var int */
34
    protected $_cache_numChildren;
35
36
    /**
37
     * The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least
38
     * this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be
39
     * lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30
40
     * children, the actual node count will be 50 (all root nodes plus first expanded child).
41
     *
42
     * @config
43
     * @var int
44
     */
45
    private static $node_threshold_total = 50;
46
47
    /**
48
     * Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available
49
     * server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding
50
     * this value typically won't display any children, although this is configurable through the $nodeCountCallback
51
     * parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting.
52
     *
53
     * @config
54
     * @var int
55
     */
56
    private static $node_threshold_leaf = 250;
57
58
    /**
59
     * A list of classnames to exclude from display in both the CMS and front end
60
     * displays. ->Children() and ->AllChildren affected.
61
     * Especially useful for big sets of pages like listings
62
     * If you use this, and still need the classes to be editable
63
     * then add a model admin for the class
64
     * Note: Does not filter subclasses (non-inheriting)
65
     *
66
     * @var array
67
     * @config
68
     */
69
    private static $hide_from_hierarchy = array();
70
71
    /**
72
     * A list of classnames to exclude from display in the page tree views of the CMS,
73
     * unlike $hide_from_hierarchy above which effects both CMS and front end.
74
     * Especially useful for big sets of pages like listings
75
     * If you use this, and still need the classes to be editable
76
     * then add a model admin for the class
77
     * Note: Does not filter subclasses (non-inheriting)
78
     *
79
     * @var array
80
     * @config
81
     */
82
    private static $hide_from_cms_tree = array();
83
84
    public static function get_extra_config($class, $extension, $args)
0 ignored issues
show
Unused Code introduced by
The parameter $extension is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $args is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
85
    {
86
        return array(
87
            'has_one' => array('Parent' => $class)
88
        );
89
    }
90
91
    /**
92
     * Validate the owner object - check for existence of infinite loops.
93
     *
94
     * @param ValidationResult $validationResult
95
     */
96
    public function validate(ValidationResult $validationResult)
97
    {
98
        // The object is new, won't be looping.
99
        if (!$this->owner->ID) {
100
            return;
101
        }
102
        // The object has no parent, won't be looping.
103
        if (!$this->owner->ParentID) {
104
            return;
105
        }
106
        // The parent has not changed, skip the check for performance reasons.
107
        if (!$this->owner->isChanged('ParentID')) {
108
            return;
109
        }
110
111
        // Walk the hierarchy upwards until we reach the top, or until we reach the originating node again.
112
        $node = $this->owner;
113
        while ($node) {
114
            if ($node->ParentID==$this->owner->ID) {
115
                // Hierarchy is looping.
116
                $validationResult->addError(
117
                    _t(
118
                        'Hierarchy.InfiniteLoopNotAllowed',
119
                        'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this',
120
                        'First argument is the class that makes up the hierarchy.',
121
                        array('type' => $this->owner->class)
122
                    ),
123
                    'bad',
124
                    'INFINITE_LOOP'
125
                );
126
                break;
127
            }
128
            $node = $node->ParentID ? $node->Parent() : null;
129
        }
130
131
        // At this point the $validationResult contains the response.
132
    }
133
134
    /**
135
     * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
136
     * have children they will be displayed as a UL inside a LI.
137
     *
138
     * @param string $attributes Attributes to add to the UL
139
     * @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>'
140
     * @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function
141
     * @param bool $limitToMarked Display only marked children
142
     * @param string $childrenMethod The name of the method used to get children from each object
143
     * @param string $numChildrenMethod
144
     * @param bool $rootCall Set to true for this first call, and then to false for calls inside the recursion.
145
     * You should not change this.
146
     * @param int $nodeCountThreshold See {@link self::$node_threshold_total}
147
     * @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity to
148
     * intercept the query. Useful e.g. to avoid excessive children listings (Arguments: $parent, $numChildren)
149
     * @return string
150
     */
151
    public function getChildrenAsUL(
152
        $attributes = "",
153
        $titleEval = '"<li>" . $child->Title',
154
        $extraArg = null,
155
        $limitToMarked = false,
156
        $childrenMethod = "AllChildrenIncludingDeleted",
157
        $numChildrenMethod = "numChildren",
158
        $rootCall = true,
159
        $nodeCountThreshold = null,
160
        $nodeCountCallback = null
161
    ) {
162
        if (!is_numeric($nodeCountThreshold)) {
163
            $nodeCountThreshold = Config::inst()->get(__CLASS__, 'node_threshold_total');
164
        }
165
166
        if ($limitToMarked && $rootCall) {
167
            $this->markingFinished($numChildrenMethod);
168
        }
169
170
171
        if ($nodeCountCallback) {
172
            $nodeCountWarning = $nodeCountCallback($this->owner, $this->owner->$numChildrenMethod());
173
            if ($nodeCountWarning) {
174
                return $nodeCountWarning;
175
            }
176
        }
177
178
179
        if ($this->owner->hasMethod($childrenMethod)) {
180
            $children = $this->owner->$childrenMethod($extraArg);
181
        } else {
182
            $children = null;
183
            user_error(sprintf(
184
                "Can't find the method '%s' on class '%s' for getting tree children",
185
                $childrenMethod,
186
                get_class($this->owner)
187
            ), E_USER_ERROR);
188
        }
189
190
        $output = null;
191
        if ($children) {
192
            if ($attributes) {
193
                $attributes = " $attributes";
194
            }
195
196
            $output = "<ul$attributes>\n";
197
198
            foreach ($children as $child) {
199
                if (!$limitToMarked || $child->isMarked()) {
200
                    $foundAChild = true;
201
                    if (is_callable($titleEval)) {
202
                        $output .= $titleEval($child, $numChildrenMethod);
203
                    } else {
204
                        $output .= eval("return $titleEval;");
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
205
                    }
206
                    $output .= "\n";
207
208
                    $numChildren = $child->$numChildrenMethod();
209
210
                    if (// Always traverse into opened nodes (they might be exposed as parents of search results)
211
                        $child->isExpanded()
212
                        // Only traverse into children if we haven't reached the maximum node count already.
213
                        // Otherwise, the remaining nodes are lazy loaded via ajax.
214
                        && $child->isMarked()
215
                    ) {
216
                        // Additionally check if node count requirements are met
217
                        $nodeCountWarning = $nodeCountCallback ? $nodeCountCallback($child, $numChildren) : null;
218
                        if ($nodeCountWarning) {
219
                            $output .= $nodeCountWarning;
220
                            $child->markClosed();
221
                        } else {
222
                            $output .= $child->getChildrenAsUL(
223
                                "",
224
                                $titleEval,
225
                                $extraArg,
226
                                $limitToMarked,
227
                                $childrenMethod,
228
                                $numChildrenMethod,
229
                                false,
230
                                $nodeCountThreshold
231
                            );
232
                        }
233
                    } elseif ($child->isTreeOpened()) {
234
                        // Since we're not loading children, don't mark it as open either
235
                        $child->markClosed();
236
                    }
237
                    $output .= "</li>\n";
238
                }
239
            }
240
241
            $output .= "</ul>\n";
242
        }
243
244
        if (isset($foundAChild) && $foundAChild) {
245
            return $output;
246
        }
247
        return null;
248
    }
249
250
    /**
251
     * Mark a segment of the tree, by calling mark().
252
     *
253
     * The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
254
     * get a limited number of tree nodes to show in the CMS initially.
255
     *
256
     * This method returns the number of nodes marked.  After this method is called other methods can check
257
     * {@link isExpanded()} and {@link isMarked()} on individual nodes.
258
     *
259
     * @param int $nodeCountThreshold See {@link getChildrenAsUL()}
260
     * @param mixed $context
261
     * @param string $childrenMethod
262
     * @param string $numChildrenMethod
263
     * @return int The actual number of nodes marked.
264
     */
265
    public function markPartialTree(
266
        $nodeCountThreshold = 30,
267
        $context = null,
268
        $childrenMethod = "AllChildrenIncludingDeleted",
269
        $numChildrenMethod = "numChildren"
270
    ) {
271
        if (!is_numeric($nodeCountThreshold)) {
272
            $nodeCountThreshold = 30;
273
        }
274
275
        $this->markedNodes = array($this->owner->ID => $this->owner);
276
        $this->owner->markUnexpanded();
277
278
        // foreach can't handle an ever-growing $nodes list
279
        while (list($id, $node) = each($this->markedNodes)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $id is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
280
            $children = $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
281
            if ($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
282
                // Undo marking children as opened since they're lazy loaded
283
                if ($children) {
284
                    foreach ($children as $child) {
285
                        $child->markClosed();
286
                    }
287
                }
288
                break;
289
            }
290
        }
291
        return sizeof($this->markedNodes);
292
    }
293
294
    /**
295
     * Filter the marking to only those object with $node->$parameterName == $parameterValue
296
     *
297
     * @param string $parameterName  The parameter on each node to check when marking.
298
     * @param mixed  $parameterValue The value the parameter must be to be marked.
299
     */
300
    public function setMarkingFilter($parameterName, $parameterValue)
301
    {
302
        $this->markingFilter = array(
303
            "parameter" => $parameterName,
304
            "value" => $parameterValue
305
        );
306
    }
307
308
    /**
309
     * Filter the marking to only those where the function returns true. The node in question will be passed to the
310
     * function.
311
     *
312
     * @param string $funcName The name of the function to call
313
     */
314
    public function setMarkingFilterFunction($funcName)
315
    {
316
        $this->markingFilter = array(
317
            "func" => $funcName,
318
        );
319
    }
320
321
    /**
322
     * Returns true if the marking filter matches on the given node.
323
     *
324
     * @param DataObject $node Node to check
325
     * @return bool
326
     */
327
    public function markingFilterMatches($node)
328
    {
329
        if (!$this->markingFilter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->markingFilter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
330
            return true;
331
        }
332
333
        if (isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
334
            if (is_array($this->markingFilter['value'])) {
335
                $ret = false;
336
                foreach ($this->markingFilter['value'] as $value) {
337
                    $ret = $ret||$node->$parameterName==$value;
338
                    if ($ret == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
339
                        break;
340
                    }
341
                }
342
                return $ret;
343
            } else {
344
                return ($node->$parameterName == $this->markingFilter['value']);
345
            }
346
        } elseif ($func = $this->markingFilter['func']) {
347
            return call_user_func($func, $node);
348
        }
349
    }
350
351
    /**
352
     * Mark all children of the given node that match the marking filter.
353
     *
354
     * @param DataObject $node              Parent node
355
     * @param mixed      $context
356
     * @param string     $childrenMethod    The name of the instance method to call to get the object's list of children
357
     * @param string     $numChildrenMethod The name of the instance method to call to count the object's children
358
     * @return DataList
359
     */
360
    public function markChildren(
361
        $node,
362
        $context = null,
363
        $childrenMethod = "AllChildrenIncludingDeleted",
364
        $numChildrenMethod = "numChildren"
365
    ) {
366
        if ($node->hasMethod($childrenMethod)) {
367
            $children = $node->$childrenMethod($context);
368
        } else {
369
            $children = null;
370
            user_error(sprintf(
371
                "Can't find the method '%s' on class '%s' for getting tree children",
372
                $childrenMethod,
373
                get_class($node)
374
            ), E_USER_ERROR);
375
        }
376
377
        $node->markExpanded();
378
        if ($children) {
379
            foreach ($children as $child) {
380
                $markingMatches = $this->markingFilterMatches($child);
381
                if ($markingMatches) {
382
                    // Mark a child node as unexpanded if it has children and has not already been expanded
383
                    if ($child->$numChildrenMethod() && !$child->isExpanded()) {
384
                        $child->markUnexpanded();
385
                    } else {
386
                        $child->markExpanded();
387
                    }
388
                    $this->markedNodes[$child->ID] = $child;
389
                }
390
            }
391
        }
392
393
        return $children;
394
    }
395
396
    /**
397
     * Ensure marked nodes that have children are also marked expanded. Call this after marking but before iterating
398
     * over the tree.
399
     *
400
     * @param string $numChildrenMethod The name of the instance method to call to count the object's children
401
     */
402
    protected function markingFinished($numChildrenMethod = "numChildren")
403
    {
404
        // Mark childless nodes as expanded.
405
        if ($this->markedNodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->markedNodes of type SilverStripe\ORM\DataObject[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
406
            foreach ($this->markedNodes as $id => $node) {
407
                if (!$node->isExpanded() && !$node->$numChildrenMethod()) {
408
                    $node->markExpanded();
409
                }
410
            }
411
        }
412
    }
413
414
    /**
415
     * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
416
     * marking of this DataObject.
417
     *
418
     * @param string $numChildrenMethod The name of the instance method to call to count the object's children
419
     * @return string
420
     */
421
    public function markingClasses($numChildrenMethod = "numChildren")
422
    {
423
        $classes = '';
424
        if (!$this->isExpanded()) {
425
            $classes .= " unexpanded";
426
        }
427
428
        // Set jstree open state, or mark it as a leaf (closed) if there are no children
429
        if (!$this->owner->$numChildrenMethod()) {
430
            $classes .= " jstree-leaf closed";
431
        } elseif ($this->isTreeOpened()) {
432
            $classes .= " jstree-open";
433
        } else {
434
            $classes .= " jstree-closed closed";
435
        }
436
        return $classes;
437
    }
438
439
    /**
440
     * Mark the children of the DataObject with the given ID.
441
     *
442
     * @param int  $id   ID of parent node
443
     * @param bool $open If this is true, mark the parent node as opened
444
     * @return bool
445
     */
446
    public function markById($id, $open = false)
447
    {
448
        if (isset($this->markedNodes[$id])) {
449
            $this->markChildren($this->markedNodes[$id]);
450
            if ($open) {
451
                $this->markedNodes[$id]->markOpened();
452
            }
453
            return true;
454
        } else {
455
            return false;
456
        }
457
    }
458
459
    /**
460
     * Expose the given object in the tree, by marking this page and all it ancestors.
461
     *
462
     * @param DataObject $childObj
463
     */
464
    public function markToExpose($childObj)
465
    {
466
        if (is_object($childObj)) {
467
            $stack = array_reverse($childObj->parentStack());
468
            foreach ($stack as $stackItem) {
469
                $this->markById($stackItem->ID, true);
470
            }
471
        }
472
    }
473
474
    /**
475
     * Return the IDs of all the marked nodes.
476
     *
477
     * @return array
478
     */
479
    public function markedNodeIDs()
480
    {
481
        return array_keys($this->markedNodes);
482
    }
483
484
    /**
485
     * Return an array of this page and its ancestors, ordered item -> root.
486
     *
487
     * @return SiteTree[]
488
     */
489
    public function parentStack()
490
    {
491
        $p = $this->owner;
492
493
        while ($p) {
494
            $stack[] = $p;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$stack was never initialized. Although not strictly required by PHP, it is generally a good practice to add $stack = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
495
            $p = $p->ParentID ? $p->Parent() : null;
496
        }
497
498
        return $stack;
0 ignored issues
show
Bug introduced by
The variable $stack does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
499
    }
500
501
    /**
502
     * Cache of DataObjects' marked statuses: [ClassName][ID] = bool
503
     * @var array
504
     */
505
    protected static $marked = array();
506
507
    /**
508
     * Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
509
     * @var array
510
     */
511
    protected static $expanded = array();
512
513
    /**
514
     * Cache of DataObjects' opened statuses: [ClassName][ID] = bool
515
     * @var array
516
     */
517
    protected static $treeOpened = array();
518
519
    /**
520
     * Mark this DataObject as expanded.
521
     */
522
    public function markExpanded()
523
    {
524
        self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
525
        self::$expanded[$this->owner->baseClass()][$this->owner->ID] = true;
526
    }
527
528
    /**
529
     * Mark this DataObject as unexpanded.
530
     */
531
    public function markUnexpanded()
532
    {
533
        self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
534
        self::$expanded[$this->owner->baseClass()][$this->owner->ID] = false;
535
    }
536
537
    /**
538
     * Mark this DataObject's tree as opened.
539
     */
540
    public function markOpened()
541
    {
542
        self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
543
        self::$treeOpened[$this->owner->baseClass()][$this->owner->ID] = true;
544
    }
545
546
    /**
547
     * Mark this DataObject's tree as closed.
548
     */
549
    public function markClosed()
550
    {
551
        if (isset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID])) {
552
            unset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID]);
553
        }
554
    }
555
556
    /**
557
     * Check if this DataObject is marked.
558
     *
559
     * @return bool
560
     */
561
    public function isMarked()
562
    {
563
        $baseClass = $this->owner->baseClass();
564
        $id = $this->owner->ID;
565
        return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
566
    }
567
568
    /**
569
     * Check if this DataObject is expanded.
570
     *
571
     * @return bool
572
     */
573
    public function isExpanded()
574
    {
575
        $baseClass = $this->owner->baseClass();
576
        $id = $this->owner->ID;
577
        return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
578
    }
579
580
    /**
581
     * Check if this DataObject's tree is opened.
582
     *
583
     * @return bool
584
     */
585
    public function isTreeOpened()
586
    {
587
        $baseClass = $this->owner->baseClass();
588
        $id = $this->owner->ID;
589
        return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
590
    }
591
592
    /**
593
     * Get a list of this DataObject's and all it's descendants IDs.
594
     *
595
     * @return int[]
596
     */
597
    public function getDescendantIDList()
598
    {
599
        $idList = array();
600
        $this->loadDescendantIDListInto($idList);
601
        return $idList;
602
    }
603
604
    /**
605
     * Get a list of this DataObject's and all it's descendants ID, and put them in $idList.
606
     *
607
     * @param array $idList Array to put results in.
608
     */
609
    public function loadDescendantIDListInto(&$idList)
610
    {
611
        if ($children = $this->AllChildren()) {
612
            foreach ($children as $child) {
613
                if (in_array($child->ID, $idList)) {
614
                    continue;
615
                }
616
                $idList[] = $child->ID;
617
                /** @var Hierarchy $ext */
618
                $ext = $child->getExtensionInstance('SilverStripe\ORM\Hierarchy\Hierarchy');
619
                $ext->setOwner($child);
620
                $ext->loadDescendantIDListInto($idList);
621
                $ext->clearOwner();
622
            }
623
        }
624
    }
625
626
    /**
627
     * Get the children for this DataObject.
628
     *
629
     * @return DataList
630
     */
631
    public function Children()
632
    {
633
        if (!(isset($this->_cache_children) && $this->_cache_children)) {
634
            $result = $this->owner->stageChildren(false);
635
            $children = array();
636
            foreach ($result as $record) {
637
                if ($record->canView()) {
638
                    $children[] = $record;
639
                }
640
            }
641
            $this->_cache_children = new ArrayList($children);
0 ignored issues
show
Bug introduced by
The property _cache_children does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
642
        }
643
        return $this->_cache_children;
644
    }
645
646
    /**
647
     * Return all children, including those 'not in menus'.
648
     *
649
     * @return DataList
650
     */
651
    public function AllChildren()
652
    {
653
        return $this->owner->stageChildren(true);
654
    }
655
656
    /**
657
     * Return all children, including those that have been deleted but are still in live.
658
     * - Deleted children will be marked as "DeletedFromStage"
659
     * - Added children will be marked as "AddedToStage"
660
     * - Modified children will be marked as "ModifiedOnStage"
661
     * - Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
662
     *
663
     * @param mixed $context
664
     * @return ArrayList
665
     */
666
    public function AllChildrenIncludingDeleted($context = null)
667
    {
668
        return $this->doAllChildrenIncludingDeleted($context);
669
    }
670
671
    /**
672
     * @see AllChildrenIncludingDeleted
673
     *
674
     * @param mixed $context
675
     * @return ArrayList
676
     */
677
    public function doAllChildrenIncludingDeleted($context = null)
678
    {
679
        if (!$this->owner) {
680
            user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
681
        }
682
683
        $baseClass = $this->owner->baseClass();
684
        if ($baseClass) {
685
            $stageChildren = $this->owner->stageChildren(true);
686
687
            // Add live site content that doesn't exist on the stage site, if required.
688
            if ($this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
689
                // Next, go through the live children.  Only some of these will be listed
690
                $liveChildren = $this->owner->liveChildren(true, true);
691
                if ($liveChildren) {
692
                    $merged = new ArrayList();
693
                    $merged->merge($stageChildren);
694
                    $merged->merge($liveChildren);
695
                    $stageChildren = $merged;
696
                }
697
            }
698
699
            $this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
700
        } else {
701
            user_error(
702
                "Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'",
703
                E_USER_ERROR
704
            );
705
        }
706
707
        return $stageChildren;
0 ignored issues
show
Bug introduced by
The variable $stageChildren does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
708
    }
709
710
    /**
711
     * Return all the children that this page had, including pages that were deleted from both stage & live.
712
     *
713
     * @return DataList
714
     * @throws Exception
715
     */
716
    public function AllHistoricalChildren()
717
    {
718
        if (!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
719
            throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
720
        }
721
722
        $baseTable = $this->owner->baseTable();
723
        $parentIDColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ParentID');
724
        return Versioned::get_including_deleted(
725
            $this->owner->baseClass(),
726
            [ $parentIDColumn => $this->owner->ID ],
727
            "\"{$baseTable}\".\"ID\" ASC"
728
        );
729
    }
730
731
    /**
732
     * Return the number of children that this page ever had, including pages that were deleted.
733
     *
734
     * @return int
735
     * @throws Exception
736
     */
737
    public function numHistoricalChildren()
738
    {
739
        if (!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
740
            throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
741
        }
742
743
        return $this->AllHistoricalChildren()->count();
744
    }
745
746
    /**
747
     * Return the number of direct children. By default, values are cached after the first invocation. Can be
748
     * augumented by {@link augmentNumChildrenCountQuery()}.
749
     *
750
     * @param bool $cache Whether to retrieve values from cache
751
     * @return int
752
     */
753
    public function numChildren($cache = true)
754
    {
755
        // Build the cache for this class if it doesn't exist.
756
        if (!$cache || !is_numeric($this->_cache_numChildren)) {
757
            // Hey, this is efficient now!
758
            // We call stageChildren(), because Children() has canView() filtering
759
            $this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count();
760
        }
761
762
        // If theres no value in the cache, it just means that it doesn't have any children.
763
        return $this->_cache_numChildren;
764
    }
765
766
    /**
767
     * Checks if we're on a controller where we should filter. ie. Are we loading the SiteTree?
768
     *
769
     * @return bool
770
     */
771
    public function showingCMSTree()
772
    {
773
        if (!Controller::has_curr()) {
774
            return false;
775
        }
776
        $controller = Controller::curr();
777
        return $controller instanceof LeftAndMain
778
            && in_array($controller->getAction(), array("treeview", "listview", "getsubtree"));
779
    }
780
781
    /**
782
     * Return children in the stage site.
783
     *
784
     * @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when
785
     *                      extension is applied to {@link SiteTree}.
786
     * @return DataList
787
     */
788
    public function stageChildren($showAll = false)
789
    {
790
        $baseClass = $this->owner->baseClass();
791
        $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
0 ignored issues
show
Documentation introduced by
The property hide_from_hierarchy does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
792
        $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
0 ignored issues
show
Documentation introduced by
The property hide_from_cms_tree does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
793
        $staged = $baseClass::get()
794
                ->filter('ParentID', (int)$this->owner->ID)
795
                ->exclude('ID', (int)$this->owner->ID);
796
        if ($hide_from_hierarchy) {
797
            $staged = $staged->exclude('ClassName', $hide_from_hierarchy);
798
        }
799
        if ($hide_from_cms_tree && $this->showingCMSTree()) {
800
            $staged = $staged->exclude('ClassName', $hide_from_cms_tree);
801
        }
802
        if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SilverStripe\ORM\DataOb...->owner, 'ShowInMenus') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
803
            $staged = $staged->filter('ShowInMenus', 1);
804
        }
805
        $this->owner->extend("augmentStageChildren", $staged, $showAll);
806
        return $staged;
807
    }
808
809
    /**
810
     * Return children in the live site, if it exists.
811
     *
812
     * @param bool $showAll              Include all of the elements, even those not shown in the menus. Only
813
     *                                   applicable when extension is applied to {@link SiteTree}.
814
     * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage
815
     * @return DataList
816
     * @throws Exception
817
     */
818
    public function liveChildren($showAll = false, $onlyDeletedFromStage = false)
819
    {
820
        if (!$this->owner->hasExtension(Versioned::class)) {
821
            throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
822
        }
823
824
        $baseClass = $this->owner->baseClass();
825
        $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
0 ignored issues
show
Documentation introduced by
The property hide_from_hierarchy does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
826
        $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
0 ignored issues
show
Documentation introduced by
The property hide_from_cms_tree does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
827
        $children = $baseClass::get()
828
            ->filter('ParentID', (int)$this->owner->ID)
829
            ->exclude('ID', (int)$this->owner->ID)
830
            ->setDataQueryParam(array(
831
                'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage',
832
                'Versioned.stage' => 'Live'
833
            ));
834
        if ($hide_from_hierarchy) {
835
            $children = $children->exclude('ClassName', $hide_from_hierarchy);
836
        }
837
        if ($hide_from_cms_tree && $this->showingCMSTree()) {
838
            $children = $children->exclude('ClassName', $hide_from_cms_tree);
839
        }
840
        if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SilverStripe\ORM\DataOb...->owner, 'ShowInMenus') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
841
            $children = $children->filter('ShowInMenus', 1);
842
        }
843
844
        return $children;
845
    }
846
847
    /**
848
     * Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing
849
     * is returned.
850
     *
851
     * @param string $filter
852
     * @return DataObject
853
     */
854
    public function getParent($filter = null)
855
    {
856
        $parentID = $this->owner->ParentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
857
        if (empty($parentID)) {
858
            return null;
859
        }
860
        $idSQL = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ID');
861
        return DataObject::get_one($this->owner->class, array(
862
            array($idSQL => $parentID),
863
            $filter
864
        ));
865
    }
866
867
    /**
868
     * Return all the parents of this class in a set ordered from the lowest to highest parent.
869
     *
870
     * @return ArrayList
871
     */
872
    public function getAncestors()
873
    {
874
        $ancestors = new ArrayList();
875
        $object    = $this->owner;
876
877
        while ($object = $object->getParent()) {
878
            $ancestors->push($object);
879
        }
880
881
        return $ancestors;
882
    }
883
884
    /**
885
     * Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute.
886
     *
887
     * @param string $separator
888
     * @return string
889
     */
890
    public function getBreadcrumbs($separator = ' &raquo; ')
891
    {
892
        $crumbs = array();
893
        $ancestors = array_reverse($this->owner->getAncestors()->toArray());
894
        foreach ($ancestors as $ancestor) {
895
            $crumbs[] = $ancestor->Title;
896
        }
897
        $crumbs[] = $this->owner->Title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
898
        return implode($separator, $crumbs);
899
    }
900
901
    /**
902
     * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
903
     * then search the parents.
904
     *
905
     * @todo Write!
906
     *
907
     * @param string     $className Class name of the node to find
908
     * @param DataObject $afterNode Used for recursive calls to this function
909
     * @return DataObject
910
     */
911
    public function naturalPrev($className, $afterNode = null)
0 ignored issues
show
Unused Code introduced by
The parameter $className is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $afterNode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
912
    {
913
        return null;
914
    }
915
916
    /**
917
     * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
918
     * then search the parents.
919
     * @param string     $className Class name of the node to find.
920
     * @param string|int $root      ID/ClassName of the node to limit the search to
921
     * @param DataObject $afterNode Used for recursive calls to this function
922
     * @return DataObject
923
     */
924
    public function naturalNext($className = null, $root = 0, $afterNode = null)
925
    {
926
        // If this node is not the node we are searching from, then we can possibly return this node as a solution
927
        if ($afterNode && $afterNode->ID != $this->owner->ID) {
928
            if (!$className || ($className && $this->owner->class == $className)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
929
                return $this->owner;
930
            }
931
        }
932
933
        $nextNode = null;
934
        $baseClass = $this->owner->baseClass();
935
936
        $children = $baseClass::get()
937
            ->filter('ParentID', (int)$this->owner->ID)
938
            ->sort('"Sort"', 'ASC');
939
        if ($afterNode) {
940
            $children = $children->filter('Sort:GreaterThan', $afterNode->Sort);
941
        }
942
943
        // Try all the siblings of this node after the given node
944
        /*if( $siblings = DataObject::get( $this->owner->baseClass(),
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
945
		"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
946
		> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/
947
948
        if ($children) {
949
            foreach ($children as $node) {
950
                if ($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
951
                    break;
952
                }
953
            }
954
955
            if ($nextNode) {
956
                return $nextNode;
957
            }
958
        }
959
960
        // if this is not an instance of the root class or has the root id, search the parent
961
        if (!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class)
962
                && ($parent = $this->owner->Parent())) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\ORM\DataObject. Did you maybe mean parentClass()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
963
            return $parent->naturalNext($className, $root, $this->owner);
964
        }
965
966
        return null;
967
    }
968
969
    /**
970
     * Flush all Hierarchy caches:
971
     * - Children (instance)
972
     * - NumChildren (instance)
973
     * - Marked (global)
974
     * - Expanded (global)
975
     * - TreeOpened (global)
976
     */
977
    public function flushCache()
978
    {
979
        $this->_cache_children = null;
980
        $this->_cache_numChildren = null;
981
        self::$marked = array();
982
        self::$expanded = array();
983
        self::$treeOpened = array();
984
    }
985
986
    /**
987
     * Reset global Hierarchy caches:
988
     * - Marked
989
     * - Expanded
990
     * - TreeOpened
991
     */
992
    public static function reset()
993
    {
994
        self::$marked = array();
995
        self::$expanded = array();
996
        self::$treeOpened = array();
997
    }
998
}
999