Completed
Push — master ( 230d01...b82d58 )
by Damian
12:40
created

Hierarchy::markUnexpanded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
/**
3
 * DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents.
4
 * The most obvious example of this is SiteTree.
5
 * @package framework
6
 * @subpackage model
7
 */
8
class Hierarchy extends DataExtension {
9
10
	protected $markedNodes;
11
12
	protected $markingFilter;
13
14
	/**
15
	 * @var Int
16
	 */
17
	protected $_cache_numChildren;
18
19
	/**
20
	 * @config
21
	 * @var integer The lower bounds for the amount of nodes to mark. If set, the logic will expand
22
	 * nodes until it reaches at least this number, and then stops. Root nodes will always
23
	 * show regardless of this settting. Further nodes can be lazy-loaded via ajax.
24
	 * This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having
25
	 * 30 children, the actual node count will be 50 (all root nodes plus first expanded child).
26
	 */
27
	private static $node_threshold_total = 50;
28
29
	/**
30
	 * @config
31
	 * @var integer Limit on the maximum children a specific node can display.
32
	 * Serves as a hard limit to avoid exceeding available server resources
33
	 * in generating the tree, and browser resources in rendering it.
34
	 * Nodes with children exceeding this value typically won't display
35
	 * any children, although this is configurable through the $nodeCountCallback
36
	 * parameter in {@link getChildrenAsUL()}. "Root" nodes will always show
37
	 * all children, regardless of this setting.
38
	 */
39
	private static $node_threshold_leaf = 250;
40
41
	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...
42
		return array(
43
			'has_one' => array('Parent' => $class)
44
		);
45
	}
46
47
	/**
48
	 * Validate the owner object - check for existence of infinite loops.
49
	 */
50
	public function validate(ValidationResult $validationResult) {
51
		// The object is new, won't be looping.
52
		if (!$this->owner->ID) return;
53
		// The object has no parent, won't be looping.
54
		if (!$this->owner->ParentID) return;
55
		// The parent has not changed, skip the check for performance reasons.
56
		if (!$this->owner->isChanged('ParentID')) return;
57
58
		// Walk the hierarchy upwards until we reach the top, or until we reach the originating node again.
59
		$node = $this->owner;
60
		while($node) {
61
			if ($node->ParentID==$this->owner->ID) {
62
				// Hierarchy is looping.
63
				$validationResult->error(
64
					_t(
65
						'Hierarchy.InfiniteLoopNotAllowed',
66
						'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this',
67
						'First argument is the class that makes up the hierarchy.',
68
						array('type' => $this->owner->class)
69
					),
70
					'INFINITE_LOOP'
71
				);
72
				break;
73
			}
74
			$node = $node->ParentID ? $node->Parent() : null;
75
		}
76
77
		// At this point the $validationResult contains the response.
78
	}
79
80
	/**
81
	 * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child,
82
	 * so if they have children they will be displayed as a UL inside a LI.
83
	 * @param string $attributes Attributes to add to the UL.
84
	 * @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>'
85
	 * @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function.
86
	 * @param boolean $limitToMarked Display only marked children.
87
	 * @param string $childrenMethod The name of the method used to get children from each object
88
	 * @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You
89
	 *                          should not change this.
90
	 * @param int $nodeCountThreshold See {@link self::$node_threshold_total}
91
	 * @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity
92
	 *                 to intercept the query. Useful e.g. to avoid excessive children listings
93
	 *                 (Arguments: $parent, $numChildren)
94
	 *
95
	 * @return string
96
	 */
97
	public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $child->Title', $extraArg = null,
98
			$limitToMarked = false, $childrenMethod = "AllChildrenIncludingDeleted",
99
			$numChildrenMethod = "numChildren", $rootCall = true,
100
			$nodeCountThreshold = null, $nodeCountCallback = null) {
101
102
		if(!is_numeric($nodeCountThreshold)) {
103
			$nodeCountThreshold = Config::inst()->get('Hierarchy', 'node_threshold_total');
104
		}
105
106
		if($limitToMarked && $rootCall) {
107
			$this->markingFinished($numChildrenMethod);
108
		}
109
110
111
		if($nodeCountCallback) {
112
			$nodeCountWarning = $nodeCountCallback($this->owner, $this->owner->$numChildrenMethod());
113
			if($nodeCountWarning) return $nodeCountWarning;
114
		}
115
116
117
		if($this->owner->hasMethod($childrenMethod)) {
118
			$children = $this->owner->$childrenMethod($extraArg);
119
		} else {
120
			user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
121
				$childrenMethod, get_class($this->owner)), E_USER_ERROR);
