Completed
Push — master ( efd350...5ba1c4 )
by Daniel
10:49
created

TreeDropdownField::setTreeBaseID()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
use SilverStripe\ORM\DataObject;
4
/**
5
 * Dropdown-like field that allows you to select an item from a hierarchical
6
 * AJAX-expandable tree.
7
 *
8
 * Creates a field which opens a dropdown (actually a div via javascript
9
 * included for you) which contains a tree with the ability to select a singular
10
 * item for the value of the field. This field has the ability to store one-to-one
11
 * joins related to hierarchy or a hierarchy based filter.
12
 *
13
 * **Note:** your source object must use an implementation of hierarchy for this
14
 * field to generate the tree correctly, e.g. {@link Group}, {@link SiteTree} etc.
15
 *
16
 * All operations are carried out through javascript and provides no fallback
17
 * to non JS.
18
 *
19
 * <b>Usage</b>.
20
 *
21
 * <code>
22
 * static $has_one = array(
23
 *   'RightContent' => 'SiteTree'
24
 * );
25
 *
26
 * function getCMSFields() {
27
 * ...
28
 * $treedropdownfield = new TreeDropdownField("RightContentID", "Choose a page to show on the right:", "SiteTree");
29
 * ..
30
 * }
31
 * </code>
32
 *
33
 * This will generate a tree allowing the user to expand and contract subsections
34
 * to find the appropriate page to save to the field.
35
 *
36
 * @see TreeMultiselectField for the same implementation allowing multiple selections
37
 * @see DropdownField for a simple dropdown field.
38
 * @see CheckboxSetField for multiple selections through checkboxes.
39
 * @see OptionsetField for single selections via radiobuttons.
40
 *
41
 * @package forms
42
 * @subpackage fields-relational
43
 */
44
45
class TreeDropdownField extends FormField {
46
47
	private static $url_handlers = array(
48
		'$Action!/$ID' => '$Action'
49
	);
50
51
	private static $allowed_actions = array(
52
		'tree'
53
	);
54
55
	/**
56
	 * @ignore
57
	 */
58
	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...
59
		$disableCallback, $searchCallback, $baseID = 0;
60
	/**
61
	 * @var string default child method in Hierarchy->getChildrenAsUL
62
	 */
63
	protected $childrenMethod = 'AllChildrenIncludingDeleted';
64
65
	/**
66
	 * @var string default child counting method in Hierarchy->getChildrenAsUL
67
	 */
68
	protected $numChildrenMethod = 'numChildren';
69
70
	/**
71
	 * Used by field search to leave only the relevant entries
72
	 */
73
	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...
74
75
	/**
76
	 * CAVEAT: for search to work properly $labelField must be a database field,
77
	 * or you need to setSearchFunction.
78
	 *
79
	 * @param string $name the field name
80
	 * @param string $title the field label
81
	 * @param string|array $sourceObject The object-type to list in the tree. This could
82
	 * be one of the following:
83
	 * - A DataObject class name with the {@link Hierarchy} extension.
84
	 * - An array of key/value pairs, like a {@link DropdownField} source. In
85
	 *   this case, the field will act like show a flat list of tree items,
86
	 *	 without any hierarchy. This is most useful in conjunction with
87
	 *   {@link TreeMultiselectField}, for presenting a set of checkboxes in
88
	 *   a compact view. Note, that all value strings must be XML encoded
89
	 *   safely prior to being passed in.
90
	 *
91
	 * @param string $keyField to field on the source class to save as the
92
	 *		field value (default ID).
93
	 * @param string $labelField the field name to show as the human-readable
94
	 *		value on the tree (default Title).
95
	 * @param bool $showSearch enable the ability to search the tree by
96
	 *		entering the text in the input field.
97
	 */
98
	public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID',
99
		$labelField = 'TreeTitle', $showSearch = true
100
	) {
101
102
		$this->sourceObject = $sourceObject;
103
		$this->keyField     = $keyField;
104
		$this->labelField   = $labelField;
105
		$this->showSearch	= $showSearch;
106
107
		//Extra settings for Folders
108
		if ($sourceObject == 'Folder') {
109
			$this->childrenMethod = 'ChildFolders';
110
			$this->numChildrenMethod = 'numChildFolders';
111
		}
112
113
		$this->addExtraClass('single');
114
115
		parent::__construct($name, $title);
116
	}
117
118
	/**
119
	 * Set the ID of the root node of the tree. This defaults to 0 - i.e.
120
	 * displays the whole tree.
121
	 *
122
	 * @param int $ID
123
	 */
124
	public function setTreeBaseID($ID) {
125
		$this->baseID = (int) $ID;
126
		return $this;
127
	}
