HTMLCheckMatrix::getTableRow()   B
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 32
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 24
nc 2
nop 1
dl 0
loc 32
rs 8.8571
c 0
b 0
f 0
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 ) {
36
			throw new HTMLFormFieldRequiredOptionsException( $this, $missing );
37
		}
38
		parent::__construct( $params );
39
	}
40
41
	public 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
	public 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
	public 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
	public function loadDataFromRequest( $request ) {
228
		if ( $this->isSubmitAttempt( $request ) ) {
229
			// Checkboxes are just not added to the request arrays if they're not checked,
230
			// so it's perfectly possible for there not to be an entry at all
231
			return $request->getArray( $this->mName, [] );
232
		} else {
233
			// That's ok, the user has not yet submitted the form, so show the defaults
234
			return $this->getDefault();
235
		}
236
	}
237
238
	public function getDefault() {
239
		if ( isset( $this->mDefault ) ) {
240
			return $this->mDefault;
241
		} else {
242
			return [];
243
		}
244
	}
245
246
	public function filterDataForSubmit( $data ) {
247
		$columns = HTMLFormField::flattenOptions( $this->mParams['columns'] );
248
		$rows = HTMLFormField::flattenOptions( $this->mParams['rows'] );
249
		$res = [];
250
		foreach ( $columns as $column ) {
251
			foreach ( $rows as $row ) {
252
				// Make sure option hasn't been forced
253
				$thisTag = "$column-$row";
254
				if ( $this->isTagForcedOff( $thisTag ) ) {
255
					$res[$thisTag] = false;
256
				} elseif ( $this->isTagForcedOn( $thisTag ) ) {
257
					$res[$thisTag] = true;
258
				} else {
259
					$res[$thisTag] = in_array( $thisTag, $data );
260
				}
261
			}
262
		}
263
264
		return $res;
265
	}
266
}
267