Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

ListboxField::setSource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 13
rs 9.4285
cc 3
eloc 8
nc 3
nop 1
1
<?php
2
/**
3
 * Multi-line listbox field, created from a <select> tag.
4
 *
5
 * <b>Usage</b>
6
 *
7
 * <code>
8
 * new ListboxField(
9
 *    $name = "pickanumber",
10
 *    $title = "Pick a number",
11
 *    $source = array(
12
 *       "1" => "one",
13
 *       "2" => "two",
14
 *       "3" => "three"
15
 *    ),
16
 *    $value = 1
17
 * )
18
 * </code>
19
 *
20
 * @see DropdownField for a simple <select> field with a single element.
21
 * @see CheckboxSetField for multiple selections through checkboxes.
22
 * @see OptionsetField for single selections via radiobuttons.
23
 * @see TreeDropdownField for a rich and customizeable UI that can visualize a tree of selectable elements
24
 *
25
 * @package forms
26
 * @subpackage fields-basic
27
 */
28
class ListboxField extends DropdownField {
29
30
	/**
31
	 * The size of the field in rows.
32
	 * @var int
33
	 */
34
	protected $size;
35
36
	/**
37
	 * Should the user be able to select multiple
38
	 * items on this dropdown field?
39
	 *
40
	 * @var boolean
41
	 */
42
	protected $multiple = false;
43
44
	/**
45
	 * @var Array
46
	 */
47
	protected $disabledItems = array();
48
49
	/**
50
	 * @var Array
51
	 */
52
	protected $defaultItems = array();
53
54
	/**
55
	 * Creates a new dropdown field.
56
	 *
57
	 * @param string $name The field name
58
	 * @param string $title The field title
59
	 * @param array $source An map of the dropdown items
60
	 * @param string|array $value You can pass an array of values or a single value like a drop down to be selected
61
	 * @param int $size Optional size of the select element
62
	 * @param form The parent form
63
	 */
64
	public function __construct($name, $title = '', $source = array(), $value = '', $size = null, $multiple = false) {
65
		if($size) $this->size = $size;
0 ignored issues
show
Bug Best Practice introduced by
The expression $size of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
66
		if($multiple) $this->multiple = $multiple;
67
68
		parent::__construct($name, $title, $source, $value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 64 can also be of type array; however, DropdownField::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
69
	}
70
71
	/**
72
	 * Returns a <select> tag containing all the appropriate <option> tags
73
	 */
74
	public function Field($properties = array()) {
75
		if($this->multiple) $this->name .= '[]';
76
		$options = array();
77
78
		// We have an array of values
79
		if(is_array($this->value)){
80
			// Loop through and figure out which values were selected.
81 View Code Duplication
			foreach($this->getSource() as $value => $title) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
82
				$options[] = new ArrayData(array(
83
					'Title' => $title,
84
					'Value' => $value,
85
					'Selected' => (in_array($value, $this->value) || in_array($value, $this->defaultItems)),
86
					'Disabled' => $this->disabled || in_array($value, $this->disabledItems),
87
				));
88
			}
89
		} else {
90
			// Listbox was based a singlular value, so treat it like a dropdown.
91 View Code Duplication
			foreach($this->getSource() as $value => $title) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
92
				$options[] = new ArrayData(array(
93
					'Title' => $title,
94
					'Value' => $value,
95
					'Selected' => ($value == $this->value || in_array($value, $this->defaultItems)),
96
					'Disabled' => $this->disabled || in_array($value, $this->disabledItems),
97
				));
98
			}
99
		}
100
101
		$properties = array_merge($properties, array(
102
			'Options' => new ArrayList($options)
103
		));
104
105
		return $this->customise($properties)->renderWith($this->getTemplates());
106
	}
107
108
	public function getAttributes() {
109
		return array_merge(
110
			parent::getAttributes(),
111
			array(
112
				'multiple' => $this->multiple,
113
				'size' => $this->size
114
			)
115
		);
116
	}
117
118
	/**
119
	 * Sets the size of this dropdown in rows.
120
	 * @param int $size The height in rows (e.g. 3)
121
	 */
122
	public function setSize($size) {
123
		$this->size = $size;
124
		return $this;
125
	}
126
127
	/**
128
	 * Sets this field to have a muliple select attribute
129
	 * @param boolean $bool
130
	 */
131
	public function setMultiple($bool) {
132
		$this->multiple = $bool;
133
		return $this;
134
	}
135
136
	public function setSource($source) {
137
		if($source) {
138
			$hasCommas = array_filter(array_keys($source),
139
				create_function('$key', 'return strpos($key, ",") !== FALSE;'));
140
			if($hasCommas) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasCommas 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...
141
				throw new InvalidArgumentException('No commas allowed in $source keys');
142
			}
143
		}
144
145
		parent::setSource($source);
146
147
		return $this;
148
	}
149
150
	/**
151
	 * Return the CheckboxSetField value as a string
152
	 * selected item keys.
153
	 *
154
	 * @return string
155
	 */
156 View Code Duplication
	public function dataValue() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
157
		if($this->value && is_array($this->value) && $this->multiple) {
158
			$filtered = array();
159
			foreach($this->value as $item) {
160
				if($item) {
161
					$filtered[] = str_replace(",", "{comma}", $item);
162
				}
163
			}
164
			return implode(',', $filtered);
165
		} else {
166
			return parent::dataValue();
167
		}
168
	}