128
129
	/**
130
	 * Set a callback used to filter the values of the tree before
131
	 * displaying to the user.
132
	 *
133
	 * @param callback $callback
134
	 */
135
	public function setFilterFunction($callback) {
136
		if(!is_callable($callback, true)) {
137
			throw new InvalidArgumentException('TreeDropdownField->setFilterCallback(): not passed a valid callback');
138
		}
139
140
		$this->filterCallback = $callback;
141
		return $this;
142
	}
143
144
	/**
145
	 * Set a callback used to disable checkboxes for some items in the tree
146
	 *
147
	 * @param callback $callback
148
	 */
149
	public function setDisableFunction($callback) {
150
		if(!is_callable($callback, true)) {
151
			throw new InvalidArgumentException('TreeDropdownField->setDisableFunction(): not passed a valid callback');
152
		}
153
154
		$this->disableCallback = $callback;
155
		return $this;
156
	}
157
158
	/**
159
	 * Set a callback used to search the hierarchy globally, even before
160
	 * applying the filter.
161
	 *
162
	 * @param callback $callback
163
	 */
164
	public function setSearchFunction($callback) {
165
		if(!is_callable($callback, true)) {
166
			throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback');
167
		}
168
169
		$this->searchCallback = $callback;
170
		return $this;
171
	}
172
173
	public function getShowSearch() {
174
		return $this->showSearch;
175
	}
176
177
	/**
178
	 * @param Boolean
179
	 */
180
	public function setShowSearch($bool) {
181
		$this->showSearch = $bool;
182
		return $this;
183
	}
184
185
	/**
186
	 * @param $method The parameter to ChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
187
	 * {@link Hierarchy}. The method specified determines the structure of the returned list. Use "ChildFolders"
188
	 * in place of the default to get a drop-down listing with only folders, i.e. not including the child elements in
189
	 * the currently selected folder. setNumChildrenMethod() should be used as well for proper functioning.
190
	 *
191
	 * See {@link Hierarchy} for a complete list of possible methods.
192
	 */
193
	public function setChildrenMethod($method) {
194
		$this->childrenMethod = $method;
195
		return $this;
196
	}
197
198
	/**
199
	 * @param $method The parameter to numChildrenMethod to use when calling Hierarchy->getChildrenAsUL in
200
	 * {@link Hierarchy}. Should be used in conjunction with setChildrenMethod().
201
	 *
202
	 */
203
	public function setNumChildrenMethod($method) {
204
		$this->numChildrenMethod = $method;
205
		return $this;
206
	}
207
208
	/**
209
	 * @return HTMLText
210
	 */
211
	public function Field($properties = array()) {
212
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang');
213
214
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
215
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');
216
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jstree/jquery.jstree.js');
217
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/TreeDropdownField.js');
218
219
		Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css');
220
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/TreeDropdownField.css');
221
222
		$item = DataObject::singleton($this->sourceObject);
0 ignored issues
show
Bug introduced by
It seems like $this->sourceObject can also be of type array; however, SilverStripe\Framework\C...Injectable::singleton() 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...
223
		$emptyTitle = _t(
224
			'DropdownField.CHOOSE_MODEL',
225
			'(Choose {name})',
226
			['name' => $item->i18n_singular_name()]
227
		);
228
229
		$record = $this->Value() ? $this->objectForKey($this->Value()) : null;
230
		if($record instanceof ViewableData) {
231
			$title = $record->obj($this->labelField)->forTemplate();
232
		} elseif($record) {
233
			$title = Convert::raw2xml($record->{$this->labelField});
234
		}
235
		else {
236
			$title = $emptyTitle;
237
		}
238
239
		// TODO Implement for TreeMultiSelectField
240
		$metadata = array(
241
			'id' => $record ? $record->ID : null,
242
			'ClassName' => $record ? $record->ClassName : $this->sourceObject
243
		);
244
245
		$properties = array_merge(
246
			$properties,
247
			array(
248
				'Title' => $title,
249
				'EmptyTitle' => $emptyTitle,
250
				'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null,
251
			)
252
		);
253
254
		return $this->customise($properties)->renderWith('TreeDropdownField');
255
	}
256
257
	public function extraClass() {
258
		return implode(' ', array(parent::extraClass(), ($this->showSearch ? "searchable" : null)));
259
	}
260
261
	/**
262
	 * Get the whole tree of a part of the tree via an AJAX request.
263
	 *
264
	 * @param SS_HTTPRequest $request
265
	 * @return string
266
	 */
267
	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...
268
		// Array sourceObject is an explicit list of values - construct a "flat tree"
269
		if(is_array($this->sourceObject)) {
270
			$output = "<ul class=\"tree\">\n";
271
			foreach($this->sourceObject as $k => $v) {
272
				$output .= '<li id="selector-' . $this->name . '-' . $k  . '"><a>' . $v . '</a>';
273
			}
274
			$output .= "</ul>";
275
			return $output;
276
		}