122
		}
123
124
		if($children) {
125
126
			if($attributes) {
127
				$attributes = " $attributes";
128
			}
129
130
			$output = "<ul$attributes>\n";
131
132
			foreach($children as $child) {
0 ignored issues
show
Bug introduced by
The variable $children 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...
133
				if(!$limitToMarked || $child->isMarked()) {
134
					$foundAChild = true;
135
					if(is_callable($titleEval)) {
136
						$output .= $titleEval($child, $numChildrenMethod);
137
					} else {
138
						$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...
139
					}
140
					$output .= "\n";
141
142
					$numChildren = $child->$numChildrenMethod();
143
144
					if(
145
						// Always traverse into opened nodes (they might be exposed as parents of search results)
146
						$child->isExpanded()
147
						// Only traverse into children if we haven't reached the maximum node count already.
148
						// Otherwise, the remaining nodes are lazy loaded via ajax.
149
						&& $child->isMarked()
150
					) {
151
						// Additionally check if node count requirements are met
152
						$nodeCountWarning = $nodeCountCallback ? $nodeCountCallback($child, $numChildren) : null;
153
						if($nodeCountWarning) {
154
							$output .= $nodeCountWarning;
155
							$child->markClosed();
156
						} else {
157
							$output .= $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked,
158
								$childrenMethod,	$numChildrenMethod, false, $nodeCountThreshold);
159
						}
160
					} elseif($child->isTreeOpened()) {
161
						// Since we're not loading children, don't mark it as open either
162
						$child->markClosed();
163
					}
164
					$output .= "</li>\n";
165
				}
166
			}
167
168
			$output .= "</ul>\n";
169
		}
170
171
		if(isset($foundAChild) && $foundAChild) {
172
			return $output;
0 ignored issues
show
Bug introduced by
The variable $output 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...
173
		}
174
	}
175
176
	/**
177
	 * Mark a segment of the tree, by calling mark().
178
	 * The method performs a breadth-first traversal until the number of nodes is more than minCount.
179
	 * This is used to get a limited number of tree nodes to show in the CMS initially.
180
	 *
181
	 * This method returns the number of nodes marked.  After this method is called other methods
182
	 * can check isExpanded() and isMarked() on individual nodes.
183
	 *
184
	 * @param int $nodeCountThreshold See {@link getChildrenAsUL()}
185
	 * @return int The actual number of nodes marked.
186
	 */
187
	public function markPartialTree($nodeCountThreshold = 30, $context = null,
188
			$childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
189
190
		if(!is_numeric($nodeCountThreshold)) $nodeCountThreshold = 30;
191
192
		$this->markedNodes = array($this->owner->ID => $this->owner);
193
		$this->owner->markUnexpanded();
194
195
		// foreach can't handle an ever-growing $nodes list
196
		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...
197
			$children = $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
198
			if($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
199
				// Undo marking children as opened since they're lazy loaded
200
				if($children) foreach($children as $child) $child->markClosed();
201
				break;
202
			}
203
		}
204
		return sizeof($this->markedNodes);
205
	}
206
207
	/**
208
	 * Filter the marking to only those object with $node->$parameterName = $parameterValue
209
	 * @param string $parameterName The parameter on each node to check when marking.
210
	 * @param mixed $parameterValue The value the parameter must be to be marked.
211
	 */
212
	public function setMarkingFilter($parameterName, $parameterValue) {
213
		$this->markingFilter = array(
214
			"parameter" => $parameterName,
215
			"value" => $parameterValue
216
		);
217
	}
218
219
	/**
220
	 * Filter the marking to only those where the function returns true.
221
	 * The node in question will be passed to the function.
222
	 * @param string $funcName The function name.
223
	 */
