MarkedSet   F
last analyzed

Complexity

Total Complexity 97

Size/Duplication

Total Lines 801
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 227
dl 0
loc 801
rs 2
c 0
b 0
f 0
wmc 97

36 Methods

Rating   Name   Duplication   Size   Complexity  
A getChildren() 0 4 2
A isExpanded() 0 4 2
A getNumChildrenMethod() 0 3 2
A getNodeCountThreshold() 0 4 2
A isTreeOpened() 0 4 2
A getLimitingEnabled() 0 3 1
A setMaxChildNodes() 0 4 1
A setNodeCountThreshold() 0 4 1
A getMaxChildNodes() 0 4 2
A getChildrenMethod() 0 3 2
A setLimitingEnabled() 0 4 1
A markedNodeIDs() 0 3 1
A getNumChildren() 0 4 1
A isMarked() 0 4 2
A markPartialTree() 0 24 5
A markOpened() 0 6 2
B markChildren() 0 34 7
A markExpanded() 0 6 2
A setChildrenMethod() 0 12 2
A setNumChildrenMethod() 0 12 2
A setMarkingFilterFunction() 0 6 1
A markById() 0 10 3
A getChildrenAsArray() 0 16 2
A setMarkingFilter() 0 7 1
A clearMarks() 0 5 1
A __construct() 0 24 6
A isNodeLimited() 0 17 4
A markUnexpanded() 0 6 2
A markingClasses() 0 19 4
A markClosed() 0 6 2
B getSubtree() 0 36 7
A markingFilterMatches() 0 25 5
A markToExpose() 0 10 3
A getSubtreeAsArray() 0 33 6
A renderSubtree() 0 28 6
A renderChildren() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like MarkedSet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MarkedSet, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\ORM\Hierarchy;
4
5
use InvalidArgumentException;
6
use LogicException;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\ORM\ArrayLib;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\FieldType\DBField;
12
use SilverStripe\ORM\SS_List;
13
use SilverStripe\View\ArrayData;
14
15
/**
16
 * Contains a set of hierarchical objects generated from a marking compilation run.
17
 *
18
 * A set of nodes can be "marked" for later export, in order to prevent having to
19
 * export the entire contents of a potentially huge tree.
20
 */
21
class MarkedSet
22
{
23
    use Injectable;
24
25
    /**
26
     * Marked nodes for a given subtree. The first item in this list
27
     * is the root object of the subtree.
28
     *
29
     * A marked item is an item in a tree which will be included in
30
     * a resulting tree.
31
     *
32
     * @var array Map of [itemID => itemInstance]
33
     */
34
    protected $markedNodes;
35
36
    /**
37
     * Optional filter callback for filtering nodes to mark
38
     *
39
     * Array with keys:
40
     *  - parameter
41
     *  - value
42
     *  - func
43
     *
44
     * @var array
45
     * @temp made public
46
     */
47
    public $markingFilter;
48
49
    /**
50
     * @var DataObject
51
     */
52
    protected $rootNode = null;
53
54
    /**
55
     * Method to use for getting children. Defaults to 'AllChildrenIncludingDeleted'
56
     *
57
     * @var string
58
     */
59
    protected $childrenMethod = null;
60
61
    /**
62
     * Method to use for counting children. Defaults to `numChildren`
63
     *
64
     * @var string
65
     */
66
    protected $numChildrenMethod = null;
67
68
    /**
69
     * Minimum number of nodes to iterate over before stopping recursion
70
     *
71
     * @var int
72
     */
73
    protected $nodeCountThreshold = null;
74
75
    /**
76
     * Max number of nodes to return from a single children collection
77
     *
78
     * @var int
79
     */
80
    protected $maxChildNodes;
81
82
    /**
83
     * Enable limiting
84
     *
85
     * @var bool
86
     */
87
    protected $enableLimiting = true;
88
89
    /**
90
     * Create an empty set with the given class
91
     *
92
     * @param DataObject $rootNode Root node for this set. To collect the entire tree,
93
     * pass in a singelton object.
94
     * @param string $childrenMethod Override children method
95
     * @param string $numChildrenMethod Override children counting method
96
     * @param int $nodeCountThreshold Minimum threshold for number nodes to mark
97
     * @param int $maxChildNodes Maximum threshold for number of child nodes to include
98
     */
99
    public function __construct(
100
        DataObject $rootNode,
101
        $childrenMethod = null,
102
        $numChildrenMethod = null,
103
        $nodeCountThreshold = null,
104
        $maxChildNodes = null
105
    ) {
106
        if (! $rootNode::has_extension(Hierarchy::class)) {
107
            throw new InvalidArgumentException(
108
                get_class($rootNode) . " does not have the Hierarchy extension"
109
            );
110
        }
111
        $this->rootNode = $rootNode;
112
        if ($childrenMethod) {
113
            $this->setChildrenMethod($childrenMethod);
114
        }
115
        if ($numChildrenMethod) {
116
            $this->setNumChildrenMethod($numChildrenMethod);
117
        }
118
        if ($nodeCountThreshold) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $nodeCountThreshold of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
119
            $this->setNodeCountThreshold($nodeCountThreshold);
120
        }
121
        if ($maxChildNodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $maxChildNodes of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
122
            $this->setMaxChildNodes($maxChildNodes);
123
        }
124
    }
