Completed
Branch master (8ef871)
by
unknown
29:40
created

HTMLCheckMatrix::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
/**
4
 * A checkbox matrix
5
 * Operates similarly to HTMLMultiSelectField, but instead of using an array of
6
 * options, uses an array of rows and an array of columns to dynamically
7
 * construct a matrix of options. The tags used to identify a particular cell
8
 * are of the form "columnName-rowName"
9
 *
10
 * Options:
11
 *   - columns
12
 *     - Required list of columns in the matrix.
13
 *   - rows
14
 *     - Required list of rows in the matrix.
15
 *   - force-options-on
16
 *     - Accepts array of column-row tags to be displayed as enabled but unavailable to change
17
 *   - force-options-off
18
 *     - Accepts array of column-row tags to be displayed as disabled but unavailable to change.
19
 *   - tooltips
20
 *     - Optional array mapping row label to tooltip content
21
 *   - tooltip-class
22
 *     - Optional CSS class used on tooltip container span. Defaults to mw-icon-question.
23
 */
24
class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
25
	static private $requiredParams = [
26
		// Required by underlying HTMLFormField
27
		'fieldname',
28
		// Required by HTMLCheckMatrix
29
		'rows',
30
		'columns'
31
	];
32
33
	public function __construct( $params ) {
34
		$missing = array_diff( self::$requiredParams, array_keys( $params ) );
35
		if ( $missing ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $missing 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...
36
			throw new HTMLFormFieldRequiredOptionsException( $this, $missing );
37
		}
38
		parent::__construct( $params );
39
	}
40
41
	function validate( $value, $alldata ) {
42
		$rows = $this->mParams['rows'];
43
		$columns = $this->mParams['columns'];
44
45
		// Make sure user-defined validation callback is run
46
		$p = parent::validate( $value, $alldata );
47
		if ( $p !== true ) {
48
			return $p;
49
		}
50
51
		// Make sure submitted value is an array
52
		if ( !is_array( $value ) ) {
53
			return false;
54
		}
55
56
		// If all options are valid, array_intersect of the valid options
57
		// and the provided options will return the provided options.
58
		$validOptions = [];
59
		foreach ( $rows as $rowTag ) {
60
			foreach ( $columns as $columnTag ) {
61
				$validOptions[] = $columnTag . '-' . $rowTag;
62
			}
63
		}
64
		$validValues = array_intersect( $value, $validOptions );
65 View Code Duplication
		if ( count( $validValues ) == count( $value ) ) {
66
			return true;
67
		} else {
68
			return $this->msg( 'htmlform-select-badoption' )->parse();
69
		}
70
	}
71
72
	/**
73
	 * Build a table containing a matrix of checkbox options.
74
	 * The value of each option is a combination of the row tag and column tag.
75
	 * mParams['rows'] is an array with row labels as keys and row tags as values.
76
	 * mParams['columns'] is an array with column labels as keys and column tags as values.
77
	 *
78
	 * @param array $value Array of the options that should be checked
79
	 *
80
	 * @return string
81
	 */
82
	function getInputHTML( $value ) {
83
		$html = '';
84
		$tableContents = '';
85
		$rows = $this->mParams['rows'];
86
		$columns = $this->mParams['columns'];
87
88
		$attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
89
90
		// Build the column headers
91
		$headerContents = Html::rawElement( 'td', [], '&#160;' );
92
		foreach ( $columns as $columnLabel => $columnTag ) {
93
			$headerContents .= Html::rawElement( 'td', [], $columnLabel );
94
		}
95
		$tableContents .= Html::rawElement( 'tr', [], "\n$headerContents\n" );
96
97
		$tooltipClass = 'mw-icon-question';
98
		if ( isset( $this->mParams['tooltip-class'] ) ) {
99
			$tooltipClass = $this->mParams['tooltip-class'];
100
		}
101
102
		// Build the options matrix
103
		foreach ( $rows as $rowLabel => $rowTag ) {
104
			// Append tooltip if configured
105
			if ( isset( $this->mParams['tooltips'][$rowLabel] ) ) {
106
				$tooltipAttribs = [
107
					'class' => "mw-htmlform-tooltip $tooltipClass",
108
					'title' => $this->mParams['tooltips'][$rowLabel],
109
				];
110
				$rowLabel .= ' ' . Html::element( 'span', $tooltipAttribs, '' );
111
			}
112
			$rowContents = Html::rawElement( 'td', [], $rowLabel );
113
			foreach ( $columns as $columnTag ) {
114
				$thisTag = "$columnTag-$rowTag";
115
				// Construct the checkbox
116
				$thisAttribs = [
117
					'id' => "{$this->mID}-$thisTag",
118
					'value' => $thisTag,
119
				];
120
				$checked = in_array( $thisTag, (array)$value, true );
121
				if ( $this->isTagForcedOff( $thisTag ) ) {
122
					$checked = false;
123
					$thisAttribs['disabled'] = 1;
124
				} elseif ( $this->isTagForcedOn( $thisTag ) ) {
125
					$checked = true;
126
					$thisAttribs['disabled'] = 1;
127
				}
128
129
				$checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs );
130
131
				$rowContents .= Html::rawElement(
132
					'td',
133
					[],
134
					$checkbox
0 ignored issues
show
Bug introduced by
It seems like $checkbox defined by $this->getOneCheckbox($c...attribs + $thisAttribs) on line 129 can also be of type object<OOUI\CheckboxInputWidget>; however, Html::rawElement() 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...
135
				);
136
			}
137
			$tableContents .= Html::rawElement( 'tr', [], "\n$rowContents\n" );
138
		}
