Completed
Push — 3.7 ( 677d28...81b2d8 )
by Robbie
15:41 queued 06:22
created

TreeDropdownField   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 501
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 0
loc 501
rs 4.8717
c 0
b 0
f 0
wmc 80
lcom 1
cbo 7

23 Methods

Rating   Name   Duplication   Size   Complexity  
A setDisableFunction() 0 8 2
A __construct() 0 19 2
A setTreeBaseID() 0 4 1
A setFilterFunction() 0 8 2
A setSearchFunction() 0 8 2
A getShowSearch() 0 3 1
A setShowSearch() 0 4 1
A setChildrenMethod() 0 4 1
A setNumChildrenMethod() 0 4 1
C Field() 0 44 8
A extraClass() 0 3 2
A setLabelField() 0 4 1
A getLabelField() 0 3 1
F tree() 0 118 29
B filterMarking() 0 8 6
A nodeIsDisabled() 0 3 2
A setKeyField() 0 4 1
A getKeyField() 0 3 1
A setSourceObject() 0 4 1
A getSourceObject() 0 3 1
C populateIDs() 0 50 12
A objectForKey() 0 5 1
A performReadonlyTransformation() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like TreeDropdownField often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

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

1
<?php
2
/**
3
 * Dropdown-like field that allows you to select an item from a hierarchical
4
 * AJAX-expandable tree.
5
 *
6
 * Creates a field which opens a dropdown (actually a div via javascript
7
 * included for you) which contains a tree with the ability to select a singular
8
 * item for the value of the field. This field has the ability to store one-to-one
9
 * joins related to hierarchy or a hierarchy based filter.
10
 *
11
 * **Note:** your source object must use an implementation of hierarchy for this
12
 * field to generate the tree correctly, e.g. {@link Group}, {@link SiteTree} etc.
13
 *
14
 * All operations are carried out through javascript and provides no fallback
15
 * to non JS.
16
 *
17
 * <b>Usage</b>.
18
 *
19
 * <code>
20
 * static $has_one = array(
21
 *   'RightContent' => 'SiteTree'
22
 * );
23
 *
24
 * function getCMSFields() {
25
 * ...
26
 * $treedropdownfield = new TreeDropdownField("RightContentID", "Choose a page to show on the right:", "SiteTree");
27
 * ..
28
 * }
29
 * </code>
30
 *
31
 * This will generate a tree allowing the user to expand and contract subsections
32
 * to find the appropriate page to save to the field.
33
 *
34
 * @see TreeMultiselectField for the same implementation allowing multiple selections
35
 * @see DropdownField for a simple dropdown field.
36
 * @see CheckboxSetField for multiple selections through checkboxes.
37
 * @see OptionsetField for single selections via radiobuttons.
38
 *
39
 * @package forms
40
 * @subpackage fields-relational
41
 */