125
126
    /**
127
     * Get total number of nodes to get. This acts as a soft lower-bounds for
128
     * number of nodes to search until found.
129
     * Defaults to value of node_threshold_total of hierarchy class.
130
     *
131
     * @return int
132
     */
133
    public function getNodeCountThreshold()
134
    {
135
        return $this->nodeCountThreshold
136
            ?: $this->rootNode->config()->get('node_threshold_total');
137
    }
138
139
    /**
140
     * Max number of nodes that can be physically rendered at any level.
141
     * Acts as a hard upper bound, after which nodes will be trimmed for
142
     * performance reasons.
143
     *
144
     * @return int
145
     */
146
    public function getMaxChildNodes()
147
    {
148
        return $this->maxChildNodes
149
            ?: $this->rootNode->config()->get('node_threshold_leaf');
150
    }
151
152
    /**
153
     * Set hard limit of number of nodes to get for this level
154
     *
155
     * @param int $count
156
     * @return $this
157
     */
158
    public function setMaxChildNodes($count)
159
    {
160
        $this->maxChildNodes = $count;
161
        return $this;
162
    }
163
164
    /**
165
     * Set max node count
166
     *
167
     * @param int $total
168
     * @return $this
169
     */
170
    public function setNodeCountThreshold($total)
171
    {
172
        $this->nodeCountThreshold = $total;
173
        return $this;
174
    }
175
176
    /**
177
     * Get method to use for getting children
178
     *
179
     * @return string
180
     */
181
    public function getChildrenMethod()
182
    {
183
        return $this->childrenMethod ?: 'AllChildrenIncludingDeleted';
184
    }
185
186
    /**
187
     * Get children from this node
188
     *
189
     * @param DataObject $node
190
     * @return SS_List
191
     */
192
    protected function getChildren(DataObject $node)
193
    {
194
        $method = $this->getChildrenMethod();
195
        return $node->$method() ?: ArrayList::create();
196
    }
197
198
    /**
199
     * Set method to use for getting children
200
     *
201
     * @param string $method
202
     * @throws InvalidArgumentException
203
     * @return $this
204
     */
205
    public function setChildrenMethod($method)
206
    {
207
        // Check method is valid
208
        if (!$this->rootNode->hasMethod($method)) {
209
            throw new InvalidArgumentException(sprintf(
210
                "Can't find the method '%s' on class '%s' for getting tree children",
211
                $method,
212
                get_class($this->rootNode)
213
            ));
214
        }
215
        $this->childrenMethod = $method;
216
        return $this;
217
    }
218
219
    /**
220
     * Get method name for num children
221
     *
222
     * @return string
223
     */
224
    public function getNumChildrenMethod()
225
    {
226
        return $this->numChildrenMethod ?: 'numChildren';
227
    }
228
229
    /**
230
     * Count children
231
     *
232
     * @param DataObject $node
233
     * @return int
234
     */
235
    protected function getNumChildren(DataObject $node)
236
    {
237
        $method = $this->getNumChildrenMethod();
238
        return (int)$node->$method();
239
    }
240
241
    /**
242
     * Set method name to get num children
243
     *
244
     * @param string $method
245
     * @return $this
246
     */
247
    public function setNumChildrenMethod($method)
