Completed
Push — 3.4 ( 609b0a...73a839 )
by Daniel
22:37 queued 13:07
created

Hierarchy::getChildrenAsUL()   D

Complexity

Conditions 21
Paths 100

Size

Total Lines 82
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 48
nc 100
nop 9
dl 0
loc 82
rs 4.9116
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents. The most
4
 * obvious example of this is SiteTree.
5
 *
6
 * @package framework
7
 * @subpackage model
8
 *
9
 * @property int        ParentID
10
 * @property DataObject owner
11
 * @method   DataObject Parent
12
 */
13
class Hierarchy extends DataExtension {
14
15
	protected $markedNodes;
16
17
	protected $markingFilter;
18
19
	/** @var int */
20
	protected $_cache_numChildren;
21
22
	/**
23
	 * The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least
24
	 * this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be
25
	 * lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30
26
	 * children, the actual node count will be 50 (all root nodes plus first expanded child).
27
	 *
28
	 * @config
29
	 * @var int
30
	 */
31
	private static $node_threshold_total = 50;
32
33
	/**
34
	 * Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available
35
	 * server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding
36
	 * this value typically won't display any children, although this is configurable through the $nodeCountCallback
37
	 * parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting.
38
	 *
39
	 * @config
40
	 * @var int
41
	 */
42
	private static $node_threshold_leaf = 250;
43
44
	public static function get_extra_config($class, $extension, $args) {
45
		return array(
46
			'has_one' => array('Parent' => $class)
47
		);
48
	}
49
50
	/**
51
	 * Validate the owner object - check for existence of infinite loops.
52
	 *
53
	 * @param ValidationResult $validationResult
54
	 */
55
	public function validate(ValidationResult $validationResult) {
56
		// The object is new, won't be looping.
57
		if (!$this->owner->ID) return;
58
		// The object has no parent, won't be looping.
59
		if (!$this->owner->ParentID) return;
60
		// The parent has not changed, skip the check for performance reasons.
61
		if (!$this->owner->isChanged('ParentID')) return;
62
63
		// Walk the hierarchy upwards until we reach the top, or until we reach the originating node again.
64
		$node = $this->owner;
65
		while($node) {
66
			if ($node->ParentID==$this->owner->ID) {
67
				// Hierarchy is looping.
68
				$validationResult->error(
69
					_t(
70
						'Hierarchy.InfiniteLoopNotAllowed',
71
						'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this',
72
						'First argument is the class that makes up the hierarchy.',
73
						array('type' => $this->owner->class)
74
					),
75
					'INFINITE_LOOP'
76
				);
77
				break;
78
			}
79
			$node = $node->ParentID ? $node->Parent() : null;
80
		}
81
82
		// At this point the $validationResult contains the response.
83
	}
84
85
	/**
86
	 * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
87
	 * have children they will be displayed as a UL inside a LI.
88
	 *
89
	 * @param string          $attributes         Attributes to add to the UL
90
	 * @param string|callable $titleEval          PHP code to evaluate to start each child - this should include '<li>'
91
	 * @param string          $extraArg           Extra arguments that will be passed on to children, for if they
92
	 *                                            overload this function
93
	 * @param bool            $limitToMarked      Display only marked children
94
	 * @param string          $childrenMethod     The name of the method used to get children from each object
95
	 * @param bool            $rootCall           Set to true for this first call, and then to false for calls inside
96
	 *                                            the recursion. You should not change this.
97
	 * @param int             $nodeCountThreshold See {@link self::$node_threshold_total}
98
	 * @param callable        $nodeCountCallback  Called with the node count, which gives the callback an opportunity to
99
	 *                                            intercept the query. Useful e.g. to avoid excessive children listings
100
	 *                                            (Arguments: $parent, $numChildren)
101
	 *
102
	 * @return string
103
	 */
104
	public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $child->Title . "</li>"', $extraArg = null,
105
			$limitToMarked = false, $childrenMethod = "AllChildrenIncludingDeleted",
106
			$numChildrenMethod = "numChildren", $rootCall = true,
107
			$nodeCountThreshold = null, $nodeCountCallback = null) {
108
109
		if(!is_numeric($nodeCountThreshold)) {
110
			$nodeCountThreshold = Config::inst()->get('Hierarchy', 'node_threshold_total');
111
		}
112
113
		if($limitToMarked && $rootCall) {
114
			$this->markingFinished($numChildrenMethod);
115
		}
116
117
118
		if($nodeCountCallback) {
119
			$nodeCountWarning = $nodeCountCallback($this->owner, $this->owner->$numChildrenMethod());
120
			if($nodeCountWarning) return $nodeCountWarning;
121
		}
122
123
124
		if($this->owner->hasMethod($childrenMethod)) {
125
			$children = $this->owner->$childrenMethod($extraArg);
126
		} else {
127
			user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
128
				$childrenMethod, get_class($this->owner)), E_USER_ERROR);