42
43
class TreeDropdownField extends FormField {
44
45
	private static $url_handlers = array(
46
		'$Action!/$ID' => '$Action'
47
	);
48
49
	private static $allowed_actions = array(
50
		'tree'
51
	);
52
53
	/**
54
	 * @config
55
	 * @var int
56
	 * @see {@link Hierarchy::$node_threshold_total}.
57
	 */
58
	private static $node_threshold_total = 30;
59
60
	/**
61
	 * @ignore
62
	 */
63
	protected $sourceObject, $keyField, $labelField, $filterCallback,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
64
		$disableCallback, $searchCallback, $baseID = 0;
65
	/**
66
	 * @var string default child method in Hierarchy->getChildrenAsUL
67
	 */
68
	protected $childrenMethod = 'AllChildrenIncludingDeleted';
69
70
	/**
71
	 * @var string default child counting method in Hierarchy->getChildrenAsUL
72
	 */
73
	protected $numChildrenMethod = 'numChildren';
74
75
	/**
76
	 * Used by field search to leave only the relevant entries
77
	 */
78
	protected $searchIds = null, $showSearch, $searchExpanded = array();
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
79
80
	/**
81
	 * CAVEAT: for search to work properly $labelField must be a database field,
82
	 * or you need to setSearchFunction.
83
	 *
84
	 * @param string $name the field name
85
	 * @param string $title the field label
86
	 * @param string|array $sourceObject The object-type to list in the tree. This could
87
	 * be one of the following:
88
	 * - A DataObject class name with the {@link Hierarchy} extension.
89
	 * - An array of key/value pairs, like a {@link DropdownField} source. In
90
	 *   this case, the field will act like show a flat list of tree items,
91
	 *	 without any hierarchy. This is most useful in conjunction with
92
	 *   {@link TreeMultiselectField}, for presenting a set of checkboxes in
93
	 *   a compact view. Note, that all value strings must be XML encoded
94
	 *   safely prior to being passed in.
95
	 *
96
	 * @param string $keyField to field on the source class to save as the
97
	 *		field value (default ID).
98
	 * @param string $labelField the field name to show as the human-readable
99
	 *		value on the tree (default Title).
100
	 * @param bool $showSearch enable the ability to search the tree by
101
	 *		entering the text in the input field.
102
	 */
103
	public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID',
104
		$labelField = 'TreeTitle', $showSearch = true
105
	) {
106
107
		$this->sourceObject = $sourceObject;
108
		$this->keyField     = $keyField;
109
		$this->labelField   = $labelField;
110
		$this->showSearch	= $showSearch;
111
112
		//Extra settings for Folders
113
		if ($sourceObject == 'Folder') {
114
			$this->childrenMethod = 'ChildFolders';
115
			$this->numChildrenMethod = 'numChildFolders';
116
		}
117
118
		$this->addExtraClass('single');
119
120
		parent::__construct($name, $title);
121
	}
122
123
	/**
124
	 * Set the ID of the root node of the tree. This defaults to 0 - i.e.
125
	 * displays the whole tree.
126
	 *
127
	 * @param int $ID
128
	 */
129
	public function setTreeBaseID($ID) {
130
		$this->baseID = (int) $ID;
131
		return $this;
132
	}
133
134
	/**
135
	 * Set a callback used to filter the values of the tree before
136
	 * displaying to the user.
137
	 *
138
	 * @param callback $callback
139
	 */
140
	public function setFilterFunction($callback) {
141
		if(!is_callable($callback, true)) {
142
			throw new InvalidArgumentException('TreeDropdownField->setFilterCallback(): not passed a valid callback');
143
		}
144
145
		$this->filterCallback = $callback;
146
		return $this;
147
	}
148
149
	/**
150
	 * Set a callback used to disable checkboxes for some items in the tree
151
	 *
152
	 * @param callback $callback
153
	 */
154
	public function setDisableFunction($callback) {
155
		if(!is_callable($callback, true)) {
156
			throw new InvalidArgumentException('TreeDropdownField->setDisableFunction(): not passed a valid callback');
157
		}
158
159
		$this->disableCallback = $callback;
160
		return $this;
161
	}
162
163
	/**
164
	 * Set a callback used to search the hierarchy globally, even before
165
	 * applying the filter.
166
	 *
167
	 * @param callback $callback
168
	 */
169
	public function setSearchFunction($callback) {
170
		if(!is_callable($callback, true)) {
171
			throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback');
172
		}
173
174
		$this->searchCallback = $callback;
175
		return $this;
176
	}
177
178
	public function getShowSearch() {
179
		return $this->showSearch;
180
	}
181
182
	/**
183
	 * @param Boolean
184
	 */
185
	public function setShowSearch($bool) {
186
		$this->showSearch = $bool;
187
		return $this;
188
	}
189
190
	/**
191
	 * @param $method The parameter to ChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
192
	 * {@link Hierarchy}. The method specified determines the structure of the returned list. Use "ChildFolders"
193
	 * in place of the default to get a drop-down listing with only folders, i.e. not including the child elements in
194
	 * the currently selected folder. setNumChildrenMethod() should be used as well for proper functioning.
195
	 *
196
	 * See {@link Hierarchy} for a complete list of possible methods.
197
	 */
198
	public function setChildrenMethod($method) {
199
		$this->childrenMethod = $method;
200
		return $this;
201
	}
202
203
	/**
204
	 * @param $method The parameter to numChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
205
	 * {@link Hierarchy}. Should be used in conjunction with setChildrenMethod().
206
	 *
207
	 */
208
	public function setNumChildrenMethod($method) {
209
		$this->numChildrenMethod = $method;
210
		return $this;
211
	}