248
    {
249
        // Check method is valid
250
        if (!$this->rootNode->hasMethod($method)) {
251
            throw new InvalidArgumentException(sprintf(
252
                "Can't find the method '%s' on class '%s' for counting tree children",
253
                $method,
254
                get_class($this->rootNode)
255
            ));
256
        }
257
        $this->numChildrenMethod = $method;
258
        return $this;
259
    }
260
261
    /**
262
     * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
263
     * have children they will be displayed as a UL inside a LI.
264
     *
265
     * @param string $template Template for items in the list
266
     * @param array|callable $context Additional arguments to add to template when rendering
267
     * due to excessive line length. If callable, this will be executed with the current node dataobject
268
     * @return string
269
     */
270
    public function renderChildren(
271
        $template = null,
272
        $context = []
273
    ) {
274
        // Default to HTML template
275
        if (!$template) {
276
            $template = [
277
                'type' => 'Includes',
278
                self::class . '_HTML'
279
            ];
280
        }
281
        $tree = $this->getSubtree($this->rootNode, 0);
282
        $node = $this->renderSubtree($tree, $template, $context);
283
        return (string)$node->getField('SubTree');
284
    }
285
286
    /**
287
     * Get child data formatted as JSON
288
     *
289
     * @param callable $serialiseEval A callback that takes a DataObject as a single parameter,
290
     * and should return an array containing a simple array representation. This result will
291
     * replace the 'node' property at each point in the tree.
292
     * @return array
293
     */
294
    public function getChildrenAsArray($serialiseEval = null)
295
    {
296
        if (!$serialiseEval) {
297
            $serialiseEval = function ($data) {
298
                /** @var DataObject $node */
299
                $node = $data['node'];
300
                return [
301
                    'id' => $node->ID,
302
                    'title' => $node->getTitle()
303
                ];
304
            };
305
        }
306
307
        $tree = $this->getSubtree($this->rootNode, 0);
308
309
        return $this->getSubtreeAsArray($tree, $serialiseEval);
310
    }
311
312
    /**
313
     * Render a node in the tree with the given template
314
     *
315
     * @param array $data array data for current node
316
     * @param string|array $template Template to use
317
     * @param array|callable $context Additional arguments to add to template when rendering
318
     * due to excessive line length. If callable, this will be executed with the current node dataobject
319
     * @return ArrayData Viewable object representing the root node. use getField('SubTree') to get HTML
320
     */
321
    protected function renderSubtree($data, $template, $context = [])
322
    {
323
        // Render children
324
        $childNodes = new ArrayList();
325
        foreach ($data['children'] as $child) {
326
            $childData = $this->renderSubtree($child, $template, $context);
327
            $childNodes->push($childData);
328
        }
329
330
        // Build parent node
331
        $parentNode = new ArrayData($data);
332
        $parentNode->setField('children', $childNodes); // Replace raw array with template-friendly list
333
        $parentNode->setField('markingClasses', $this->markingClasses($data['node']));
334
335
        // Evaluate custom context
336
        if (!is_string($context) && is_callable($context)) {
337
            $context = call_user_func($context, $data['node']);
338
        }
339
        if ($context) {
340
            foreach ($context as $key => $value) {
341
                $parentNode->setField($key, $value);
342
            }
343
        }
344
345
        // Render
346
        $subtree = $parentNode->renderWith($template);
347
        $parentNode->setField('SubTree', $subtree);
348
        return $parentNode;
349
    }
350
351
    /**
352
     * Return sub-tree as json array
353
     *
354
     * @param array $data
355
     * @param callable $serialiseEval A callback that takes a DataObject as a single parameter,
356
     * and should return an array containing a simple array representation. This result will
357
     * replace the 'node' property at each point in the tree.
358
     * @return mixed|string
359
     */
360
    protected function getSubtreeAsArray($data, $serialiseEval)
361
    {
362
        $output = $data;
363
364
        // Serialise node
365
        $serialised = $serialiseEval($data['node']);
366
367
        // Force serialisation of DBField instances
368
        if (is_array($serialised)) {
369
            foreach ($serialised as $key => $value) {
370
                if ($value instanceof DBField) {
371
                    $serialised[$key] = $value->getSchemaValue();
372
                }
373
            }
374
375
            // Merge with top level array
376
            unset($output['node']);
377
            $output = array_merge($output, $serialised);
378
        } else {
379
            if ($serialised instanceof DBField) {
380
                $serialised = $serialised->getSchemaValue();
381
            }
382
383
            // Replace node with serialised value
384
            $output['node'] = $serialised;
385
        }
386
387
        // Replace children with serialised elements
388
        $output['children'] = [];
389
        foreach ($data['children'] as $child) {
390
            $output['children'][] = $this->getSubtreeAsArray($child, $serialiseEval);
391
        }
392
        return $output;
393
    }
