Completed
Push — master ( 3f1f9d...ab54c8 )
by Ingo
08:52
created

MarkedSet   D

Complexity

Total Complexity 94

Size/Duplication

Total Lines 793
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 793
rs 4.4444
c 0
b 0
f 0
wmc 94
lcom 1
cbo 6

36 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 26 6
A getNodeCountThreshold() 0 5 2
A getMaxChildNodes() 0 5 2
A setMaxChildNodes() 0 5 1
A setNodeCountThreshold() 0 5 1
A getChildrenMethod() 0 4 2
A getChildren() 0 5 2
A setChildrenMethod() 0 13 2
A getNumChildrenMethod() 0 4 2
A getNumChildren() 0 5 1
A setNumChildrenMethod() 0 13 2
A renderChildren() 0 15 2
A getChildrenAsArray() 0 17 2
B renderSubtree() 0 29 5
B getSubtreeAsArray() 0 25 6
B getSubtree() 0 36 5
B markPartialTree() 0 25 5
A setMarkingFilter() 0 8 1
A setMarkingFilterFunction() 0 7 1
B markingFilterMatches() 0 26 5
C markChildren() 0 35 7
A markingClasses() 0 20 4
A markById() 0 12 3
A markToExpose() 0 11 3
A markedNodeIDs() 0 4 1
A clearMarks() 0 6 1
A markExpanded() 0 7 2
A markUnexpanded() 0 7 2
A markOpened() 0 7 2
A markClosed() 0 7 2
A isMarked() 0 5 2
A isExpanded() 0 5 2
A isTreeOpened() 0 5 2
A isNodeLimited() 0 18 4
A setLimitingEnabled() 0 5 1
A getLimitingEnabled() 0 4 1

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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\ArrayList;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\FieldType\DBField;
11
use SilverStripe\ORM\SS_List;
12
use SilverStripe\View\ArrayData;
13
14
/**
15
 * Contains a set of hierarchical objects generated from a marking compilation run.
16
 *
17
 * A set of nodes can be "marked" for later export, in order to prevent having to
18
 * export the entire contents of a potentially huge tree.
19
 */
20
class MarkedSet
21
{
22
    use Injectable;
23
24
    /**
25
     * Marked nodes for a given subtree. The first item in this list
26
     * is the root object of the subtree.
27
     *
28
     * A marked item is an item in a tree which will be included in
29
     * a resulting tree.
30
     *
31
     * @var array Map of [itemID => itemInstance]
32
     */
33
    protected $markedNodes;
34
35
    /**
36
     * Optional filter callback for filtering nodes to mark
37
     *
38
     * Array with keys:
39
     *  - parameter
40
     *  - value
41
     *  - func
42
     *
43
     * @var array
44
     * @temp made public
45
     */
46
    public $markingFilter;
47
48
    /**
49
     * @var DataObject
50
     */
51
    protected $rootNode = null;
52
53
    /**
54
     * Method to use for getting children. Defaults to 'AllChildrenIncludingDeleted'
55
     *
56
     * @var string
57
     */
58
    protected $childrenMethod = null;
59
60
    /**
61
     * Method to use for counting children. Defaults to `numChildren`
62
     *
63
     * @var string
64
     */
65
    protected $numChildrenMethod = null;
66
67
    /**
68
     * Minimum number of nodes to iterate over before stopping recursion
69
     *
70
     * @var int
71
     */
72
    protected $nodeCountThreshold = null;
73
74
    /**
75
     * Max number of nodes to return from a single children collection
76
     *
77
     * @var int
78
     */
79
    protected $maxChildNodes;
80
81
    /**
82
     * Enable limiting
83
     *
84
     * @var bool
85
     */
86
    protected $enableLimiting = true;
87
88
    /**
89
     * Create an empty set with the given class
90
     *
91
     * @param DataObject $rootNode Root node for this set. To collect the entire tree,
92
     * pass in a singelton object.
93
     * @param string $childrenMethod Override children method
94
     * @param string $numChildrenMethod Override children counting method
95
     * @param int $nodeCountThreshold Minimum threshold for number nodes to mark
96
     * @param int $maxChildNodes Maximum threshold for number of child nodes to include
97
     */
98
    public function __construct(
99
        DataObject $rootNode,
100
        $childrenMethod = null,
101
        $numChildrenMethod = null,
102
        $nodeCountThreshold = null,
103
        $maxChildNodes = null
104
    ) {
105
        if (! $rootNode::has_extension(Hierarchy::class)) {
106
            throw new InvalidArgumentException(
107
                get_class($rootNode) . " does not have the Hierarchy extension"
108
            );
109
        }
110
        $this->rootNode = $rootNode;
111
        if ($childrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $childrenMethod 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...
112
            $this->setChildrenMethod($childrenMethod);
113
        }
114
        if ($numChildrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $numChildrenMethod 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...
115
            $this->setNumChildrenMethod($numChildrenMethod);
116
        }
117
        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 zero. 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...
118
            $this->setNodeCountThreshold($nodeCountThreshold);
119
        }
120
        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 zero. 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...
121
            $this->setMaxChildNodes($maxChildNodes);
122
        }