129
		}
130
131
		if($children) {
132
133
			if($attributes) {
134
				$attributes = " $attributes";
135
			}
136
137
			$output = "<ul$attributes>\n";
138
139
			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...
140
				if(!$limitToMarked || $child->isMarked()) {
141
					$foundAChild = true;
142
					if(is_callable($titleEval)) {
143
						$output .= $titleEval($child, $numChildrenMethod);
144
					} else {
145
						$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...
146
					}
147
					$output = trim($output);
148
					if (substr($output, -5) == '</li>') {
149
						$output = trim(substr($output, 0, -5));
150
					}
151
					$output .= "\n";
152
153
					$numChildren = $child->$numChildrenMethod();
154
155
					if(
156
						// Always traverse into opened nodes (they might be exposed as parents of search results)
157
						$child->isExpanded()
158
						// Only traverse into children if we haven't reached the maximum node count already.
159
						// Otherwise, the remaining nodes are lazy loaded via ajax.
160
						&& $child->isMarked()
161
					) {
162
						// Additionally check if node count requirements are met
163
						$nodeCountWarning = $nodeCountCallback ? $nodeCountCallback($child, $numChildren) : null;
164
						if($nodeCountWarning) {
165
							$output .= $nodeCountWarning;
166
							$child->markClosed();
167
						} else {
168
							$output .= $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked,
169
								$childrenMethod,	$numChildrenMethod, false, $nodeCountThreshold);
170
						}
171
					} elseif($child->isTreeOpened()) {
172
						// Since we're not loading children, don't mark it as open either
173
						$child->markClosed();
174
					}
175
					$output .= "</li>\n";
176
				}
177
			}
178
179
			$output .= "</ul>\n";
180
		}
181
182
		if(isset($foundAChild) && $foundAChild) {
183
			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...
184
		}
185
	}
186
187
	/**
188
	 * Mark a segment of the tree, by calling mark().
189
	 *
190
	 * The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
191
	 * get a limited number of tree nodes to show in the CMS initially.
192
	 *
193
	 * This method returns the number of nodes marked.  After this method is called other methods can check
194
	 * {@link isExpanded()} and {@link isMarked()} on individual nodes.
195
	 *
196
	 * @param int $nodeCountThreshold See {@link getChildrenAsUL()}
197
	 * @return int The actual number of nodes marked.
198
	 */
199
	public function markPartialTree($nodeCountThreshold = 30, $context = null,
200
			$childrenMethod = "AllChildrenIncludingDeleted", $numChildrenMethod = "numChildren") {
201
202
		if(!is_numeric($nodeCountThreshold)) $nodeCountThreshold = 30;
203
204
		$this->markedNodes = array($this->owner->ID => $this->owner);
205
		$this->owner->markUnexpanded();
206
207
		// foreach can't handle an ever-growing $nodes list
208
		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...
209
			$children = $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
210
			if($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
211
				// Undo marking children as opened since they're lazy loaded
212
				if($children) foreach($children as $child) $child->markClosed();
213
				break;
214
			}
215
		}
216
		return sizeof($this->markedNodes);
217
	}
218
219
	/**
220
	 * Filter the marking to only those object with $node->$parameterName == $parameterValue
221
	 *
222
	 * @param string $parameterName  The parameter on each node to check when marking.
223
	 * @param mixed  $parameterValue The value the parameter must be to be marked.
224
	 */
225
	public function setMarkingFilter($parameterName, $parameterValue) {
226
		$this->markingFilter = array(
227
			"parameter" => $parameterName,
228
			"value" => $parameterValue
229
		);
230
	}
231
232
	/**
233
	 * Filter the marking to only those where the function returns true. The node in question will be passed to the
234
	 * function.
235
	 *
236
	 * @param string $funcName The name of the function to call
237
	 */
238
	public function setMarkingFilterFunction($funcName) {
239
		$this->markingFilter = array(
240
			"func" => $funcName,
241
		);
242
	}