277
278
		// Regular source specification
279
		$isSubTree = false;
280
281
		$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...
282
		$ID = (is_numeric($request->latestparam('ID')))
283
			? (int)$request->latestparam('ID')
284
			: (int)$request->requestVar('ID');
285
286
		if($ID && !$request->requestVar('forceFullTree')) {
287
			$obj       = DataObject::get_by_id($this->sourceObject, $ID);
288
			$isSubTree = true;
289
			if(!$obj) {
290
				throw new Exception (
291
					"TreeDropdownField->tree(): the object #$ID of type $this->sourceObject could not be found"
292
				);
293
			}
294
		} else {
295
			if($this->baseID) {
296
				$obj = DataObject::get_by_id($this->sourceObject, $this->baseID);
297
			}
298
299
			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...
300
		}
301
302
		// pre-process the tree - search needs to operate globally, not locally as marking filter does
303
		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...
304
			$this->populateIDs();
305
306
		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...
307
			$obj->setMarkingFilterFunction(array($this, "filterMarking"));
308
309
		$obj->markPartialTree($nodeCountThreshold = 30, $context = null,
310
			$this->childrenMethod, $this->numChildrenMethod);
311
312
		// allow to pass values to be selected within the ajax request
313
		if( isset($_REQUEST['forceValue']) || $this->value ) {
314
			$forceValue = ( isset($_REQUEST['forceValue']) ? $_REQUEST['forceValue'] : $this->value);
315
			if(($values = preg_split('/,\s*/', $forceValue)) && count($values)) foreach($values as $value) {
316
				if(!$value || $value == 'unchanged') continue;
317
318
				$obj->markToExpose($this->objectForKey($value));
319
			}
320
		}
321
322
		$self = $this;
323
		$titleFn = function(&$child) use(&$self) {
324
			$keyField = $self->keyField;
325
			$labelField = $self->labelField;
326
			return sprintf(
327
				'<li id="selector-%s-%s" data-id="%s" class="class-%s %s %s"><a rel="%d">%s</a>',
328
				Convert::raw2xml($self->getName()),
329
				Convert::raw2xml($child->$keyField),
330
				Convert::raw2xml($child->$keyField),
331
				Convert::raw2xml($child->class),
332
				Convert::raw2xml($child->markingClasses($self->numChildrenMethod)),
333
				($self->nodeIsDisabled($child)) ? 'disabled' : '',
334
				(int)$child->ID,
335
				$child->obj($labelField)->forTemplate()
336
			);
337
		};
338
339
		// Limit the amount of nodes shown for performance reasons.
340
		// Skip the check if we're filtering the tree, since its not clear how many children will
341
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
342
		$nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf');
343
		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...
344
			$className = $this->sourceObject;
345
			$nodeCountCallback = function($parent, $numChildren) use($className, $nodeThresholdLeaf) {
346
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
347
					return sprintf(
348
						'<ul><li><span class="item">%s</span></li></ul>',
349
						_t('LeftAndMain.TooManyPages', 'Too many pages')
350
					);
351
				}
352
			};
353
		} else {
354
			$nodeCountCallback = null;
355
		}
356
357
		if($isSubTree) {
358
			$html = $obj->getChildrenAsUL(
359
				"",
360
				$titleFn,
361
				null,
362
				true,
363
				$this->childrenMethod,
364
				$this->numChildrenMethod,
365
				true, // root call
366
				null,
367
				$nodeCountCallback
368
			);
369
			return substr(trim($html), 4, -5);
370
		} else {
371
			$html = $obj->getChildrenAsUL(
372
				'class="tree"',
373
				$titleFn,
374
				null,
375
				true,
376
				$this->childrenMethod,
377
				$this->numChildrenMethod,
378
				true, // root call
379
				null,
380
				$nodeCountCallback
381
			);
382
			return $html;
383
		}
384
	}
385
386
	/**
387
	 * Marking public function for the tree, which combines different filters sensibly.
388
	 * If a filter function has been set, that will be called. And if search text is set,
389
	 * filter on that too. Return true if all applicable conditions are true, false otherwise.
390
	 * @param $node
391
	 * @return unknown_type
392
	 */
393
	public function filterMarking($node) {
394
		if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) return false;
395
		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...
396
			return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
397
		}
398
399
		return true;
400
	}
401
402
	/**
403
	 * Marking a specific node in the tree as disabled
404
	 * @param $node
405
	 * @return boolean
406
	 */
407
	public function nodeIsDisabled($node) {
408
		return ($this->disableCallback && call_user_func($this->disableCallback, $node));
409
	}