224
	public function setMarkingFilterFunction($funcName) {
225
		$this->markingFilter = array(
226
			"func" => $funcName,
227
		);
228
	}
229
230
	/**
231
	 * Returns true if the marking filter matches on the given node.
232
	 * @param DataObject $node Node to check.
233
	 * @return boolean
234
	 */
235
	public function markingFilterMatches($node) {
236
		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...
237
			return true;
238
		}
239
240
		if(isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
241
			if(is_array($this->markingFilter['value'])){
242
				$ret = false;
243
				foreach($this->markingFilter['value'] as $value) {
244
					$ret = $ret||$node->$parameterName==$value;
245
					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...
246
						break;
247
					}
248
				}
249
				return $ret;
250
			} else {
251
				return ($node->$parameterName == $this->markingFilter['value']);
252
			}
253
		} else if ($func = $this->markingFilter['func']) {
254
			return call_user_func($func, $node);
255
		}
256
	}
257
258
	/**
259
	 * Mark all children of the given node that match the marking filter.
260
	 * @param DataObject $node Parent node.
261
	 * @return DataList
262
	 */
263
	public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted",
264
			$numChildrenMethod = "numChildren") {
265
		if($node->hasMethod($childrenMethod)) {
266
			$children = $node->$childrenMethod($context);
267
		} else {
268
			user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
269
				$childrenMethod, get_class($node)), E_USER_ERROR);
270
		}
271
272
		$node->markExpanded();
273
		if($children) {
274
			foreach($children as $child) {
0 ignored issues
show
Bug introduced by
The variable $children 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...
275
				$markingMatches = $this->markingFilterMatches($child);
276
				if($markingMatches) {
277
					if($child->$numChildrenMethod()) {
278
						$child->markUnexpanded();
279
					} else {
280
						$child->markExpanded();
281
					}
282
					$this->markedNodes[$child->ID] = $child;
283
				}
284
			}
285
		}
286
287
		return $children;
288
	}
289
290
	/**
291
	 * Ensure marked nodes that have children are also marked expanded.
292
	 * Call this after marking but before iterating over the tree.
293
	 */
294
	protected function markingFinished($numChildrenMethod = "numChildren") {
295
		// Mark childless nodes as expanded.
296
		if($this->markedNodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->markedNodes of type array<*,object> 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...
297
			foreach($this->markedNodes as $id => $node) {
298
				if(!$node->isExpanded() && !$node->$numChildrenMethod()) {
299
					$node->markExpanded();
300
				}
301
			}
302
		}
303
	}
304
305
	/**
306
	 * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a
307
	 * 'jstree-*' state depending on the marking of this DataObject.
308
	 *
309
	 * @return string
310
	 */
311
	public function markingClasses($numChildrenMethod="numChildren") {
312
		$classes = '';
313
		if(!$this->isExpanded()) {
314
			$classes .= " unexpanded";
315
		}
316
317
		// Set jstree open state, or mark it as a leaf (closed) if there are no children
318
		if(!$this->owner->$numChildrenMethod()) {
319
			$classes .= " jstree-leaf closed";
320
		} elseif($this->isTreeOpened()) {
321
			$classes .= " jstree-open";
322
		} else {
323
			$classes .= " jstree-closed closed";
324
		}
325
		return $classes;
326
	}
327
328
	/**
329
	 * Mark the children of the DataObject with the given ID.
330
	 * @param int $id ID of parent node.
331
	 * @param boolean $open If this is true, mark the parent node as opened.
332
	 */
333
	public function markById($id, $open = false) {
334
		if(isset($this->markedNodes[$id])) {
335
			$this->markChildren($this->markedNodes[$id]);
336
			if($open) {
337
				$this->markedNodes[$id]->markOpened();
338
			}
339
			return true;
340
		} else {
341
			return false;
342
		}
343
	}
344
345
	/**
346
	 * Expose the given object in the tree, by marking this page and all it ancestors.
347
	 * @param DataObject $childObj
348
	 */
349
	public function markToExpose($childObj) {
350
		if(is_object($childObj)){
351
			$stack = array_reverse($childObj->parentStack());
352
			foreach($stack as $stackItem) {
353
				$this->markById($stackItem->ID, true);
354
			}
355
		}
356
	}