243
244
	/**
245
	 * Returns true if the marking filter matches on the given node.
246
	 *
247
	 * @param DataObject $node Node to check
248
	 * @return bool
249
	 */
250
	public function markingFilterMatches($node) {
251
		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...
252
			return true;
253
		}
254
255
		if(isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
256
			if(is_array($this->markingFilter['value'])){
257
				$ret = false;
258
				foreach($this->markingFilter['value'] as $value) {
259
					$ret = $ret||$node->$parameterName==$value;
260
					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...
261
						break;
262
					}
263
				}
264
				return $ret;
265
			} else {
266
				return ($node->$parameterName == $this->markingFilter['value']);
267
			}
268
		} else if ($func = $this->markingFilter['func']) {
269
			return call_user_func($func, $node);
270
		}
271
	}
272
273
	/**
274
	 * Mark all children of the given node that match the marking filter.
275
	 *
276
	 * @param DataObject $node              Parent node
277
	 * @param mixed      $context
278
	 * @param string     $childrenMethod    The name of the instance method to call to get the object's list of children
279
	 * @param string     $numChildrenMethod The name of the instance method to call to count the object's children
280
	 * @return DataList
281
	 */
282
	public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted",
283
			$numChildrenMethod = "numChildren") {
284
		if($node->hasMethod($childrenMethod)) {
285
			$children = $node->$childrenMethod($context);
286
		} else {
287
			user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
288
				$childrenMethod, get_class($node)), E_USER_ERROR);
289
		}
290
291
		$node->markExpanded();
292
		if($children) {
293
			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...
294
				$markingMatches = $this->markingFilterMatches($child);
295
				if($markingMatches) {
296
					// Mark a child node as unexpanded if it has children and has not already been expanded
297
					if($child->$numChildrenMethod() && !$child->isExpanded()) {
298
						$child->markUnexpanded();
299
					} else {
300
						$child->markExpanded();
301
					}
302
					$this->markedNodes[$child->ID] = $child;
303
				}
304
			}
305
		}
306
307
		return $children;
308
	}
309
310
	/**
311
	 * Ensure marked nodes that have children are also marked expanded. Call this after marking but before iterating
312
	 * over the tree.
313
	 *
314
	 * @param string $numChildrenMethod The name of the instance method to call to count the object's children
315
	 */
316
	protected function markingFinished($numChildrenMethod = "numChildren") {
317
		// Mark childless nodes as expanded.
318
		if($this->markedNodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->markedNodes of type DataObject[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
319
			foreach($this->markedNodes as $id => $node) {
320
				if(!$node->isExpanded() && !$node->$numChildrenMethod()) {
321
					$node->markExpanded();
322
				}
323
			}
324
		}
325
	}
326
327
	/**
328
	 * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
329
	 * marking of this DataObject.
330
	 *
331
	 * @param string $numChildrenMethod The name of the instance method to call to count the object's children
332
	 * @return string
333
	 */
334
	public function markingClasses($numChildrenMethod="numChildren") {
335
		$classes = '';
336
		if(!$this->isExpanded()) {
337
			$classes .= " unexpanded";
338
		}
339
340
		// Set jstree open state, or mark it as a leaf (closed) if there are no children
341
		if(!$this->owner->$numChildrenMethod()) {
342
			$classes .= " jstree-leaf closed";
343
		} elseif($this->isTreeOpened()) {
344
			$classes .= " jstree-open";
345
		} else {
346
			$classes .= " jstree-closed closed";
347
		}
348
		return $classes;
349
	}
350
351
	/**
352
	 * Mark the children of the DataObject with the given ID.
353
	 *
354
	 * @param int  $id   ID of parent node
355
	 * @param bool $open If this is true, mark the parent node as opened
356
	 * @return bool
357
	 */
358
	public function markById($id, $open = false) {
359
		if(isset($this->markedNodes[$id])) {
360
			$this->markChildren($this->markedNodes[$id]);
361
			if($open) {
362
				$this->markedNodes[$id]->markOpened();
363
			}
364
			return true;
365
		} else {
366
			return false;
367
		}
368
	}
369
370
	/**
371
	 * Expose the given object in the tree, by marking this page and all it ancestors.
372
	 *
373
	 * @param DataObject $childObj
374
	 */
375
	public function markToExpose($childObj) {
376
		if(is_object($childObj)){
377
			$stack = array_reverse($childObj->parentStack());
378
			foreach($stack as $stackItem) {
379
				$this->markById($stackItem->ID, true);
380
			}
381
		}
382
	}
383
384
	/**
385
	 * Return the IDs of all the marked nodes.
386
	 *
387
	 * @return array
388
	 */
389
	public function markedNodeIDs() {
390
		return array_keys($this->markedNodes);
391
	}
392
393
	/**
394
	 * Return an array of this page and its ancestors, ordered item -> root.
395
	 *
396
	 * @return SiteTree[]
397
	 */
398
	public function parentStack() {
399
		$p = $this->owner;
400
401
		while($p) {
402
			$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...
403
			$p = $p->ParentID ? $p->Parent() : null;
404
		}
405
406
		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...
407
	}
408
409
	/**
410
	 * Cache of DataObjects' marked statuses: [ClassName][ID] = bool
411
	 * @var array
412
	 */
413
	protected static $marked = array();
414
415
	/**
416
	 * Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
417
	 * @var array
418
	 */
419
	protected static $expanded = array();
420
421
	/**
422
	 * Cache of DataObjects' opened statuses: [ClassName][ID] = bool
423
	 * @var array
424
	 */
425
	protected static $treeOpened = array();
426
427
	/**
428
	 * Mark this DataObject as expanded.
429
	 */
430
	public function markExpanded() {
431
		self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
432
		self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
433
	}
434
435
	/**
436
	 * Mark this DataObject as unexpanded.
437
	 */
438
	public function markUnexpanded() {
439
		self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
440
		self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false;
441
	}
442
443
	/**
444
	 * Mark this DataObject's tree as opened.
445
	 */
446
	public function markOpened() {
447
		self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
448
		self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
449
	}
450
451
	/**
452
	 * Mark this DataObject's tree as closed.
453
	 */
454
	public function markClosed() {
455
		if(isset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID])) {
456
			unset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID]);