394
395
    /**
396
     * Get tree data for node
397
     *
398
     * @param DataObject $node
399
     * @param int $depth
400
     * @return array|string
401
     */
402
    protected function getSubtree($node, $depth = 0)
403
    {
404
        // Check if this node is limited due to child count
405
        $numChildren = $this->getNumChildren($node);
406
        $limited = $this->isNodeLimited($node, $numChildren);
407
408
        // Build root rode
409
        $expanded = $this->isExpanded($node);
410
        $opened = $this->isTreeOpened($node);
411
        $count = ($limited && $numChildren > $this->getMaxChildNodes()) ? 0 : $numChildren;
412
        $output = [
413
            'node' => $node,
414
            'marked' => $this->isMarked($node),
415
            'expanded' => $expanded,
416
            'opened' => $opened,
417
            'depth' => $depth,
418
            'count' => $count, // Count of DB children
419
            'limited' => $limited, // Flag whether 'items' has been limited
420
            'children' => [], // Children to return in this request
421
        ];
422
423
        // Don't iterate children if past limit
424
        // or not expanded (requires subsequent request to get)
425
        if ($limited || !$expanded) {
426
            return $output;
427
        }
428
429
        // Get children
430
        $children = $this->getChildren($node);
431
        foreach ($children as $child) {
432
            // Recurse
433
            if ($this->isMarked($child)) {
434
                $output['children'][] = $this->getSubtree($child, $depth + 1);
435
            }
436
        }
437
        return $output;
438
    }
439
440
    /**
441
     * Mark a segment of the tree, by calling mark().
442
     *
443
     * The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
444
     * get a limited number of tree nodes to show in the CMS initially.
445
     *
446
     * This method returns the number of nodes marked.  After this method is called other methods can check
447
     * {@link isExpanded()} and {@link isMarked()} on individual nodes.
448
     *
449
     * @return $this
450
     */
451
    public function markPartialTree()
452
    {
453
        $nodeCountThreshold = $this->getNodeCountThreshold();
454
455
        // Add root node, not-expanded by default
456
        /** @var DataObject|Hierarchy $rootNode */
457
        $rootNode = $this->rootNode;
458
        $this->clearMarks();
459
        $this->markUnexpanded($rootNode);
460
461
        // Build markedNodes for this subtree until we reach the threshold
462
        // foreach can't handle an ever-growing $nodes list
463
        foreach (ArrayLib::iterateVolatile($this->markedNodes) as $node) {
464
            $children = $this->markChildren($node);
465
            if ($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
466
                // Undo marking children as opened since they're lazy loaded
467
                /** @var DataObject|Hierarchy $child */
468
                foreach ($children as $child) {
469
                    $this->markClosed($child);
470
                }
471
                break;
472
            }
473
        }
474
        return $this;
475
    }
476
477
    /**
478
     * Filter the marking to only those object with $node->$parameterName == $parameterValue
479
     *
480
     * @param string $parameterName  The parameter on each node to check when marking.
481
     * @param mixed  $parameterValue The value the parameter must be to be marked.
482
     * @return $this
483
     */
484
    public function setMarkingFilter($parameterName, $parameterValue)
485
    {
486
        $this->markingFilter = array(
487
            "parameter" => $parameterName,
488
            "value" => $parameterValue
489
        );
490
        return $this;
491
    }
492
493
    /**
494
     * Filter the marking to only those where the function returns true. The node in question will be passed to the
495
     * function.
496
     *
497
     * @param callable $callback Callback to filter
498
     * @return $this
499
     */
500
    public function setMarkingFilterFunction($callback)
501
    {
502
        $this->markingFilter = array(
503
            "func" => $callback,
504
        );
505
        return $this;
506
    }
507
508
    /**
509
     * Returns true if the marking filter matches on the given node.
510
     *
511
     * @param DataObject $node Node to check
512
     * @return bool
513
     */
514
    protected function markingFilterMatches(DataObject $node)
515
    {
516
        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...
517
            return true;
518
        }
519
520
        // Func callback filter