357
358
	/**
359
	 * Return the IDs of all the marked nodes
360
	 */
361
	public function markedNodeIDs() {
362
		return array_keys($this->markedNodes);
363
	}
364
365
	/**
366
	 * Return an array of this page and its ancestors, ordered item -> root.
367
	 * @return array
368
	 */
369
	public function parentStack() {
370
		$p = $this->owner;
371
372
		while($p) {
373
			$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...
374
			$p = $p->ParentID ? $p->Parent() : null;
375
		}
376
377
		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...
378
	}
379
380
	/**
381
	 * True if this DataObject is marked.
382
	 * @var boolean
383
	 */
384
	protected static $marked = array();
385
386
	/**
387
	 * True if this DataObject is expanded.
388
	 * @var boolean
389
	 */
390
	protected static $expanded = array();
391
392
	/**
393
	 * True if this DataObject is opened.
394
	 * @var boolean
395
	 */
396
	protected static $treeOpened = array();
397
398
	/**
399
	 * Mark this DataObject as expanded.
400
	 */
401
	public function markExpanded() {
402
		self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
403
		self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
404
	}
405
406
	/**
407
	 * Mark this DataObject as unexpanded.
408
	 */
409
	public function markUnexpanded() {
410
		self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
411
		self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false;
412
	}
413
414
	/**
415
	 * Mark this DataObject's tree as opened.
416
	 */
417
	public function markOpened() {
418
		self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
419
		self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
420
	}
421
422
	/**
423
	 * Mark this DataObject's tree as closed.
424
	 */
425
	public function markClosed() {
426
		if(isset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID])) {
427
			unset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID]);
428
		}
429
	}
430
431
	/**
432
	 * Check if this DataObject is marked.
433
	 * @return boolean
434
	 */
435
	public function isMarked() {
436
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
437
		$id = $this->owner->ID;
438
		return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
439
	}
440
441
	/**
442
	 * Check if this DataObject is expanded.
443
	 * @return boolean
444
	 */
445
	public function isExpanded() {
446
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
447
		$id = $this->owner->ID;
448
		return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
449
	}
450
451
	/**
452
	 * Check if this DataObject's tree is opened.
453
	 */
454
	public function isTreeOpened() {
455
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
456
		$id = $this->owner->ID;
457
		return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
458
	}
459
460
	/**
461
	 * Get a list of this DataObject's and all it's descendants IDs.
462
	 * @return int
463
	 */
464
	public function getDescendantIDList() {
465
		$idList = array();
466
		$this->loadDescendantIDListInto($idList);
467
		return $idList;
468
	}
469
470
	/**
471
	 * Get a list of this DataObject's and all it's descendants ID, and put it in $idList.
472
	 * @var array $idList Array to put results in.
473
	 */
474
	public function loadDescendantIDListInto(&$idList) {
475
		if($children = $this->AllChildren()) {
476
			foreach($children as $child) {
477
				if(in_array($child->ID, $idList)) {
478
					continue;
479
				}
480
				$idList[] = $child->ID;
481
				$ext = $child->getExtensionInstance('Hierarchy');
482
				$ext->setOwner($child);
483
				$ext->loadDescendantIDListInto($idList);
484
				$ext->clearOwner();
485
			}
486
		}
487
	}
488
489
	/**
490
	 * Get the children for this DataObject.
491
	 * @return ArrayList
492
	 */
493
	public function Children() {
494
		if(!(isset($this->_cache_children) && $this->_cache_children)) {
495
			$result = $this->owner->stageChildren(false);
496
			$children = array();
497
			foreach ($result as $record) {
498
				if ($record->canView()) {
499
					$children[] = $record;
500
				}
501
			}
502
			$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...
503
		}
504
		return $this->_cache_children;
505
	}
506
507
	/**
508
	 * Return all children, including those 'not in menus'.
509
	 * @return SS_List
510
	 */
511
	public function AllChildren() {
512
		return $this->owner->stageChildren(true);
513
	}
514
515
	/**
516
	 * Return all children, including those that have been deleted but are still in live.
517
	 * Deleted children will be marked as "DeletedFromStage"
518
	 * Added children will be marked as "AddedToStage"
519
	 * Modified children will be marked as "ModifiedOnStage"
520
	 * Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
521
	 * @return SS_List
522
	 */