457
		}
458
	}
459
460
	/**
461
	 * Check if this DataObject is marked.
462
	 *
463
	 * @return bool
464
	 */
465
	public function isMarked() {
466
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
467
		$id = $this->owner->ID;
468
		return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
469
	}
470
471
	/**
472
	 * Check if this DataObject is expanded.
473
	 *
474
	 * @return bool
475
	 */
476
	public function isExpanded() {
477
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
478
		$id = $this->owner->ID;
479
		return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
480
	}
481
482
	/**
483
	 * Check if this DataObject's tree is opened.
484
	 *
485
	 * @return bool
486
	 */
487
	public function isTreeOpened() {
488
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
489
		$id = $this->owner->ID;
490
		return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
491
	}
492
493
	/**
494
	 * Get a list of this DataObject's and all it's descendants IDs.
495
	 *
496
	 * @return int[]
497
	 */
498
	public function getDescendantIDList() {
499
		$idList = array();
500
		$this->loadDescendantIDListInto($idList);
501
		return $idList;
502
	}
503
504
	/**
505
	 * Get a list of this DataObject's and all it's descendants ID, and put them in $idList.
506
	 *
507
	 * @param array $idList Array to put results in.
508
	 */
509
	public function loadDescendantIDListInto(&$idList) {
510
		if($children = $this->AllChildren()) {
511
			foreach($children as $child) {
512
				if(in_array($child->ID, $idList)) {
513
					continue;
514
				}
515
				$idList[] = $child->ID;
516
				$ext = $child->getExtensionInstance('Hierarchy');
517
				$ext->setOwner($child);
518
				$ext->loadDescendantIDListInto($idList);
519
				$ext->clearOwner();
520
			}
521
		}
522
	}
523
524
	/**
525
	 * Get the children for this DataObject.
526
	 *
527
	 * @return DataList
528
	 */
529
	public function Children() {
530
		if(!(isset($this->_cache_children) && $this->_cache_children)) {
531
			$result = $this->owner->stageChildren(false);
532
			$children = array();
533
			foreach ($result as $record) {
534
				if ($record->canView()) {
535
					$children[] = $record;
536
				}
537
			}
538
			$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...
539
		}
540
		return $this->_cache_children;
541
	}
542
543
	/**
544
	 * Return all children, including those 'not in menus'.
545
	 *
546
	 * @return DataList
547
	 */
548
	public function AllChildren() {
549
		return $this->owner->stageChildren(true);
550
	}