521
        if (isset($this->markingFilter['func'])) {
522
            $func = $this->markingFilter['func'];
523
            return call_user_func($func, $node);
524
        }
525
526
        // Check object property filter
527
        if (isset($this->markingFilter['parameter'])) {
528
            $parameterName = $this->markingFilter['parameter'];
529
            $value = $this->markingFilter['value'];
530
531
            if (is_array($value)) {
532
                return in_array($node->$parameterName, $value);
533
            } else {
534
                return $node->$parameterName == $value;
535
            }
536
        }
537
538
        throw new LogicException("Invalid marking filter");
539
    }
540
541
    /**
542
     * Mark all children of the given node that match the marking filter.
543
     *
544
     * @param DataObject $node Parent node
545
     * @return array List of children marked by this operation
546
     */
547
    protected function markChildren(DataObject $node)
548
    {
549
        $this->markExpanded($node);
550
551
        // If too many children leave closed
552
        if ($this->isNodeLimited($node)) {
553
            // Limited nodes are always expanded
554
            $this->markClosed($node);
555
            return [];
556
        }
557
558
        // Iterate children if not limited
559
        $children = $this->getChildren($node);
560
        if (!$children) {
0 ignored issues
show
introduced by
$children is of type SilverStripe\ORM\SS_List, thus it always evaluated to true.
Loading history...
561
            return [];
562
        }
563
564
        // Mark all children
565
        $markedChildren = [];
566
        foreach ($children as $child) {
567
            $markingMatches = $this->markingFilterMatches($child);
568
            if (!$markingMatches) {
569
                continue;
570
            }
571
            // Mark a child node as unexpanded if it has children and has not already been expanded
572
            if ($this->getNumChildren($child) > 0 && !$this->isExpanded($child)) {
573
                $this->markUnexpanded($child);
574
            } else {
575
                $this->markExpanded($child);
576
            }
577
578
            $markedChildren[] = $child;
579
        }
580
        return $markedChildren;
581
    }
582
583
    /**
584
     * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
585
     * marking of this DataObject.
586
     *
587
     * @param DataObject $node
588
     * @return string
589
     */
590
    protected function markingClasses($node)
591
    {
592
        $classes = [];
593
        if (!$this->isExpanded($node)) {
594
            $classes[] = 'unexpanded';
595
        }
596
597
        // Set jstree open state, or mark it as a leaf (closed) if there are no children
598
        if (!$this->getNumChildren($node)) {
599
            // No children
600
            $classes[] = "jstree-leaf closed";
601
        } elseif ($this->isTreeOpened($node)) {
602
            // Open with children
603
            $classes[] = "jstree-open";
604
        } else {
605
            // Closed with children
606
            $classes[] = "jstree-closed closed";
607
        }
608
        return implode(' ', $classes);
609
    }
610
611
    /**
612
     * Mark the children of the DataObject with the given ID.
613
     *
614
     * @param int  $id   ID of parent node
615
     * @param bool $open If this is true, mark the parent node as opened
616
     * @return bool
617
     */
618
    public function markById($id, $open = false)
619
    {
620
        if (isset($this->markedNodes[$id])) {
621
            $this->markChildren($this->markedNodes[$id]);
622
            if ($open) {
623
                $this->markOpened($this->markedNodes[$id]);
624
            }
625
            return true;
626
        } else {
627
            return false;
628
        }
629
    }
630
631
    /**
632
     * Expose the given object in the tree, by marking this page and all it ancestors.
633
     *
634
     * @param DataObject|Hierarchy $childObj
635
     * @return $this
636
     */
637
    public function markToExpose(DataObject $childObj)
638
    {
639
        if (!$childObj) {
0 ignored issues
show
introduced by
$childObj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
640
            return $this;
641
        }
642
        $stack = $childObj->getAncestors(true)->reverse();
0 ignored issues
show
Bug introduced by
The method getAncestors() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

642
        $stack = $childObj->/** @scrutinizer ignore-call */ getAncestors(true)->reverse();
Loading history...
643
        foreach ($stack as $stackItem) {
644
            $this->markById($stackItem->ID, true);
645
        }
646
        return $this;
647
    }
648
649
    /**
650
     * Return the IDs of all the marked nodes.
651
     *
652
     * @refactor called from CMSMain
653
     * @return array
654
     */
655
    public function markedNodeIDs()
