Completed
Push — master ( a96889...4a099b )
by
unknown
02:45
created

ViolationMessageRenderer::msgEscaped()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Message;
4
5
use Config;
6
use DataValues\DataValue;
7
use InvalidArgumentException;
8
use Language;
9
use LogicException;
10
use Message;
11
use MessageLocalizer;
12
use ValueFormatters\ValueFormatter;
13
use Wikibase\DataModel\Entity\EntityId;
14
use Wikibase\DataModel\Entity\ItemId;
15
use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
16
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
17
use WikibaseQuality\ConstraintReport\ConstraintCheck\ItemIdSnakValue;
18
19
/**
20
 * Render a {@link ViolationMessage} into a localized string.
21
 *
22
 * Note: This class does <em>not</em> support multilingual text arguments –
23
 * for that, use {@link MultilingualTextViolationMessageRenderer}.
24
 *
25
 * @license GPL-2.0-or-later
26
 */
27
class ViolationMessageRenderer {
28
29
	/**
30
	 * @var EntityIdFormatter
31
	 */
32
	private $entityIdFormatter;
33
34
	/**
35
	 * @var ValueFormatter
36
	 */
37
	private $dataValueFormatter;
38
39
	/**
40
	 * @var MessageLocalizer
41
	 */
42
	protected $messageLocalizer;
43
44
	/**
45
	 * @var Config
46
	 */
47
	private $config;
48
49
	/**
50
	 * @var int
51
	 */
52
	private $maxListLength;
53
54
	/**
55
	 * @param EntityIdFormatter $entityIdFormatter
56
	 * @param ValueFormatter $dataValueFormatter
57
	 * @param MessageLocalizer $messageLocalizer
58
	 * @param Config $config
59
	 * @param int $maxListLength The maximum number of elements to be rendered in a list parameter.
60
	 * Longer lists are truncated to this length and then rendered with an ellipsis in the HMTL list.
61
	 */
62
	public function __construct(
63
		EntityIdFormatter $entityIdFormatter,
64
		ValueFormatter $dataValueFormatter,
65
		MessageLocalizer $messageLocalizer,
66
		Config $config,
67
		$maxListLength = 10
68
	) {
69
		$this->entityIdFormatter = $entityIdFormatter;
70
		$this->dataValueFormatter = $dataValueFormatter;
71
		$this->messageLocalizer = $messageLocalizer;
72
		$this->config = $config;
73
		$this->maxListLength = $maxListLength;
74
	}
75
76
	/**
77
	 * @param ViolationMessage|string $violationMessage
78
	 * (temporarily, pre-rendered strings are allowed and returned without changes)
79
	 * @return string
80
	 */
81
	public function render( $violationMessage ) {
82
		if ( is_string( $violationMessage ) ) {
83
			// TODO remove this once all checkers produce ViolationMessage objects
84
			return $violationMessage;
85
		}
86
87
		$messageKey = $violationMessage->getMessageKey();
88
		$paramsLists = [ [] ];
89
		foreach ( $violationMessage->getArguments() as $argument ) {
90
			$params = $this->renderArgument( $argument );
91
			$paramsLists[] = $params;
92
		}
93
		$allParams = call_user_func_array( 'array_merge', $paramsLists );
94
		return $this->messageLocalizer
95
			->msg( $messageKey )
96
			->params( $allParams )
97
			->escaped();
98
	}
99
100
	/**
101
	 * @param string $value HTML
102
	 * @param string|null $role one of the Role::* constants
103
	 * @return string HTML
104
	 */
105
	protected function addRole( $value, $role ) {
106
		if ( $role === null ) {
107
			return $value;
108
		}
109
110
		return '<span class="wbqc-role wbqc-role-' . htmlspecialchars( $role ) . '">' .
111
			$value .
112
			'</span>';
113
	}
114
115
	/**
116
	 * @param string $key message key
117
	 * @return string HTML
118
	 */
119
	protected function msgEscaped( $key ) {
120
		return $this->messageLocalizer->msg( $key )->escaped();
121
	}
122
123
	/**
124
	 * @param array $argument
125
	 * @return array[] params (for Message::params)
126
	 */
127
	protected function renderArgument( array $argument ) {
128
		$methods = [
129
			ViolationMessage::TYPE_ENTITY_ID => 'renderEntityId',
130
			ViolationMessage::TYPE_ENTITY_ID_LIST => 'renderEntityIdList',
131
			ViolationMessage::TYPE_ITEM_ID_SNAK_VALUE => 'renderItemIdSnakValue',
132
			ViolationMessage::TYPE_ITEM_ID_SNAK_VALUE_LIST => 'renderItemIdSnakValueList',
133
			ViolationMessage::TYPE_DATA_VALUE => 'renderDataValue',
134
			ViolationMessage::TYPE_DATA_VALUE_TYPE => 'renderDataValueType',
135
			ViolationMessage::TYPE_INLINE_CODE => 'renderInlineCode',
136
			ViolationMessage::TYPE_CONSTRAINT_SCOPE => 'renderConstraintScope',
137
			ViolationMessage::TYPE_CONSTRAINT_SCOPE_LIST => 'renderConstraintScopeList',
138
			ViolationMessage::TYPE_LANGUAGE => 'renderLanguage',
139
		];
140
141
		$type = $argument['type'];
142
		$value = $argument['value'];
143
		$role = $argument['role'];
144
145 View Code Duplication
		if ( array_key_exists( $type, $methods ) ) {
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...
146
			$method = $methods[$type];
147
			$params = $this->$method( $value, $role );
148
		} else {
149
			throw new InvalidArgumentException(
150
				'Unknown ViolationMessage argument type ' . $type . '!'
151
			);
152
		}
153
154
		return $params;
155
	}
156
157
	/**
158
	 * @param array $list
159
	 * @param string|null $role one of the Role::* constants
160
	 * @param callable $render must accept $list elements and $role as parameters
161
	 * and return a single-element array with a raw message param (i. e. [ Message::rawParam( … ) ])
162
	 * @return array[] list of parameters as accepted by Message::params()
163
	 */
164
	private function renderList( array $list, $role, callable $render ) {
165
		if ( $list === [] ) {
166
			return [
167
				Message::numParam( 0 ),
168
				Message::rawParam( '<ul></ul>' ),
169
			];
170
		}
171
172
		if ( count( $list ) > $this->maxListLength ) {
173
			$list = array_slice( $list, 0, $this->maxListLength );
174
			$truncated = true;
175
		}
176
177
		$renderedParamsLists = array_map(
178
			$render,
179
			$list,
180
			array_fill( 0, count( $list ), $role )
181
		);
182
		$renderedParams = array_map(
183
			function ( $params ) {
184
				return $params[0];
185
			},
186
			$renderedParamsLists
187
		);
188
		$renderedElements = array_map(
189
			function ( $param ) {
190
				return $param['raw'];
191
			},
192
			$renderedParams
193
		);
194
		if ( isset( $truncated ) ) {
195
			$renderedElements[] = $this->msgEscaped( 'ellipsis' );
196
		}
197
198
		return array_merge(
199
			[
200
				Message::numParam( count( $list ) ),
201
				Message::rawParam(
202
					'<ul><li>' .
203
					implode( '</li><li>', $renderedElements ) .
204
					'</li></ul>'
205
				),
206
			],
207
			$renderedParams
208
		);
209
	}
210
211
	/**
212
	 * @param EntityId $entityId
213
	 * @param string|null $role one of the Role::* constants
214
	 * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
215
	 */
216
	private function renderEntityId( EntityId $entityId, $role ) {
217
		return [ Message::rawParam( $this->addRole(
218
			$this->entityIdFormatter->formatEntityId( $entityId ),
219
			$role
220
		) ) ];
221
	}
222
223
	/**
224
	 * @param EntityId[] $entityIdList
225
	 * @param string|null $role one of the Role::* constants
226
	 * @return array[] list of parameters as accepted by Message::params()
227
	 */
228
	private function renderEntityIdList( array $entityIdList, $role ) {
229
		return $this->renderList( $entityIdList, $role, [ $this, 'renderEntityId' ] );
230
	}
231
232
	/**
233
	 * @param ItemIdSnakValue $value
234
	 * @param string|null $role one of the Role::* constants
235
	 * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
236
	 */
237
	private function renderItemIdSnakValue( ItemIdSnakValue $value, $role ) {
238
		switch ( true ) {
239
			case $value->isValue():
240
				return $this->renderEntityId( $value->getItemId(), $role );
0 ignored issues
show
Bug introduced by
It seems like $value->getItemId() can be null; however, renderEntityId() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
241 View Code Duplication
			case $value->isSomeValue():
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...
242
				return [ Message::rawParam( $this->addRole(
243
					'<span class="wikibase-snakview-variation-somevaluesnak">' .
244
						$this->msgEscaped( 'wikibase-snakview-snaktypeselector-somevalue' ) .
245
						'</span>',
246
					$role
247
				) ) ];
248 View Code Duplication
			case $value->isNoValue():
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...
249
				return [ Message::rawParam( $this->addRole(
250
					'<span class="wikibase-snakview-variation-novaluesnak">' .
251
					$this->msgEscaped( 'wikibase-snakview-snaktypeselector-novalue' ) .
252
						'</span>',
253
					$role
254
				) ) ];
255
			default:
256
				// @codeCoverageIgnoreStart
257
				throw new LogicException(
258
					'ItemIdSnakValue should guarantee that one of is{,Some,No}Value() is true'
259
				);
260
				// @codeCoverageIgnoreEnd
261
		}
262
	}
263
264
	/**
265
	 * @param ItemIdSnakValue[] $valueList
266
	 * @param string|null $role one of the Role::* constants
267
	 * @return array[] list of parameters as accepted by Message::params()
268
	 */
269
	private function renderItemIdSnakValueList( array $valueList, $role ) {
270
		return $this->renderList( $valueList, $role, [ $this, 'renderItemIdSnakValue' ] );
271
	}
272
273
	/**
274
	 * @param DataValue $dataValue
275
	 * @param string|null $role one of the Role::* constants
276
	 * @return array[] list of parameters as accepted by Message::params()
277
	 */
278
	private function renderDataValue( DataValue $dataValue, $role ) {
279
		return [ Message::rawParam( $this->addRole(
280
			$this->dataValueFormatter->format( $dataValue ),
281
			$role
282
		) ) ];
283
	}
284
285
	/**
286
	 * @param string $dataValueType
287
	 * @param string|null $role one of the Role::* constants
288
	 * @return array[] list of parameters as accepted by Message::params()
289
	 */
290
	private function renderDataValueType( $dataValueType, $role ) {
291
		$messageKeys = [
292
			'string' => 'datatypes-type-string',
293
			'monolingualtext' => 'datatypes-type-monolingualtext',
294
			'time' => 'datatypes-type-time',
295
			'quantity' => 'datatypes-type-quantity',
296
			'wikibase-entityid' => 'wbqc-dataValueType-wikibase-entityid',
297
		];
298
299
		if ( array_key_exists( $dataValueType, $messageKeys ) ) {
300
			return [ Message::rawParam( $this->addRole(
301
				$this->msgEscaped( $messageKeys[$dataValueType] ),
302
				$role
303
			) ) ];
304
		} else {
305
			// @codeCoverageIgnoreStart
306
			throw new LogicException(
307
				'Unknown data value type ' . $dataValueType
308
			);
309
			// @codeCoverageIgnoreEnd
310
		}
311
	}
312
313
	/**
314
	 * @param string $code (not yet HTML-escaped)
315
	 * @param string|null $role one of the Role::* constants
316
	 * @return array[] list of parameters as accepted by Message::params()
317
	 */
318
	private function renderInlineCode( $code, $role ) {
319
		return [ Message::rawParam( $this->addRole(
320
			'<code>' . htmlspecialchars( $code ) . '</code>',
321
			$role
322
		) ) ];
323
	}
324
325
	/**
326
	 * @param string $scope one of the Context::TYPE_* constants
327
	 * @param string|null $role one of the Role::* constants
328
	 * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
329
	 */
330
	private function renderConstraintScope( $scope, $role ) {
331
		switch ( $scope ) {
332
			case Context::TYPE_STATEMENT:
333
				$itemId = $this->config->get(
334
					'WBQualityConstraintsConstraintCheckedOnMainValueId'
335
				);
336
				break;
337
			case Context::TYPE_QUALIFIER:
338
				$itemId = $this->config->get(
339
					'WBQualityConstraintsConstraintCheckedOnQualifiersId'
340
				);
341
				break;
342
			case Context::TYPE_REFERENCE:
343
				$itemId = $this->config->get(
344
					'WBQualityConstraintsConstraintCheckedOnReferencesId'
345
				);
346
				break;
347
			default:
348
				// callers should never let this happen, but if it does happen,
349
				// showing “unknown value” seems reasonable
350
				// @codeCoverageIgnoreStart
351
				return $this->renderItemIdSnakValue( ItemIdSnakValue::someValue(), $role );
352
				// @codeCoverageIgnoreEnd
353
		}
354
		return $this->renderEntityId( new ItemId( $itemId ), $role );
355
	}
356
357
	/**
358
	 * @param string[] $text Context::TYPE_* constants
0 ignored issues
show
Bug introduced by
There is no parameter named $text. 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...
359
	 * @param string|null $role one of the Role::* constants
360
	 * @return array[] list of parameters as accepted by Message::params()
361
	 */
362
	private function renderConstraintScopeList( array $scopeList, $role ) {
363
		return $this->renderList( $scopeList, $role, [ $this, 'renderConstraintScope' ] );
364
	}
365
366
	/**
367
	 * @param string $languageCode MediaWiki language code
368
	 * @param string|null $role one of the Role::* constants
369
	 * @return array[] list of parameters as accepted by Message::params()
370
	 */
371
	private function renderLanguage( $languageCode, $role ) {
0 ignored issues
show
Unused Code introduced by
The parameter $role is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
372
		return [
373
			Message::plaintextParam( Language::fetchLanguageName( $languageCode ) ),
374
			Message::plaintextParam( $languageCode ),
375
		];
376
	}
377
378
}
379