551
552
	/**
553
	 * Return all children, including those that have been deleted but are still in live.
554
	 * - Deleted children will be marked as "DeletedFromStage"
555
	 * - Added children will be marked as "AddedToStage"
556
	 * - Modified children will be marked as "ModifiedOnStage"
557
	 * - Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
558
	 *
559
	 * @param mixed $context
560
	 * @return ArrayList
561
	 */
562
	public function AllChildrenIncludingDeleted($context = null) {
563
		return $this->doAllChildrenIncludingDeleted($context);
564
	}
565
566
	/**
567
	 * @see AllChildrenIncludingDeleted
568
	 *
569
	 * @param mixed $context
570
	 * @return ArrayList
571
	 */
572
	public function doAllChildrenIncludingDeleted($context = null) {
573
		if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
574
575
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
576
		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...
577
			$stageChildren = $this->owner->stageChildren(true);
578
579
			// Add live site content that doesn't exist on the stage site, if required.
580
			if($this->owner->hasExtension('Versioned')) {
581
				// Next, go through the live children.  Only some of these will be listed
582
				$liveChildren = $this->owner->liveChildren(true, true);
583
				if($liveChildren) {
584
					$merged = new ArrayList();
585
					$merged->merge($stageChildren);
586
					$merged->merge($liveChildren);
587
					$stageChildren = $merged;
588
				}
589
			}
590
591
			$this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
592
593
		} else {
594
			user_error("Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'",
595
				E_USER_ERROR);
596
		}
597
598
		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...
599
	}
600
601
	/**
602
	 * Return all the children that this page had, including pages that were deleted from both stage & live.
603
	 *
604
	 * @return DataList
605
	 * @throws Exception
606
	 */
607
	public function AllHistoricalChildren() {
608
		if(!$this->owner->hasExtension('Versioned')) {
609
			throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
610
		}
611
612
		$baseClass=ClassInfo::baseDataClass($this->owner->class);
613
		return Versioned::get_including_deleted($baseClass,
614
			"\"ParentID\" = " . (int)$this->owner->ID, "\"$baseClass\".\"ID\" ASC");
615
	}
616
617
	/**
618
	 * Return the number of children that this page ever had, including pages that were deleted.
619
	 *
620
	 * @return int
621
	 * @throws Exception
622
	 */
623
	public function numHistoricalChildren() {
624
		if(!$this->owner->hasExtension('Versioned')) {
625
			throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
626
		}
627
628
		return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class),
629
			"\"ParentID\" = " . (int)$this->owner->ID)->count();
630
	}
631
632
	/**
633
	 * Return the number of direct children. By default, values are cached after the first invocation. Can be
634
	 * augumented by {@link augmentNumChildrenCountQuery()}.
635
	 *
636
	 * @param bool $cache Whether to retrieve values from cache
637
	 * @return int
638
	 */
639
	public function numChildren($cache = true) {
640
		// Build the cache for this class if it doesn't exist.
641
		if(!$cache || !is_numeric($this->_cache_numChildren)) {
642
			// Hey, this is efficient now!
643
			// We call stageChildren(), because Children() has canView() filtering
644
			$this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count();
645
		}
646
647
		// If theres no value in the cache, it just means that it doesn't have any children.
648
		return $this->_cache_numChildren;
649
	}
650
651
	/**
652
	 * Return children in the stage site.
653
	 *
654
	 * @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when
655
	 *                      extension is applied to {@link SiteTree}.
656
	 * @return DataList
657
	 */
658
	public function stageChildren($showAll = false) {
659
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
660
		$staged = $baseClass::get()
661
			->filter('ParentID', (int)$this->owner->ID)
662
			->exclude('ID', (int)$this->owner->ID);
663
		if (!$showAll && $this->owner->db('ShowInMenus')) {
664
			$staged = $staged->filter('ShowInMenus', 1);
665
		}
666
		$this->owner->extend("augmentStageChildren", $staged, $showAll);
667
		return $staged;
668
	}
669
670
	/**
671
	 * Return children in the live site, if it exists.
672
	 *
673
	 * @param bool $showAll              Include all of the elements, even those not shown in the menus. Only
674
	 *                                   applicable when extension is applied to {@link SiteTree}.
675
	 * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage
676
	 * @return DataList
677
	 * @throws Exception
678
	 */
679
	public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
680
		if(!$this->owner->hasExtension('Versioned')) {
681
			throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
682
		}
683
684
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
685
		$children = $baseClass::get()
686
			->filter('ParentID', (int)$this->owner->ID)
687
			->exclude('ID', (int)$this->owner->ID)