212
213
	/**
214
	 * @return HTMLText
215
	 */
216
	public function Field($properties = array()) {
217
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
218
219
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
220
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');
221
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jstree/jquery.jstree.js');
222
		Requirements::javascript(FRAMEWORK_DIR . '/javascript/TreeDropdownField.js');
223
224
		Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css');
225
		Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css');
226
227
		if($this->showSearch) {
228
			$emptyTitle = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown');
229
		} else {
230
			$emptyTitle = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
231
		}
232
233
		$record = $this->Value() ? $this->objectForKey($this->Value()) : null;
234
		if($record instanceof ViewableData) {
235
			$title = $record->obj($this->labelField)->forTemplate();
236
		} elseif($record) {
237
			$title = Convert::raw2xml($record->{$this->labelField});
238
		}
239
		else {
240
			$title = $emptyTitle;
241
		}
242
243
		// TODO Implement for TreeMultiSelectField
244
		$metadata = array(
245
			'id' => $record ? $record->ID : null,
246
			'ClassName' => $record ? $record->ClassName : $this->sourceObject
247
		);
248
249
		$properties = array_merge(
250
			$properties,
251
			array(
252
				'Title' => $title,
253
				'EmptyTitle' => $emptyTitle,
254
				'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null,
255
			)
256
		);
257
258
		return parent::Field($properties);
259
	}
260
261
	public function extraClass() {
262
		return implode(' ', array(parent::extraClass(), ($this->showSearch ? "searchable" : null)));
263
	}
264
265
	/**
266
	 * Get the whole tree of a part of the tree via an AJAX request.
267
	 *
268
	 * @param SS_HTTPRequest $request
269
	 * @return string
270
	 */
271
	public function tree(SS_HTTPRequest $request) {
0 ignored issues
show
Coding Style introduced by
tree uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
272
		// Array sourceObject is an explicit list of values - construct a "flat tree"
273
		if(is_array($this->sourceObject)) {
274
			$output = "<ul class=\"tree\">\n";
275
			foreach($this->sourceObject as $k => $v) {
276
				$output .= '<li id="selector-' . $this->name . '-' . $k  . '"><a>' . $v . '</a>';
277
			}
278
			$output .= "</ul>";
279
			return $output;
280
		}
281
282
		// Regular source specification
283
		$isSubTree = false;
284
285
		$this->search = $request->requestVar('search');
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
286
		$ID = (is_numeric($request->latestparam('ID')))
287
			? (int)$request->latestparam('ID')
288
			: (int)$request->requestVar('ID');
289
290
		if($ID && !$request->requestVar('forceFullTree')) {
291
			$obj       = DataObject::get_by_id($this->sourceObject, $ID);
292
			$isSubTree = true;
293
			if(!$obj) {
294
				throw new Exception (
295
					"TreeDropdownField->tree(): the object #$ID of type $this->sourceObject could not be found"
296
				);
297
			}
298
		} else {
299
			if($this->baseID) {
300
				$obj = DataObject::get_by_id($this->sourceObject, $this->baseID);
301
			}
302
303
			if(!$this->baseID || !$obj) $obj = singleton($this->sourceObject);
0 ignored issues
show
Bug introduced by
The variable $obj 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...
304
		}
305
306
		// pre-process the tree - search needs to operate globally, not locally as marking filter does
307
		if ( $this->search != "" )
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
308
			$this->populateIDs();
309
310
		if ($this->filterCallback || $this->search != "" )
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
311
			$obj->setMarkingFilterFunction(array($this, "filterMarking"));
312
313
		$obj->markPartialTree($nodeCountThreshold = 30, $context = null,
314
			$this->childrenMethod, $this->numChildrenMethod);
315
316
		// allow to pass values to be selected within the ajax request
317
		if( isset($_REQUEST['forceValue']) || $this->value ) {
318
			$forceValue = ( isset($_REQUEST['forceValue']) ? $_REQUEST['forceValue'] : $this->value);
319
			if(($values = preg_split('/,\s*/', $forceValue)) && count($values)) foreach($values as $value) {
320
				if(!$value || $value == 'unchanged') continue;
321
322
				$obj->markToExpose($this->objectForKey($value));
323
					}
324
		}
325
326
		$self = $this;
327
		$titleFn = function(&$child) use(&$self) {
328
			$keyField = $self->keyField;
329
			$labelField = $self->labelField;
330
			return sprintf(
331
				'<li id="selector-%s-%s" data-id="%s" class="class-%s %s %s"><a rel="%d">%s</a>',
332
				Convert::raw2xml($self->getName()),
333
				Convert::raw2xml($child->$keyField),
334
				Convert::raw2xml($child->$keyField),
335
				Convert::raw2xml($child->class),
336
				Convert::raw2xml($child->markingClasses($self->numChildrenMethod)),
337
				($self->nodeIsDisabled($child)) ? 'disabled' : '',
338
				(int)$child->ID,
339
				$child->obj($labelField)->forTemplate()
340
			);
341
		};
342
343
		// Limit the amount of nodes shown for performance reasons.
344
		// Skip the check if we're filtering the tree, since its not clear how many children will
345
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
346
		$nodeThresholdLeaf = Config::inst()->get('Hierarchy', 'node_threshold_leaf');
347
		if($nodeThresholdLeaf && !$this->filterCallback && !$this->search) {
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
348
			$className = $this->sourceObject;
349
			$nodeCountCallback = function($parent, $numChildren) use($className, $nodeThresholdLeaf) {
350
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
351
					return sprintf(
352
						'<ul><li><span class="item">%s</span></li></ul>',
353
						_t('LeftAndMain.TooManyPages', 'Too many pages')
354
					);
355
				}
356
			};
357
		} else {
358
			$nodeCountCallback = null;
359
		}