169
170
	/**
171
	 * Save the current value of this field into a DataObject.
172
	 * If the field it is saving to is a has_many or many_many relationship,
173
	 * it is saved by setByIDList(), otherwise it creates a comma separated
174
	 * list for a standard DB text/varchar field.
175
	 *
176
	 * @param DataObject $record The record to save into
177
	 */
178
	public function saveInto(DataObjectInterface $record) {
179
		if($this->multiple) {
180
			$fieldname = $this->name;
181
			$relation = ($fieldname && $record && $record->hasMethod($fieldname)) ? $record->$fieldname() : null;
182
			if($fieldname && $record && $relation &&
183
				($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
184
				$idList = (is_array($this->value)) ? array_values($this->value) : array();
185
				if(!$record->ID) {
0 ignored issues
show
Bug introduced by
Accessing ID on the interface DataObjectInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
186
					$record->write(); // record needs to have an ID in order to set relationships
187
					$relation = ($fieldname && $record && $record->hasMethod($fieldname))
188
						? $record->$fieldname()
189
						: null;
190
				}
191
				$relation->setByIDList($idList);
192 View Code Duplication
			} elseif($fieldname && $record) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
193
				if($this->value) {
194
					$this->value = str_replace(',', '{comma}', $this->value);
195
					$record->$fieldname = implode(",", $this->value);
196
				} else {
197
					$record->$fieldname = null;
198
				}
199
			}
200
		} else {
201
			parent::saveInto($record);
202
		}
203
	}
204
205
	/**
206
	 * Load a value into this ListboxField
207
	 */
208
	public function setValue($val, $obj = null) {
209
		// If we're not passed a value directly,
210
		// we can look for it in a relation method on the object passed as a second arg
211 View Code Duplication
		if(!$val && $obj && $obj instanceof DataObject && $obj->hasMethod($this->name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
212
			$funcName = $this->name;
213
			$val = array_values($obj->$funcName()->getIDList());
214
		}
215
216
		if($val) {
217
			if(!$this->multiple && is_array($val)) {
218
				throw new InvalidArgumentException('Array values are not allowed (when multiple=false).');
219
			}
220
221
			if($this->multiple) {
222
				$parts = (is_array($val)) ? $val : preg_split("/ *, */", trim($val));
223
				if(ArrayLib::is_associative($parts)) {
224
					// This is due to the possibility of accidentally passing an array of values (as keys) and titles (as values) when only the keys were intended to be saved.
225
					throw new InvalidArgumentException('Associative arrays are not allowed as values (when multiple=true), only indexed arrays.');
226
				}
227
228
				// Doesn't check against unknown values in order to allow for less rigid data handling.
229
				// They're silently ignored and overwritten the next time the field is saved.
230
				parent::setValue($parts);
231
			} else {
232
				if(!in_array($val, array_keys($this->getSource()))) {
233
					throw new InvalidArgumentException(sprintf(
234
						'Invalid value "%s" for multiple=false',
235
						Convert::raw2xml($val)
236
					));
237
				}
238
239
				parent::setValue($val);
240
			}
241
		} else {
242
			parent::setValue($val);
243
		}
244
245
		return $this;
246
	}
247
248
	/**
249
	 * Mark certain elements as disabled,
250
	 * regardless of the {@link setDisabled()} settings.
251
	 *
252
	 * @param array $items Collection of array keys, as defined in the $source array
253
	 */
254
	public function setDisabledItems($items) {
255
		$this->disabledItems = $items;
256
		return $this;
257
	}
258
259
	/**
260
	 * @return Array
261
	 */
262
	public function getDisabledItems() {
263
		return $this->disabledItems;
264
	}
265
266
	/**
267
	 * Default selections, regardless of the {@link setValue()} settings.
268
	 * Note: Items marked as disabled through {@link setDisabledItems()} can still be
269
	 * selected by default through this method.
270
	 *
271
	 * @param Array $items Collection of array keys, as defined in the $source array
272
	 */
273
	public function setDefaultItems($items) {
274
		$this->defaultItems = $items;
275
		return $this;
276
	}
277
278
	/**
279
	 * @return Array
280
	 */
281
	public function getDefaultItems() {
282
		return $this->defaultItems;
283
	}
284
285
	/**
286
	 * Validate this field
287
	 *
288
	 * @param Validator $validator
289
	 * @return bool
290
	 */
291
	public function validate($validator) {
292
		$values = $this->value;
293
		if (!$values) {
294
			return true;
295
		}
296
		$source = $this->getSourceAsArray();
297
		if (is_array($values)) {
298
			if (!array_intersect_key($source,array_flip($values))) {
299
				$validator->validationError(
300
					$this->name,
301
					_t(
302
						"Please select a value within the list provided. {value} is not a valid option",
303
						array('value' => $this->value)
0 ignored issues
show
Documentation introduced by
array('value' => $this->value) is of type array<string,*,{"value":"*"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
304
					),
305
					"validation"
306
				);
307
				return false;
308
			}
309 View Code Duplication
		} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
310
			if (!array_key_exists($this->value, $source)) {
311
				$validator->validationError(
312
					$this->name,
313
					_t(
314
						'ListboxField.SOURCE_VALIDATION',
315
						"Please select a value within the list provided. %s is not a valid option",
316
						array('value' => $this->value)
0 ignored issues
show
Documentation introduced by
array('value' => $this->value) is of type array<string,*,{"value":"*"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
317
					),
318
					"validation"
319
				);
320
				return false;
321
			}
322
		}
323
		return true;
324
	}
325
326
}
327