123
    }
124
125
    /**
126
     * Get total number of nodes to get. This acts as a soft lower-bounds for
127
     * number of nodes to search until found.
128
     * Defaults to value of node_threshold_total of hierarchy class.
129
     *
130
     * @return int
131
     */
132
    public function getNodeCountThreshold()
133
    {
134
        return $this->nodeCountThreshold
135
            ?: $this->rootNode->config()->get('node_threshold_total');
136
    }
137
138
    /**
139
     * Max number of nodes that can be physically rendered at any level.
140
     * Acts as a hard upper bound, after which nodes will be trimmed for
141
     * performance reasons.
142
     *
143
     * @return int
144
     */
145
    public function getMaxChildNodes()
146
    {
147
        return $this->maxChildNodes
148
            ?: $this->rootNode->config()->get('node_threshold_leaf');
149
    }
150
151
    /**
152
     * Set hard limit of number of nodes to get for this level
153
     *
154
     * @param int $count
155
     * @return $this
156
     */
157
    public function setMaxChildNodes($count)
158
    {
159
        $this->maxChildNodes = $count;
160
        return $this;
161
    }
162
163
    /**
164
     * Set max node count
165
     *
166
     * @param int $total
167
     * @return $this
168
     */
169
    public function setNodeCountThreshold($total)
170
    {
171
        $this->nodeCountThreshold = $total;
172
        return $this;
173
    }
174
175
    /**
176
     * Get method to use for getting children
177
     *
178
     * @return string
179
     */
180
    public function getChildrenMethod()
181
    {
182
        return $this->childrenMethod ?: 'AllChildrenIncludingDeleted';
183
    }
184
185
    /**
186
     * Get children from this node
187
     *
188
     * @param DataObject $node
189
     * @return SS_List
190
     */
191
    protected function getChildren(DataObject $node)
192
    {
193
        $method = $this->getChildrenMethod();
194
        return $node->$method() ?: ArrayList::create();
195
    }
196
197
    /**
198
     * Set method to use for getting children
199
     *
200
     * @param string $method
201
     * @throws InvalidArgumentException
202
     * @return $this
203
     */
204
    public function setChildrenMethod($method)
205
    {
206
        // Check method is valid
207
        if (!$this->rootNode->hasMethod($method)) {
208
            throw new InvalidArgumentException(sprintf(
209
                "Can't find the method '%s' on class '%s' for getting tree children",
210
                $method,
211
                get_class($this->rootNode)
212
            ));
213
        }
214
        $this->childrenMethod = $method;
215
        return $this;
216
    }
217
218
    /**
219
     * Get method name for num children
220
     *
221
     * @return string
222
     */
223
    public function getNumChildrenMethod()
224
    {
225
        return $this->numChildrenMethod ?: 'numChildren';
226
    }
227
228
    /**
229
     * Count children
230
     *
231
     * @param DataObject $node
232
     * @return int
233
     */
234
    protected function getNumChildren(DataObject $node)
235
    {
236
        $method = $this->getNumChildrenMethod();
237
        return (int)$node->$method();
238
    }
239
240
    /**
241
     * Set method name to get num children
242
     *
243
     * @param string $method
244
     * @return $this
245
     */
246
    public function setNumChildrenMethod($method)
247
    {
248
        // Check method is valid
249
        if (!$this->rootNode->hasMethod($method)) {
250
            throw new InvalidArgumentException(sprintf(
251
                "Can't find the method '%s' on class '%s' for counting tree children",
252
                $method,
253
                get_class($this->rootNode)
254
            ));
255
        }
256
        $this->numChildrenMethod = $method;
257
        return $this;
258
    }