410
411
	/**
412
	 * @param String $field
413
	 */
414
	public function setLabelField($field) {
415
		$this->labelField = $field;
416
		return $this;
417
	}
418
419
	/**
420
	 * @return String
421
	 */
422
	public function getLabelField() {
423
		return $this->labelField;
424
	}
425
426
	/**
427
	 * @param String $field
428
	 */
429
	public function setKeyField($field) {
430
		$this->keyField = $field;
431
		return $this;
432
	}
433
434
	/**
435
	 * @return String
436
	 */
437
	public function getKeyField() {
438
		return $this->keyField;
439
	}
440
441
	/**
442
	 * @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...
443
	 */
444
	public function setSourceObject($class) {
445
		$this->sourceObject = $class;
446
		return $this;
447
	}
448
449
	/**
450
	 * @return String
451
	 */
452
	public function getSourceObject() {
453
		return $this->sourceObject;
454
	}
455
456
	/**
457
	 * Populate $this->searchIds with the IDs of the pages matching the searched parameter and their parents.
458
	 * Reverse-constructs the tree starting from the leaves. Initially taken from CMSSiteTreeFilter, but modified
459
	 * with pluggable search function.
460
	 */
461
	protected function populateIDs() {
462
		// get all the leaves to be displayed
463
		if ($this->searchCallback) {
464
			$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...
465
		} else {
466
			$sourceObject = $this->sourceObject;
467
			$filters = array();
468
			if(singleton($sourceObject)->hasDatabaseField($this->labelField)) {
0 ignored issues
show
Bug introduced by
It seems like $sourceObject defined by $this->sourceObject on line 466 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...
469
				$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...
470
			} else {
471
				if(singleton($sourceObject)->hasDatabaseField('Title')) {
0 ignored issues
show
Bug introduced by
It seems like $sourceObject defined by $this->sourceObject on line 466 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...
472
					$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...
473
				}
474
				if(singleton($sourceObject)->hasDatabaseField('Name')) {
0 ignored issues
show
Bug introduced by
It seems like $sourceObject defined by $this->sourceObject on line 466 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...
475
					$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...
476
				}
477
			}
478
479
			if(empty($filters)) {
480
				throw new InvalidArgumentException(sprintf(
481
					'Cannot query by %s.%s, not a valid database column',
482
					$sourceObject,
483
					$this->labelField
484
				));
485
			}
486
			$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, SilverStripe\ORM\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...
487
		}
488
489
		if( $res ) {
490
			// iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control
491
			foreach($res as $row) {
492
				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...
493
				$this->searchIds[$row->ID] = true;
494
			}
495
496
			$sourceObject = $this->sourceObject;
497
498
			while (!empty($parents)) {
499
				$items = $sourceObject::get()
500
					->filter("ID",array_keys($parents));
501
				$parents = array();
502
503
				foreach($items as $item) {
504
					if ($item->ParentID) $parents[$item->ParentID] = true;
505
					$this->searchIds[$item->ID] = true;
506
					$this->searchExpanded[$item->ID] = true;
507
				}
508
			}
509
		}
510
	}
511
512
	/**
513
	 * Get the object where the $keyField is equal to a certain value
514
	 *
515
	 * @param string|int $key
516
	 * @return DataObject
517
	 */
518
	protected function objectForKey($key) {
519
		return DataObject::get($this->sourceObject)
0 ignored issues
show
Bug introduced by
It seems like $this->sourceObject can also be of type array; however, SilverStripe\ORM\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...
520
			->filter($this->keyField, $key)
521
			->first();
522
	}
523
524
	/**
525
	 * Changes this field to the readonly field.
526
	 */
527
	public function performReadonlyTransformation() {
528
		$copy = $this->castedCopy('TreeDropdownField_Readonly');
529
		$copy->setKeyField($this->keyField);
530
		$copy->setLabelField($this->labelField);
531
		$copy->setSourceObject($this->sourceObject);
532
533
		return $copy;
534
	}
535
536
}
537
538
/**
539
 * @package forms
540
 * @subpackage fields-relational
541
 */
542
class TreeDropdownField_Readonly extends TreeDropdownField {
543
	protected $readonly = true;
544
545
	public function Field($properties = array()) {
546
		$fieldName = $this->labelField;
547
		if($this->value) {
548
			$keyObj = $this->objectForKey($this->value);
549
			$obj = $keyObj ? $keyObj->$fieldName : '';
550
		} else {
551
			$obj = null;
552
		}
553
554
		$source = array(
555
			$this->value => $obj
556
		);
557
558
		$field = new LookupField($this->name, $this->title, $source);
559
		$field->setValue($this->value);
560
		$field->setForm($this->form);
561
		$field->dontEscape = true;
562
		return $field->Field();
563
	}
564
}
565