139
140
		// Put it all in a table
141
		$html .= Html::rawElement( 'table',
142
				[ 'class' => 'mw-htmlform-matrix' ],
143
				Html::rawElement( 'tbody', [], "\n$tableContents\n" ) ) . "\n";
144
145
		return $html;
146
	}
147
148
	protected function getOneCheckbox( $checked, $attribs ) {
149
		if ( $this->mParent instanceof OOUIHTMLForm ) {
150
			return new OOUI\CheckboxInputWidget( [
151
				'name' => "{$this->mName}[]",
152
				'selected' => $checked,
153
			] + OOUI\Element::configFromHtmlAttributes(
154
				$attribs
155
			) );
156
		} else {
157
			$checkbox = Xml::check( "{$this->mName}[]", $checked, $attribs );
158
			if ( $this->mParent->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
159
				$checkbox = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
160
					$checkbox .
161
					Html::element( 'label', [ 'for' => $attribs['id'] ] ) .
162
					Html::closeElement( 'div' );
163
			}
164
			return $checkbox;
165
		}
166
	}
167
168
	protected function isTagForcedOff( $tag ) {
169
		return isset( $this->mParams['force-options-off'] )
170
			&& in_array( $tag, $this->mParams['force-options-off'] );
171
	}
172
173
	protected function isTagForcedOn( $tag ) {
174
		return isset( $this->mParams['force-options-on'] )
175
			&& in_array( $tag, $this->mParams['force-options-on'] );
176
	}
177
178
	/**
179
	 * Get the complete table row for the input, including help text,
180
	 * labels, and whatever.
181
	 * We override this function since the label should always be on a separate
182
	 * line above the options in the case of a checkbox matrix, i.e. it's always
183
	 * a "vertical-label".
184
	 *
185
	 * @param string $value The value to set the input to
186
	 *
187
	 * @return string Complete HTML table row
188
	 */
189
	function getTableRow( $value ) {
190
		list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
191
		$inputHtml = $this->getInputHTML( $value );
192
		$fieldType = get_class( $this );
193
		$helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
0 ignored issues
show
Bug introduced by
It seems like $this->getHelpText() targeting HTMLFormField::getHelpText() can also be of type array<integer,?,{"0":"?"}>; however, HTMLFormField::getHelpTextHtmlTable() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that 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...
194
		$cellAttributes = [ 'colspan' => 2 ];
195
196
		$hideClass = '';
197
		$hideAttributes = [];
198
		if ( $this->mHideIf ) {
199
			$hideAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
200
			$hideClass = 'mw-htmlform-hide-if';
201
		}
202
203
		$label = $this->getLabelHtml( $cellAttributes );
204
205
		$field = Html::rawElement(
206
			'td',
207
			[ 'class' => 'mw-input' ] + $cellAttributes,
208
			$inputHtml . "\n$errors"
209
		);
210
211
		$html = Html::rawElement( 'tr',
212
			[ 'class' => "mw-htmlform-vertical-label $hideClass" ] + $hideAttributes,
213
			$label );
214
		$html .= Html::rawElement( 'tr',
215
			[ 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $hideClass" ] +
216
				$hideAttributes,
217
			$field );
218
219
		return $html . $helptext;
220
	}
221
222
	/**
223
	 * @param WebRequest $request
224
	 *
225
	 * @return array
226
	 */
227 View Code Duplication
	function loadDataFromRequest( $request ) {
228
		if ( $this->mParent->getMethod() == 'post' ) {
229
			if ( $request->wasPosted() ) {
230
				// Checkboxes are not added to the request arrays if they're not checked,
231
				// so it's perfectly possible for there not to be an entry at all
232
				return $request->getArray( $this->mName, [] );
233
			} else {
234
				// That's ok, the user has not yet submitted the form, so show the defaults
235
				return $this->getDefault();
236
			}
237
		} else {
238
			// This is the impossible case: if we look at $_GET and see no data for our
239
			// field, is it because the user has not yet submitted the form, or that they
240
			// have submitted it with all the options unchecked. We will have to assume the
241
			// latter, which basically means that you can't specify 'positive' defaults
242
			// for GET forms.
243
			return $request->getArray( $this->mName, [] );
244
		}
245
	}
246
247
	function getDefault() {
248
		if ( isset( $this->mDefault ) ) {
249
			return $this->mDefault;
250
		} else {
251
			return [];
252
		}
253
	}
254
255
	function filterDataForSubmit( $data ) {
256
		$columns = HTMLFormField::flattenOptions( $this->mParams['columns'] );
257
		$rows = HTMLFormField::flattenOptions( $this->mParams['rows'] );
258
		$res = [];
259
		foreach ( $columns as $column ) {
260
			foreach ( $rows as $row ) {
261
				// Make sure option hasn't been forced
262
				$thisTag = "$column-$row";
263
				if ( $this->isTagForcedOff( $thisTag ) ) {
264
					$res[$thisTag] = false;
265
				} elseif ( $this->isTagForcedOn( $thisTag ) ) {
266
					$res[$thisTag] = true;
267
				} else {
268
					$res[$thisTag] = in_array( $thisTag, $data );
269
				}
270
			}
271
		}
272
273
		return $res;
274
	}
275
}
276