259
260
    /**
261
     * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
262
     * have children they will be displayed as a UL inside a LI.
263
     *
264
     * @param string $template Template for items in the list
265
     * @param array|callable $context Additional arguments to add to template when rendering
266
     * due to excessive line length. If callable, this will be executed with the current node dataobject
267
     * @return string
268
     */
269
    public function renderChildren(
270
        $template = null,
271
        $context = []
272
    ) {
273
        // Default to HTML template
274
        if (!$template) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $template 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...
275
            $template = [
276
                'type' => 'Includes',
277
                self::class . '_HTML'
278
            ];
279
        }
280
        $tree = $this->getSubtree($this->rootNode, 0);
281
        $node = $this->renderSubtree($tree, $template, $context);
0 ignored issues
show
Bug introduced by
It seems like $tree defined by $this->getSubtree($this->rootNode, 0) on line 280 can also be of type string; however, SilverStripe\ORM\Hierarc...kedSet::renderSubtree() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
282
        return (string)$node->getField('SubTree');
283
    }
284
285
    /**
286
     * Get child data formatted as JSON
287
     *
288
     * @param callable $serialiseEval A callback that takes a DataObject as a single parameter,
289
     * and should return an array containing a simple array representation. This result will
290
     * replace the 'node' property at each point in the tree.
291
     * @return array
292
     */
293
    public function getChildrenAsArray($serialiseEval = null)
294
    {
295
        if (!$serialiseEval) {
296
            $serialiseEval = function ($data) {
297
                /** @var DataObject $node */
298
                $node = $data['node'];
299
                return [
300
                    'id' => $node->ID,
301
                    'title' => $node->getTitle()
302
                ];
303
            };
304
        }
305
306
        $tree = $this->getSubtree($this->rootNode, 0);
307
308
        return $this->getSubtreeAsArray($tree, $serialiseEval);
0 ignored issues
show
Bug introduced by
It seems like $tree defined by $this->getSubtree($this->rootNode, 0) on line 306 can also be of type string; however, SilverStripe\ORM\Hierarc...et::getSubtreeAsArray() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
309
    }
310
311
    /**
312
     * Render a node in the tree with the given template
313
     *
314
     * @param array $data array data for current node
315
     * @param string|array $template Template to use
316
     * @param array|callable $context Additional arguments to add to template when rendering
317
     * due to excessive line length. If callable, this will be executed with the current node dataobject
318
     * @return ArrayData Viewable object representing the root node. use getField('SubTree') to get HTML
319
     */
320
    protected function renderSubtree($data, $template, $context = [])
321
    {
322
        // Render children
323
        $childNodes = new ArrayList();
324
        foreach ($data['children'] as $child) {
325
            $childData = $this->renderSubtree($child, $template, $context);
326
            $childNodes->push($childData);
327
        }
328
329
        // Build parent node
330
        $parentNode = new ArrayData($data);
331
        $parentNode->setField('children', $childNodes); // Replace raw array with template-friendly list
332
        $parentNode->setField('markingClasses', $this->markingClasses($data['node']));
333
334
        // Evaluate custom context
335
        if (is_callable($context)) {
336
            $context = call_user_func($context, $data['node']);
337
        }
338
        if ($context) {
339
            foreach ($context as $key => $value) {
340
                $parentNode->setField($key, $value);
341
            }
342
        }
343
344
        // Render
345
        $subtree = $parentNode->renderWith($template);
346
        $parentNode->setField('SubTree', $subtree);
347
        return $parentNode;
348
    }
349
350
    /**
351
     * Return sub-tree as json array
352
     *
353
     * @param array $data
354
     * @param callable $serialiseEval A callback that takes a DataObject as a single parameter,
355
     * and should return an array containing a simple array representation. This result will
356
     * replace the 'node' property at each point in the tree.
357
     * @return mixed|string
358
     */
359
    protected function getSubtreeAsArray($data, $serialiseEval)