360
361
		if($isSubTree) {
362
			$html = $obj->getChildrenAsUL(
363
				"",
364
				$titleFn,
365
				null,
366
				true,
367
				$this->childrenMethod,
368
				$this->numChildrenMethod,
369
				true, // root call
370
				null,
371
				$nodeCountCallback
372
			);
373
			return substr(trim($html), 4, -5);
374
		} else {
375
			$html = $obj->getChildrenAsUL(
376
				'class="tree"',
377
				$titleFn,
378
				null,
379
				true,
380
				$this->childrenMethod,
381
				$this->numChildrenMethod,
382
				true, // root call
383
				null,
384
				$nodeCountCallback
385
			);
386
			return $html;
387
		}
388
	}
389
390
	/**
391
	 * Marking public function for the tree, which combines different filters sensibly.
392
	 * If a filter function has been set, that will be called. And if search text is set,
393
	 * filter on that too. Return true if all applicable conditions are true, false otherwise.
394
	 * @param DataObject $node
395
	 * @return boolean
396
	 */
397
	public function filterMarking($node) {
398
		if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) return false;
399
		if ($this->search != "") {
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
400
			return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
401
		}
402
403
		return true;
404
	}
405
406
	/**
407
	 * Marking a specific node in the tree as disabled
408
	 * @param $node
409
	 * @return boolean
410
	 */
411
	public function nodeIsDisabled($node) {
412
		return ($this->disableCallback && call_user_func($this->disableCallback, $node));
413
	}
414
415
	/**
416
	 * @param string $field
417
	 * @return $this
418
	 */
419
	public function setLabelField($field) {
420
		$this->labelField = $field;
421
		return $this;
422
	}
423
424
	/**
425
	 * @return string
426
	 */
427
	public function getLabelField() {
428
		return $this->labelField;
429
	}
430
431
	/**
432
	 * @param string $field
433
	 * @return $this
434
	 */
435
	public function setKeyField($field) {
436
		$this->keyField = $field;
437
		return $this;
438
	}
439
440
	/**
441
	 * @return string
442
	 */
443
	public function getKeyField() {
444
		return $this->keyField;
445
	}
446
447
	/**
448
	 * @param string $field
0 ignored issues
show
Bug introduced by
There is no parameter named $field. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
449
	 * @return $this
450
	 */
451
	public function setSourceObject($class) {
452
		$this->sourceObject = $class;
453
		return $this;
454
	}
455
456
	/**
457
	 * @return string
458
	 */
459
	public function getSourceObject() {
460
		return $this->sourceObject;
461
	}
462
463
	/**
464
	 * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
465
	 * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
466
	 * with pluggable search function.
467
	 */