688
			->setDataQueryParam(array(
689
				'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage',
690
				'Versioned.stage' => 'Live'
691
			));
692
693
		if(!$showAll) $children = $children->filter('ShowInMenus', 1);
694
695
		return $children;
696
	}
697
698
	/**
699
	 * Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing
700
	 * is returned.
701
	 *
702
	 * @param string $filter
703
	 * @return DataObject
704
	 */
705
	public function getParent($filter = null) {
706
		if($p = $this->owner->__get("ParentID")) {
707
			$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
708
			$baseClass = array_shift($tableClasses);
709
			return DataObject::get_one($this->owner->class, array(
710
				array("\"$baseClass\".\"ID\"" => $p),
711
				$filter
712
			));
713
		}
714
	}
715
716
	/**
717
	 * Return all the parents of this class in a set ordered from the lowest to highest parent.
718
	 *
719
	 * @return ArrayList
720
	 */
721
	public function getAncestors() {
722
		$ancestors = new ArrayList();
723
		$object    = $this->owner;
724
725
		while($object = $object->getParent()) {
726
			$ancestors->push($object);
727
		}
728
729
		return $ancestors;
730
	}
731
732
	/**
733
	 * Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute.
734
	 *
735
	 * @param string $separator
736
	 * @return string
737
	 */
738
	public function getBreadcrumbs($separator = ' &raquo; ') {
739
		$crumbs = array();
740
		$ancestors = array_reverse($this->owner->getAncestors()->toArray());
741
		foreach($ancestors as $ancestor) $crumbs[] = $ancestor->Title;
742
		$crumbs[] = $this->owner->Title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
743
		return implode($separator, $crumbs);
744
	}
745
746
	/**
747
	 * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
748
	 * then search the parents.
749
	 *
750
	 * @todo Write!
751
	 *
752
	 * @param string     $className Class name of the node to find
753
	 * @param DataObject $afterNode Used for recursive calls to this function
754
	 * @return DataObject
755
	 */
756
	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...
757
		return null;
758
	}
759
760
	/**
761
	 * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
762
	 * then search the parents.
763
	 * @param string     $className Class name of the node to find.
764
	 * @param string|int $root      ID/ClassName of the node to limit the search to
765
	 * @param DataObject $afterNode Used for recursive calls to this function
766
	 * @return DataObject
767
	 */
768
	public function naturalNext($className = null, $root = 0, $afterNode = null ) {
769
		// If this node is not the node we are searching from, then we can possibly return this node as a solution
770
		if($afterNode && $afterNode->ID != $this->owner->ID) {
771
			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...
772
				return $this->owner;
773
			}
774
		}
775
776
		$nextNode = null;
777
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
778
779
		$children = $baseClass::get()
780
			->filter('ParentID', (int)$this->owner->ID)
781
			->sort('"Sort"', 'ASC');
782
		if ($afterNode) {
783
			$children = $children->filter('Sort:GreaterThan', $afterNode->Sort);
784
		}
785
786
		// Try all the siblings of this node after the given node
787
		/*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...
788
		"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
789
		> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/
790
791
		if($children) {
792
			foreach($children as $node) {
793
				if($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
794
					break;
795
				}
796
			}
797
798
			if($nextNode) {
799
				return $nextNode;
800
			}
801
		}
802
803
		// if this is not an instance of the root class or has the root id, search the parent
804
		if(!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class)
805
				&& ($parent = $this->owner->Parent())) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on DataObject. Did you maybe mean parentClass()?

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

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

Loading history...
806
807
			return $parent->naturalNext( $className, $root, $this->owner );
808
		}
809
810
		return null;
811
	}
812
813
	/**
814
	 * Flush all Hierarchy caches:
815
	 * - Children (instance)
816
	 * - NumChildren (instance)
817
	 * - Marked (global)
818
	 * - Expanded (global)
819
	 * - TreeOpened (global)
820
	 */
821
	public function flushCache() {
822
		$this->_cache_children = null;
823
		$this->_cache_numChildren = null;
824
		self::$marked = array();
825
		self::$expanded = array();
826
		self::$treeOpened = array();
827
	}
828
829
	/**
830
	 * Reset global Hierarchy caches:
831
	 * - Marked
832
	 * - Expanded
833
	 * - TreeOpened
834
	 */
835
	public static function reset() {
836
		self::$marked = array();
837
		self::$expanded = array();
838
		self::$treeOpened = array();
839
	}
840
841
}
842