360
    {
361
        $output = $data;
362
363
        // Serialise node
364
        $output['node'] = $serialiseEval($data['node']);
365
366
        // Force serialisation of DBField instances
367
        if (is_array($output['node'])) {
368
            foreach ($output['node'] as $key => $value) {
369
                if ($value instanceof DBField) {
370
                    $output['node'][$key] = $value->getSchemaValue();
371
                }
372
            }
373
        } elseif ($output['node'] instanceof DBField) {
374
            $output['node'] = $output['node']->getSchemaValue();
375
        }
376
377
        // Replace children with serialised elements
378
        $output['children'] = [];
379
        foreach ($data['children'] as $child) {
380
            $output['children'][] = $this->getSubtreeAsArray($child, $serialiseEval);
381
        }
382
        return $output;
383
    }
384
385
    /**
386
     * Get tree data for node
387
     *
388
     * @param DataObject $node
389
     * @param int $depth
390
     * @return array|string
391
     */
392
    protected function getSubtree($node, $depth = 0)
393
    {
394
        // Check if this node is limited due to child count
395
        $numChildren = $this->getNumChildren($node);
396
        $limited = $this->isNodeLimited($node, $numChildren);
397
398
        // Build root rode
399
        $expanded = $this->isExpanded($node);
400
        $opened = $this->isTreeOpened($node);
401
        $output = [
402
            'node' => $node,
403
            'marked' => $this->isMarked($node),
404
            'expanded' => $expanded,
405
            'opened' => $opened,
406
            'depth' => $depth,
407
            'count' => $numChildren, // Count of DB children
408
            'limited' => $limited, // Flag whether 'items' has been limited
409
            'children' => [], // Children to return in this request
410
        ];
411
412
        // Don't iterate children if past limit
413
        // or not expanded (requires subsequent request to get)
414
        if ($limited || !$expanded) {
415
            return $output;
416
        }
417
418
        // Get children
419
        $children = $this->getChildren($node);
420
        foreach ($children as $child) {
421
            // Recurse
422
            if ($this->isMarked($child)) {
423
                $output['children'][] = $this->getSubtree($child, $depth + 1);
424
            }
425
        }
426
        return $output;
427
    }
428
429
    /**
430
     * Mark a segment of the tree, by calling mark().
431
     *
432
     * The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
433
     * get a limited number of tree nodes to show in the CMS initially.
434
     *
435
     * This method returns the number of nodes marked.  After this method is called other methods can check
436
     * {@link isExpanded()} and {@link isMarked()} on individual nodes.
437
     *
438
     * @return $this
439
     */
440
    public function markPartialTree()
441
    {
442
        $nodeCountThreshold = $this->getNodeCountThreshold();
443
444
        // Add root node, not-expanded by default
445
        /** @var DataObject|Hierarchy $rootNode */
446
        $rootNode = $this->rootNode;
447
        $this->clearMarks();
448
        $this->markUnexpanded($rootNode);
0 ignored issues
show
Bug introduced by
It seems like $rootNode can also be of type object<SilverStripe\ORM\Hierarchy\Hierarchy>; however, SilverStripe\ORM\Hierarc...edSet::markUnexpanded() does only seem to accept object<SilverStripe\ORM\DataObject>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
449
450
        // Build markedNodes for this subtree until we reach the threshold
451
        // foreach can't handle an ever-growing $nodes list
452
        while (list(, $node) = each($this->markedNodes)) {
453
            $children = $this->markChildren($node);
454
            if ($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
455
                // Undo marking children as opened since they're lazy loaded
456
                /** @var DataObject|Hierarchy $child */
457
                foreach ($children as $child) {
458
                    $this->markClosed($child);
459
                }
460
                break;
461
            }
462
        }
463
        return $this;
464
    }
465
466
    /**
467
     * Filter the marking to only those object with $node->$parameterName == $parameterValue
468
     *
469
     * @param string $parameterName  The parameter on each node to check when marking.
470
     * @param mixed  $parameterValue The value the parameter must be to be marked.
471
     * @return $this
472
     */
473
    public function setMarkingFilter($parameterName, $parameterValue)
474
    {
475
        $this->markingFilter = array(
476
            "parameter" => $parameterName,
477
            "value" => $parameterValue
478
        );
479
        return $this;
480
    }
481
482
    /**
483
     * Filter the marking to only those where the function returns true. The node in question will be passed to the
484
     * function.
485
     *
486
     * @param callable $callback Callback to filter
487
     * @return $this
488
     */
489
    public function setMarkingFilterFunction($callback)
490
    {
491
        $this->markingFilter = array(
492
            "func" => $callback,
493
        );
494
        return $this;
495
    }