468
	protected function populateIDs() {
469
		// get all the leaves to be displayed
470
		if ($this->searchCallback) {
471
			$res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
472
		} else {
473
			$sourceObject = $this->sourceObject;
474
			$filters = array();
475
			if(singleton($sourceObject)->hasDatabaseField($this->labelField)) {
0 ignored issues
show
Bug introduced by
It seems like $sourceObject defined by $this->sourceObject on line 473 can also be of type array; however, singleton() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
476
				$filters["{$this->labelField}:PartialMatch"]  = $this->search;
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
477
			} else {
478
				if(singleton($sourceObject)->hasDatabaseField('Title')) {
0 ignored issues
show
Bug introduced by
It seems like $sourceObject defined by $this->sourceObject on line 473 can also be of type array; however, singleton() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
479
					$filters["Title:PartialMatch"] = $this->search;
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
480
				}
481
				if(singleton($sourceObject)->hasDatabaseField('Name')) {
0 ignored issues
show
Bug introduced by
It seems like $sourceObject defined by $this->sourceObject on line 473 can also be of type array; however, singleton() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
482
					$filters["Name:PartialMatch"] = $this->search;
0 ignored issues
show
Bug introduced by
The property search does not seem to exist. Did you mean searchCallback?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
483
				}
484
			}
485
486
			if(empty($filters)) {
487
				throw new InvalidArgumentException(sprintf(
488
					'Cannot query by %s.%s, not a valid database column',
489
					$sourceObject,
490
					$this->labelField
491
				));
492
			}
493
			$res = DataObject::get($this->sourceObject)->filterAny($filters);
0 ignored issues
show
Bug introduced by
It seems like $this->sourceObject can also be of type array; however, DataObject::get() does only seem to accept string|null, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
494
		}
495
496
		if( $res ) {
497
			// iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
498
			foreach($res as $row) {
499
				if ($row->ParentID) $parents[$row->ParentID] = true;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parents was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parents = 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...
500
				$this->searchIds[$row->ID] = true;
501
			}
502
503
			$sourceObject = $this->sourceObject;
504
505
			while (!empty($parents)) {
506
				$items = $sourceObject::get()
507
					->filter("ID",array_keys($parents));
508
				$parents = array();
509
510
				foreach($items as $item) {
511
					if ($item->ParentID) $parents[$item->ParentID] = true;
512
					$this->searchIds[$item->ID] = true;
513
					$this->searchExpanded[$item->ID] = true;
514
				}
515
			}
516
		}
517
	}
518
519
	/**
520
	 * Get the object where the $keyField is equal to a certain value
521
	 *
522
	 * @param string|int $key
523
	 * @return DataObject
524
	 */
525
	protected function objectForKey($key) {
526
		return DataObject::get($this->sourceObject)
0 ignored issues
show
Bug introduced by
It seems like $this->sourceObject can also be of type array; however, DataObject::get() does only seem to accept string|null, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
527
			->filter($this->keyField, $key)
528
			->first();
529
	}
530
531
	/**
532
	 * Changes this field to the readonly field.
533
	 */
534
	public function performReadonlyTransformation() {
535
		$copy = $this->castedCopy('TreeDropdownField_Readonly');
536
		$copy->setKeyField($this->keyField);
537
		$copy->setLabelField($this->labelField);
538
		$copy->setSourceObject($this->sourceObject);
539
540
		return $copy;
541
	}
542
543
}
544
545
/**
546
 * @package forms
547
 * @subpackage fields-relational
548
 */
549
class TreeDropdownField_Readonly extends TreeDropdownField {
550
	protected $readonly = true;
551
552
	public function Field($properties = array()) {
553
		$fieldName = $this->labelField;
554
		if($this->value) {
555
			$keyObj = $this->objectForKey($this->value);
556
			$obj = $keyObj ? $keyObj->$fieldName : '';
557
		} else {
558
			$obj = null;
559
		}
560
561
		$source = array(
562
			$this->value => $obj
563
		);
564
565
		$field = new LookupField($this->name, $this->title, $source);
566
		$field->setValue($this->value);
567
		$field->setForm($this->form);
568
		$field->dontEscape = true;
569
		return $field->Field();
570
	}
571
}
572