523
	public function AllChildrenIncludingDeleted($context = null) {
524
		return $this->doAllChildrenIncludingDeleted($context);
525
	}
526
527
	/**
528
	 * @see AllChildrenIncludingDeleted
529
	 *
530
	 * @param unknown_type $context
531
	 * @return SS_List
532
	 */
533
	public function doAllChildrenIncludingDeleted($context = null) {
534
		if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
535
536
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
537
		if($baseClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $baseClass of type null|string 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...
538
			$stageChildren = $this->owner->stageChildren(true);
539
540
			// Add live site content that doesn't exist on the stage site, if required.
541
			if($this->owner->hasExtension('Versioned')) {
542
				// Next, go through the live children.  Only some of these will be listed
543
				$liveChildren = $this->owner->liveChildren(true, true);
544
				if($liveChildren) {
545
					$merged = new ArrayList();
546
					$merged->merge($stageChildren);
547
					$merged->merge($liveChildren);
548
					$stageChildren = $merged;
549
				}
550
			}
551
552
			$this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
553
554
		} else {
555
			user_error("Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'",
556
				E_USER_ERROR);
557
		}
558
559
		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...
560
	}
561
562
	/**
563
	 * Return all the children that this page had, including pages that were deleted
564
	 * from both stage & live.
565
	 */
566
	public function AllHistoricalChildren() {
567
		if(!$this->owner->hasExtension('Versioned')) {
568
			throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
569
		}
570
571
		$baseClass=ClassInfo::baseDataClass($this->owner->class);
572
		return Versioned::get_including_deleted($baseClass,
573
			"\"ParentID\" = " . (int)$this->owner->ID, "\"$baseClass\".\"ID\" ASC");
574
	}
575
576
	/**
577
	 * Return the number of children that this page ever had, including pages that were deleted
578
	 */
579
	public function numHistoricalChildren() {
580
		if(!$this->owner->hasExtension('Versioned')) {
581
			throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
582
		}
583
584
		return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class),
585
			"\"ParentID\" = " . (int)$this->owner->ID)->count();
586
	}
587
588
	/**
589
	 * Return the number of direct children.
590
	 * By default, values are cached after the first invocation.
591
	 * Can be augumented by {@link augmentNumChildrenCountQuery()}.
592
	 *
593
	 * @param Boolean $cache
594
	 * @return int
595
	 */
596
	public function numChildren($cache = true) {
597
		// Build the cache for this class if it doesn't exist.
598
		if(!$cache || !is_numeric($this->_cache_numChildren)) {
599
			// Hey, this is efficient now!
600
			// We call stageChildren(), because Children() has canView() filtering
601
			$this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count();
602
		}
603
604
		// If theres no value in the cache, it just means that it doesn't have any children.
605
		return $this->_cache_numChildren;
606
	}
607
608
	/**
609
	 * Return children from the stage site
610
	 *
611
	 * @param showAll Inlcude all of the elements, even those not shown in the menus.
612
	 *   (only applicable when extension is applied to {@link SiteTree}).
613
	 * @return DataList
614
	 */
615
	public function stageChildren($showAll = false) {
616
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
617
		$staged = $baseClass::get()
618
			->filter('ParentID', (int)$this->owner->ID)
619
			->exclude('ID', (int)$this->owner->ID);
620
		if (!$showAll && $this->owner->db('ShowInMenus')) {
621
			$staged = $staged->filter('ShowInMenus', 1);
622
		}
623
		$this->owner->extend("augmentStageChildren", $staged, $showAll);
624
		return $staged;
625
	}
626
627
	/**
628
	 * Return children from the live site, if it exists.
629
	 *
630
	 * @param boolean $showAll Include all of the elements, even those not shown in the menus.
631
	 *   (only applicable when extension is applied to {@link SiteTree}).
632
	 * @param boolean $onlyDeletedFromStage Only return items that have been deleted from stage
633
	 * @return SS_List
634
	 */
635
	public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
636
		if(!$this->owner->hasExtension('Versioned')) {
637
			throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
638
		}