496
497
    /**
498
     * Returns true if the marking filter matches on the given node.
499
     *
500
     * @param DataObject $node Node to check
501
     * @return bool
502
     */
503
    protected function markingFilterMatches(DataObject $node)
504
    {
505
        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...
506
            return true;
507
        }
508
509
        // Func callback filter
510
        if (isset($this->markingFilter['func'])) {
511
            $func = $this->markingFilter['func'];
512
            return call_user_func($func, $node);
513
        }
514
515
        // Check object property filter
516
        if (isset($this->markingFilter['parameter'])) {
517
            $parameterName = $this->markingFilter['parameter'];
518
            $value = $this->markingFilter['value'];
519
520
            if (is_array($value)) {
521
                return in_array($node->$parameterName, $value);
522
            } else {
523
                return $node->$parameterName == $value;
524
            }
525
        }
526
527
        throw new LogicException("Invalid marking filter");
528
    }
529
530
    /**
531
     * Mark all children of the given node that match the marking filter.
532
     *
533
     * @param DataObject $node Parent node
534
     * @return array List of children marked by this operation
535
     */
536
    protected function markChildren(DataObject $node)
537
    {
538
        $this->markExpanded($node);
539
540
        // If too many children leave closed
541
        if ($this->isNodeLimited($node)) {
542
            // Limited nodes are always expanded
543
            $this->markClosed($node);
544
            return [];
545
        }
546
547
        // Iterate children if not limited
548
        $children = $this->getChildren($node);
549
        if (!$children) {
550
            return [];
551
        }
552
553
        // Mark all children
554
        $markedChildren = [];
555
        foreach ($children as $child) {
556
            $markingMatches = $this->markingFilterMatches($child);
557
            if (!$markingMatches) {
558
                continue;
559
            }
560
            // Mark a child node as unexpanded if it has children and has not already been expanded
561
            if ($this->getNumChildren($child) > 0 && !$this->isExpanded($child)) {
562
                $this->markUnexpanded($child);
563
            } else {
564
                $this->markExpanded($child);
565
            }
566
567
            $markedChildren[] = $child;
568
        }
569
        return $markedChildren;
570
    }
571
572
    /**
573
     * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
574
     * marking of this DataObject.
575
     *
576
     * @param DataObject $node
577
     * @return string
578
     */
579
    protected function markingClasses($node)
580
    {
581
        $classes = [];
582
        if (!$this->isExpanded($node)) {
583
            $classes[] = 'unexpanded';
584
        }
585
586
        // Set jstree open state, or mark it as a leaf (closed) if there are no children
587
        if (!$this->getNumChildren($node)) {
588
            // No children
589
            $classes[] = "jstree-leaf closed";
590
        } elseif ($this->isTreeOpened($node)) {
591
            // Open with children
592
            $classes[] = "jstree-open";
593
        } else {
594
            // Closed with children
595
            $classes[] = "jstree-closed closed";
596
        }
597
        return implode(' ', $classes);
598
    }
599
600
    /**
601
     * Mark the children of the DataObject with the given ID.
602
     *
603
     * @param int  $id   ID of parent node
604
     * @param bool $open If this is true, mark the parent node as opened
605
     * @return bool
606
     */
607
    public function markById($id, $open = false)
608
    {
609
        if (isset($this->markedNodes[$id])) {
610
            $this->markChildren($this->markedNodes[$id]);
611
            if ($open) {
612
                $this->markOpened($this->markedNodes[$id]);
613
            }
614
            return true;
615
        } else {
616
            return false;
617
        }
618
    }
619
620
    /**
621
     * Expose the given object in the tree, by marking this page and all it ancestors.
622
     *
623
     * @param DataObject|Hierarchy $childObj
624
     * @return $this
625
     */
626
    public function markToExpose(DataObject $childObj)
627
    {
628
        if (!$childObj) {
629
            return $this;
630
        }
631
        $stack = $childObj->getAncestors(true)->reverse();
632
        foreach ($stack as $stackItem) {
633
            $this->markById($stackItem->ID, true);
634
        }
635
        return $this;
636
    }
637
638
    /**
639
     * Return the IDs of all the marked nodes.
640
     *
641
     * @refactor called from CMSMain
642
     * @return array
643
     */
644
    public function markedNodeIDs()
645
    {
646
        return array_keys($this->markedNodes);
647
    }