656
    {
657
        return array_keys($this->markedNodes);
658
    }
659
660
    /**
661
     * Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
662
     * @var array
663
     */
664
    protected $expanded = [];
665
666
    /**
667
     * Cache of DataObjects' opened statuses: [ID] = bool
668
     * @var array
669
     */
670
    protected $treeOpened = [];
671
672
    /**
673
     * Reset marked nodes
674
     */
675
    public function clearMarks()
676
    {
677
        $this->markedNodes = [];
678
        $this->expanded = [];
679
        $this->treeOpened = [];
680
    }
681
682
    /**
683
     * Mark this DataObject as expanded.
684
     *
685
     * @param DataObject $node
686
     * @return $this
687
     */
688
    public function markExpanded(DataObject $node)
689
    {
690
        $id = $node->ID ?: 0;
691
        $this->markedNodes[$id] = $node;
692
        $this->expanded[$id] = true;
693
        return $this;
694
    }
695
696
    /**
697
     * Mark this DataObject as unexpanded.
698
     *
699
     * @param DataObject $node
700
     * @return $this
701
     */
702
    public function markUnexpanded(DataObject $node)
703
    {
704
        $id = $node->ID ?: 0;
705
        $this->markedNodes[$id] = $node;
706
        unset($this->expanded[$id]);
707
        return $this;
708
    }
709
710
    /**
711
     * Mark this DataObject's tree as opened.
712
     *
713
     * @param DataObject $node
714
     * @return $this
715
     */
716
    public function markOpened(DataObject $node)
717
    {
718
        $id = $node->ID ?: 0;
719
        $this->markedNodes[$id] = $node;
720
        $this->treeOpened[$id] = true;
721
        return $this;
722
    }
723
724
    /**
725
     * Mark this DataObject's tree as closed.
726
     *
727
     * @param DataObject $node
728
     * @return $this
729
     */
730
    public function markClosed(DataObject $node)
731
    {
732
        $id = $node->ID ?: 0;
733
        $this->markedNodes[$id] = $node;
734
        unset($this->treeOpened[$id]);
735
        return $this;
736
    }
737
738
    /**
739
     * Check if this DataObject is marked.
740
     *
741
     * @param DataObject $node
742
     * @return bool
743
     */
744
    public function isMarked(DataObject $node)
745
    {
746
        $id = $node->ID ?: 0;
747
        return !empty($this->markedNodes[$id]);
748
    }
749
750
    /**
751
     * Check if this DataObject is expanded.
752
     * An expanded object has had it's children iterated through.
753
     *
754
     * @param DataObject $node
755
     * @return bool
756
     */
757
    public function isExpanded(DataObject $node)
758
    {
759
        $id = $node->ID ?: 0;
760
        return !empty($this->expanded[$id]);
761
    }
762
763
    /**
764
     * Check if this DataObject's tree is opened.
765
     * This is an expanded node which also should have children visually shown.
766
     *
767
     * @param DataObject $node
768
     * @return bool
769
     */
770
    public function isTreeOpened(DataObject $node)
771
    {
772
        $id = $node->ID ?: 0;
773
        return !empty($this->treeOpened[$id]);
774
    }
775
776
    /**
777
     * Check if this node has too many children
778
     *
779
     * @param DataObject|Hierarchy $node
780
     * @param int $count Children count (if already calculated)
781
     * @return bool
782
     */
783
    protected function isNodeLimited(DataObject $node, $count = null)
784
    {
785
        // Singleton root node isn't limited
786
        if (!$node->ID) {
787
            return false;
788
        }
789
790
        // Check if limiting is enabled first
791
        if (!$this->getLimitingEnabled()) {
792
            return false;
793
        }
794
795
        // Count children for this node and compare to max
796
        if (!isset($count)) {
797
            $count = $this->getNumChildren($node);
798
        }
799
        return $count > $this->getMaxChildNodes();
800
    }
801
802
    /**
803
     * Toggle limiting on or off
804
     *
805
     * @param bool $enabled
806
     * @return $this
807
     */
808
    public function setLimitingEnabled($enabled)
809
    {
810
        $this->enableLimiting = $enabled;
811
        return $this;
812
    }
813
814
    /**
815
     * Check if limiting is enabled
816
     *
817
     * @return bool
818
     */
819
    public function getLimitingEnabled()
820
    {
821
        return $this->enableLimiting;
822
    }
823
}
824