639
640
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
641
		$children = $baseClass::get()
642
			->filter('ParentID', (int)$this->owner->ID)
643
			->exclude('ID', (int)$this->owner->ID)
644
			->setDataQueryParam(array(
645
				'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage',
646
				'Versioned.stage' => 'Live'
647
			));
648
649
		if(!$showAll) $children = $children->filter('ShowInMenus', 1);
650
651
		return $children;
652
	}
653
654
	/**
655
	 * Get the parent of this class.
656
	 * @return DataObject
657
	 */
658
	public function getParent($filter = null) {
659
		if($p = $this->owner->__get("ParentID")) {
660
			$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
661
			$baseClass = array_shift($tableClasses);
662
			return DataObject::get_one($this->owner->class, array(
663
				array("\"$baseClass\".\"ID\"" => $p),
664
				$filter
665
			));
666
		}
667
	}
668
669
	/**
670
	 * Return all the parents of this class in a set ordered from the lowest to highest parent.
671
	 *
672
	 * @return SS_List
673
	 */
674
	public function getAncestors() {
675
		$ancestors = new ArrayList();
676
		$object    = $this->owner;
677
678
		while($object = $object->getParent()) {
679
			$ancestors->push($object);
680
		}
681
682
		return $ancestors;
683
	}
684
685
	/**
686
	 * Returns a human-readable, flattened representation of the path to the object,
687
	 * using its {@link Title()} attribute.
688
	 *
689
	 * @param String
690
	 * @return String
691
	 */
692
	public function getBreadcrumbs($separator = ' &raquo; ') {
693
		$crumbs = array();
694
		$ancestors = array_reverse($this->owner->getAncestors()->toArray());
695
		foreach($ancestors as $ancestor) $crumbs[] = $ancestor->Title;
696
		$crumbs[] = $this->owner->Title;
697
		return implode($separator, $crumbs);
698
	}
699
700
	/**
701
	 * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
702
	 * then search the parents.
703
	 *
704
	 * @todo Write!
705
	 */
706
	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...
707
		return null;
708
	}
709
710
	/**
711
	 * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
712
	 * then search the parents.
713
	 * @param string $className Class name of the node to find.
714
	 * @param string|int $root ID/ClassName of the node to limit the search to
715
	 * @param DataObject afterNode Used for recursive calls to this function
716
	 * @return DataObject
717
	 */
718
	public function naturalNext( $className = null, $root = 0, $afterNode = null ) {
719
		// If this node is not the node we are searching from, then we can possibly return this
720
		// node as a solution
721
		if($afterNode && $afterNode->ID != $this->owner->ID) {
722
			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...
723
				return $this->owner;
724
			}
725
		}
726
727
		$nextNode = null;
728
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
729
730
		$children = $baseClass::get()
731
			->filter('ParentID', (int)$this->owner->ID)
732
			->sort('"Sort"', 'ASC');
733
		if ($afterNode) {
734
			$children = $children->filter('Sort:GreaterThan', $afterNode->Sort);
735
		}
736
737
		// Try all the siblings of this node after the given node
738
		/*if( $siblings = DataObject::get( ClassInfo::baseDataClass($this->owner->class),
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% 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...
739
		"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
740
		> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/
741
742
		if($children) {
743
			foreach($children as $node) {
744
				if($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
745
					break;
746
				}
747
			}
748
749
			if($nextNode) {
750
				return $nextNode;
751
			}
752
		}
753
754
		// if this is not an instance of the root class or has the root id, search the parent
755
		if(!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class)
756
				&& ($parent = $this->owner->Parent())) {
757
758
			return $parent->naturalNext( $className, $root, $this->owner );
759
		}
760
761
		return null;
762
	}
763
764
	public function flushCache() {
765
		$this->_cache_children = null;
766
		$this->_cache_numChildren = null;
767
		self::$marked = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $marked.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
768
		self::$expanded = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $expanded.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
769
		self::$treeOpened = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $treeOpened.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
770
	}
771
772
	public static function reset() {
773
		self::$marked = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $marked.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
774
		self::$expanded = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $expanded.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
775
		self::$treeOpened = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $treeOpened.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
776
	}
777
778
}
779