648
649
    /**
650
     * Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
651
     * @var array
652
     */
653
    protected $expanded = [];
654
655
    /**
656
     * Cache of DataObjects' opened statuses: [ID] = bool
657
     * @var array
658
     */
659
    protected $treeOpened = [];
660
661
    /**
662
     * Reset marked nodes
663
     */
664
    public function clearMarks()
665
    {
666
        $this->markedNodes = [];
667
        $this->expanded = [];
668
        $this->treeOpened = [];
669
    }
670
671
    /**
672
     * Mark this DataObject as expanded.
673
     *
674
     * @param DataObject $node
675
     * @return $this
676
     */
677
    public function markExpanded(DataObject $node)
678
    {
679
        $id = $node->ID ?: 0;
680
        $this->markedNodes[$id] = $node;
681
        $this->expanded[$id] = true;
682
        return $this;
683
    }
684
685
    /**
686
     * Mark this DataObject as unexpanded.
687
     *
688
     * @param DataObject $node
689
     * @return $this
690
     */
691
    public function markUnexpanded(DataObject $node)
692
    {
693
        $id = $node->ID ?: 0;
694
        $this->markedNodes[$id] = $node;
695
        unset($this->expanded[$id]);
696
        return $this;
697
    }
698
699
    /**
700
     * Mark this DataObject's tree as opened.
701
     *
702
     * @param DataObject $node
703
     * @return $this
704
     */
705
    public function markOpened(DataObject $node)
706
    {
707
        $id = $node->ID ?: 0;
708
        $this->markedNodes[$id] = $node;
709
        $this->treeOpened[$id] = true;
710
        return $this;
711
    }
712
713
    /**
714
     * Mark this DataObject's tree as closed.
715
     *
716
     * @param DataObject $node
717
     * @return $this
718
     */
719
    public function markClosed(DataObject $node)
720
    {
721
        $id = $node->ID ?: 0;
722
        $this->markedNodes[$id] = $node;
723
        unset($this->treeOpened[$id]);
724
        return $this;
725
    }
726
727
    /**
728
     * Check if this DataObject is marked.
729
     *
730
     * @param DataObject $node
731
     * @return bool
732
     */
733
    public function isMarked(DataObject $node)
734
    {
735
        $id = $node->ID ?: 0;
736
        return !empty($this->markedNodes[$id]);
737
    }
738
739
    /**
740
     * Check if this DataObject is expanded.
741
     * An expanded object has had it's children iterated through.
742
     *
743
     * @param DataObject $node
744
     * @return bool
745
     */
746
    public function isExpanded(DataObject $node)
747
    {
748
        $id = $node->ID ?: 0;
749
        return !empty($this->expanded[$id]);
750
    }
751
752
    /**
753
     * Check if this DataObject's tree is opened.
754
     * This is an expanded node which also should have children visually shown.
755
     *
756
     * @param DataObject $node
757
     * @return bool
758
     */
759
    public function isTreeOpened(DataObject $node)
760
    {
761
        $id = $node->ID ?: 0;
762
        return !empty($this->treeOpened[$id]);
763
    }
764
765
    /**
766
     * Check if this node has too many children
767
     *
768
     * @param DataObject|Hierarchy $node
769
     * @param int $count Children count (if already calculated)
770
     * @return bool
771
     */
772
    protected function isNodeLimited(DataObject $node, $count = null)
773
    {
774
        // Singleton root node isn't limited
775
        if (!$node->ID) {
776
            return false;
777
        }
778
779
        // Check if limiting is enabled first
780
        if (!$this->getLimitingEnabled()) {
781
            return false;
782
        }
783
784
        // Count children for this node and compare to max
785
        if (!isset($count)) {
786
            $count = $this->getNumChildren($node);
787
        }
788
        return $count > $this->getMaxChildNodes();
789
    }
790
791
    /**
792
     * Toggle limiting on or off
793
     *
794
     * @param bool $enabled
795
     * @return $this
796
     */
797
    public function setLimitingEnabled($enabled)
798
    {
799
        $this->enableLimiting = $enabled;
800
        return $this;
801
    }
802
803
    /**
804
     * Check if limiting is enabled
805
     *
806
     * @return bool
807
     */
808
    public function getLimitingEnabled()
809
    {
810
        return $this->enableLimiting;
811
    }
812
}
813