Completed
Push — master ( 66a8d7...54b0e6 )
by
unknown
02:31
created

SpecialConstraintReport::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 53
rs 9.0254
c 0
b 0
f 0
cc 1
nc 1
nop 10

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace WikibaseQuality\ConstraintReport\Specials;
4
5
use Config;
6
use DataValues\DataValue;
7
use Html;
8
use HTMLForm;
9
use IBufferingStatsdDataFactory;
10
use InvalidArgumentException;
11
use MediaWiki\MediaWikiServices;
12
use OOUI\IconWidget;
13
use OOUI\LabelWidget;
14
use SpecialPage;
15
use UnexpectedValueException;
16
use ValueFormatters\FormatterOptions;
17
use ValueFormatters\ValueFormatter;
18
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\MultilingualTextViolationMessageRenderer;
19
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRenderer;
20
use WikibaseQuality\ConstraintReport\ConstraintParameterRenderer;
21
use Wikibase\DataModel\Entity\EntityId;
22
use Wikibase\DataModel\Entity\EntityIdParser;
23
use Wikibase\DataModel\Entity\EntityIdParsingException;
24
use Wikibase\DataModel\Entity\EntityIdValue;
25
use Wikibase\DataModel\Entity\ItemId;
26
use Wikibase\DataModel\Entity\PropertyId;
27
use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
28
use Wikibase\DataModel\Services\Lookup\EntityLookup;
29
use Wikibase\Lib\OutputFormatValueFormatterFactory;
30
use Wikibase\Lib\SnakFormatter;
31
use Wikibase\Lib\Store\EntityTitleLookup;
32
use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookupFactory;
33
use Wikibase\Repo\EntityIdHtmlLinkFormatterFactory;
34
use Wikibase\Repo\EntityIdLabelFormatterFactory;
35
use Wikibase\Repo\WikibaseRepo;
36
use WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker;
37
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
38
use WikibaseQuality\ConstraintReport\ConstraintReportFactory;
39
use WikibaseQuality\Html\HtmlTableBuilder;
40
use WikibaseQuality\Html\HtmlTableCellBuilder;
41
use WikibaseQuality\Html\HtmlTableHeaderBuilder;
42
43
/**
44
 * Special page that displays all constraints that are defined on an Entity with additional information
45
 * (whether it complied or was a violation, which parameters the constraint has etc.).
46
 *
47
 * @author BP2014N1
48
 * @license GPL-2.0-or-later
49
 */
50
class SpecialConstraintReport extends SpecialPage {
51
52
	/**
53
	 * @var EntityIdParser
54
	 */
55
	private $entityIdParser;
56
57
	/**
58
	 * @var EntityLookup
59
	 */
60
	private $entityLookup;
61
62
	/**
63
	 * @var EntityTitleLookup
64
	 */
65
	private $entityTitleLookup;
66
67
	/**
68
	 * @var ValueFormatter
69
	 */
70
	private $dataValueFormatter;
71
72
	/**
73
	 * @var EntityIdFormatter
74
	 */
75
	private $entityIdLabelFormatter;
76
77
	/**
78
	 * @var EntityIdFormatter
79
	 */
80
	private $entityIdLinkFormatter;
81
82
	/**
83
	 * @var DelegatingConstraintChecker
84
	 */
85
	private $constraintChecker;
86
87
	/**
88
	 * @var ConstraintParameterRenderer
89
	 */
90
	private $constraintParameterRenderer;
91
92
	/**
93
	 * @var ViolationMessageRenderer
94
	 */
95
	private $violationMessageRenderer;
96
97
	/**
98
	 * @var Config
99
	 */
100
	private $config;
101
102
	/**
103
	 * @var IBufferingStatsdDataFactory
104
	 */
105
	private $dataFactory;
106
107
	public static function newFromGlobalState() {
108
		$constraintReportFactory = ConstraintReportFactory::getDefaultInstance();
109
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
110
		$config = MediaWikiServices::getInstance()->getMainConfig();
111
		$dataFactory = MediaWikiServices::getInstance()->getStatsdDataFactory();
112
113
		return new self(
114
			$wikibaseRepo->getEntityLookup(),
115
			$wikibaseRepo->getEntityTitleLookup(),
116
			new EntityIdLabelFormatterFactory(),
117
			$wikibaseRepo->getEntityIdHtmlLinkFormatterFactory(),
118
			$wikibaseRepo->getLanguageFallbackLabelDescriptionLookupFactory(),
119
			$wikibaseRepo->getEntityIdParser(),
120
			$wikibaseRepo->getValueFormatterFactory(),
121
			$constraintReportFactory->getConstraintChecker(),
122
			$config,
123
			$dataFactory
124
		);
125
	}
126
127
	public function __construct(
128
		EntityLookup $entityLookup,
129
		EntityTitleLookup $entityTitleLookup,
130
		EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory,
131
		EntityIdHtmlLinkFormatterFactory $entityIdHtmlLinkFormatterFactory,
132
		LanguageFallbackLabelDescriptionLookupFactory $fallbackLabelDescLookupFactory,
133
		EntityIdParser $entityIdParser,
134
		OutputFormatValueFormatterFactory $valueFormatterFactory,
135
		DelegatingConstraintChecker $constraintChecker,
136
		Config $config,
137
		IBufferingStatsdDataFactory $dataFactory
138
	) {
139
		parent::__construct( 'ConstraintReport' );
140
141
		$this->entityLookup = $entityLookup;
142
		$this->entityTitleLookup = $entityTitleLookup;
143
		$this->entityIdParser = $entityIdParser;
144
145
		$language = $this->getLanguage();
146
147
		$formatterOptions = new FormatterOptions();
148
		$formatterOptions->setOption( SnakFormatter::OPT_LANG, $language->getCode() );
149
		$this->dataValueFormatter = $valueFormatterFactory->getValueFormatter(
150
			SnakFormatter::FORMAT_HTML,
151
			$formatterOptions
152
		);
153
154
		$this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter(
155
			$language
156
		);
157
158
		$this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter(
159
			$language
160
		);
161
162
		$this->constraintChecker = $constraintChecker;
163
164
		$this->constraintParameterRenderer = new ConstraintParameterRenderer(
165
			$this->entityIdLabelFormatter,
166
			$this->dataValueFormatter,
167
			$this->getContext(),
168
			$config
169
		);
170
		$this->violationMessageRenderer = new MultilingualTextViolationMessageRenderer(
171
			$this->entityIdLinkFormatter,
172
			$this->dataValueFormatter,
173
			$this->getContext(),
174
			$config
175
		);
176
177
		$this->config = $config;
178
		$this->dataFactory = $dataFactory;
179
	}
180
181
	/**
182
	 * Returns array of modules that should be added
183
	 *
184
	 * @return string[]
185
	 */
186
	private function getModules() {
187
		return [
188
			'SpecialConstraintReportPage',
189
			'wikibase.quality.constraints.icon',
190
		];
191
	}
192
193
	/**
194
	 * @see SpecialPage::getGroupName
195
	 *
196
	 * @return string
197
	 */
198
	protected function getGroupName() {
199
		return 'wikibasequality';
200
	}
201
202
	/**
203
	 * @see SpecialPage::getDescription
204
	 *
205
	 * @return string
206
	 */
207
	public function getDescription() {
208
		return $this->msg( 'wbqc-constraintreport' )->escaped();
209
	}
210
211
	/**
212
	 * @see SpecialPage::execute
213
	 *
214
	 * @param string|null $subPage
215
	 *
216
	 * @throws InvalidArgumentException
217
	 * @throws EntityIdParsingException
218
	 * @throws UnexpectedValueException
219
	 */
220
	public function execute( $subPage ) {
221
		$out = $this->getOutput();
222
223
		$postRequest = $this->getContext()->getRequest()->getVal( 'entityid' );
224
		if ( $postRequest ) {
225
			$out->redirect( $this->getPageTitle( strtoupper( $postRequest ) )->getLocalURL() );
226
			return;
227
		}
228
229
		$out->enableOOUI();
230
		$out->addModules( $this->getModules() );
231
232
		$this->setHeaders();
233
234
		$out->addHTML( $this->getExplanationText() );
235
		$this->buildEntityIdForm();
236
237
		if ( !$subPage ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subPage of type string|null is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
238
			return;
239
		}
240
241
		if ( !is_string( $subPage ) ) {
242
			throw new InvalidArgumentException( '$subPage must be string.' );
243
		}
244
245
		try {
246
			$entityId = $this->entityIdParser->parse( $subPage );
247
		} catch ( EntityIdParsingException $e ) {
248
			$out->addHTML(
249
				$this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true )
250
			);
251
			return;
252
		}
253
254
		if ( !$this->entityLookup->hasEntity( $entityId ) ) {
255
			$out->addHTML(
256
				$this->buildNotice( 'wbqc-constraintreport-not-existent-entity', true )
257
			);
258
			return;
259
		}
260
261
		$this->dataFactory->increment(
262
			'wikibase.quality.constraints.specials.specialConstraintReport.executeCheck'
263
		);
264
		$results = $this->constraintChecker->checkAgainstConstraintsOnEntityId( $entityId );
265
266
		if ( $results !== [] ) {
267
			$out->addHTML(
268
				$this->buildResultHeader( $entityId )
269
				. $this->buildSummary( $results )
270
				. $this->buildResultTable( $entityId, $results )
271
			);
272
		} else {
273
			$out->addHTML(
274
				$this->buildResultHeader( $entityId )
275
				. $this->buildNotice( 'wbqc-constraintreport-empty-result' )
276
			);
277
		}
278
	}
279
280
	/**
281
	 * Builds html form for entity id input
282
	 */
283
	private function buildEntityIdForm() {
284
		$formDescriptor = [
285
			'entityid' => [
286
				'class' => 'HTMLTextField',
287
				'section' => 'section',
288
				'name' => 'entityid',
289
				'label-message' => 'wbqc-constraintreport-form-entityid-label',
290
				'cssclass' => 'wbqc-constraintreport-form-entity-id',
291
				'placeholder' => $this->msg( 'wbqc-constraintreport-form-entityid-placeholder' )->escaped()
292
			]
293
		];
294
		$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext(), 'wbqc-constraintreport-form' );
295
		$htmlForm->setSubmitText( $this->msg( 'wbqc-constraintreport-form-submit-label' )->escaped() );
296
		$htmlForm->setSubmitCallback( function() {
297
			return false;
298
		} );
299
		$htmlForm->setMethod( 'post' );
300
		$htmlForm->show();
301
	}
302
303
	/**
304
	 * Builds notice with given message. Optionally notice can be handles as error by settings $error to true
305
	 *
306
	 * @param string $messageKey
307
	 * @param bool $error
308
	 *
309
	 * @throws InvalidArgumentException
310
	 *
311
	 * @return string HTML
312
	 */
313
	private function buildNotice( $messageKey, $error = false ) {
314
		if ( !is_string( $messageKey ) ) {
315
			throw new InvalidArgumentException( '$message must be string.' );
316
		}
317
		if ( !is_bool( $error ) ) {
318
			throw new InvalidArgumentException( '$error must be bool.' );
319
		}
320
321
		$cssClasses = 'wbqc-constraintreport-notice';
322
		if ( $error ) {
323
			$cssClasses .= ' wbqc-constraintreport-notice-error';
324
		}
325
326
		return Html::rawElement(
327
				'p',
328
				[
329
					'class' => $cssClasses
330
				],
331
				$this->msg( $messageKey )->escaped()
332
			);
333
	}
334
335
	/**
336
	 * @return string HTML
337
	 */
338
	private function getExplanationText() {
339
		return Html::rawElement(
340
			'div',
341
			[ 'class' => 'wbqc-explanation' ],
342
			Html::rawElement(
343
				'p',
344
				[],
345
				$this->msg( 'wbqc-constraintreport-explanation-part-one' )->escaped()
346
			)
347
			. Html::rawElement(
348
				'p',
349
				[],
350
				$this->msg( 'wbqc-constraintreport-explanation-part-two' )->escaped()
351
			)
352
		);
353
	}
354
355
	/**
356
	 * @param EntityId $entityId
357
	 * @param CheckResult[] $results
358
	 *
359
	 * @return string HTML
360
	 */
361
	private function buildResultTable( EntityId $entityId, array $results ) {
362
		// Set table headers
363
		$table = new HtmlTableBuilder(
364
			[
365
				new HtmlTableHeaderBuilder(
366
					$this->msg( 'wbqc-constraintreport-result-table-header-status' )->escaped(),
367
					true
368
				),
369
				new HtmlTableHeaderBuilder(
370
					$this->msg( 'wbqc-constraintreport-result-table-header-property' )->escaped(),
371
					true
372
				),
373
				new HtmlTableHeaderBuilder(
374
					$this->msg( 'wbqc-constraintreport-result-table-header-message' )->escaped(),
375
					true
376
				),
377
				new HtmlTableHeaderBuilder(
378
					$this->msg( 'wbqc-constraintreport-result-table-header-constraint' )->escaped(),
379
					true
380
				)
381
			]
382
		);
383
384
		foreach ( $results as $result ) {
385
			$table = $this->appendToResultTable( $table, $entityId, $result );
386
		}
387
388
		return $table->toHtml();
389
	}
390
391
	private function appendToResultTable( HtmlTableBuilder $table, EntityId $entityId, CheckResult $result ) {
392
		$message = $result->getMessage();
393
		if ( $message === null ) {
394
			// no row for this result
395
			return $table;
396
		}
397
398
		// Status column
399
		$statusColumn = $this->formatStatus( $result->getStatus() );
400
401
		// Property column
402
		$propertyId = new PropertyId( $result->getContextCursor()->getSnakPropertyId() );
403
		$propertyColumn = $this->getClaimLink(
404
			$entityId,
405
			$propertyId,
406
			$this->entityIdLabelFormatter->formatEntityId( $propertyId )
407
		);
408
409
		// Message column
410
		$messageColumn = $this->violationMessageRenderer->render( $message );
411
412
		// Constraint column
413
		$constraintTypeItemId = $result->getConstraint()->getConstraintTypeItemId();
414
		try {
415
			$constraintTypeLabel = $this->entityIdLabelFormatter->formatEntityId( new ItemId( $constraintTypeItemId ) );
416
		} catch ( InvalidArgumentException $e ) {
417
			$constraintTypeLabel = htmlspecialchars( $constraintTypeItemId );
418
		}
419
		$constraintLink = $this->getClaimLink(
420
			$propertyId,
421
			new PropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) ),
422
			$constraintTypeLabel
423
		);
424
		$constraintColumn = $this->buildExpandableElement(
425
			$constraintLink,
426
			$this->constraintParameterRenderer->formatParameters( $result->getParameters() ),
0 ignored issues
show
Documentation introduced by
$result->getParameters() is of type array<integer,array>, but the function expects a array<integer,string|obj...Values\DataValue>>|null.

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...
427
			'[...]'
428
		);
429
430
		// Append cells
431
		$table->appendRow(
432
			[
433
				new HtmlTableCellBuilder(
434
					$statusColumn,
435
					[],
436
					true
437
				),
438
				new HtmlTableCellBuilder(
439
					$propertyColumn,
440
					[],
441
					true
442
				),
443
				new HtmlTableCellBuilder(
444
					$messageColumn,
445
					[],
446
					true
447
				),
448
				new HtmlTableCellBuilder(
449
					$constraintColumn,
450
					[],
451
					true
452
				)
453
			]
454
		);
455
456
		return $table;
457
	}
458
459
	/**
460
	 * Returns html text of the result header
461
	 *
462
	 * @param EntityId $entityId
463
	 *
464
	 * @return string HTML
465
	 */
466
	protected function buildResultHeader( EntityId $entityId ) {
467
		$entityLink = sprintf( '%s (%s)',
468
							   $this->entityIdLinkFormatter->formatEntityId( $entityId ),
469
							   htmlspecialchars( $entityId->getSerialization() ) );
470
471
		return Html::rawElement(
472
			'h3',
473
			[],
474
			sprintf( '%s %s', $this->msg( 'wbqc-constraintreport-result-headline' )->escaped(), $entityLink )
475
		);
476
	}
477
478
	/**
479
	 * Builds summary from given results
480
	 *
481
	 * @param CheckResult[] $results
482
	 *
483
	 * @return string HTML
484
	 */
485
	protected function buildSummary( array $results ) {
486
		$statuses = [];
487
		foreach ( $results as $result ) {
488
			$status = strtolower( $result->getStatus() );
489
			$statuses[$status] = isset( $statuses[$status] ) ? $statuses[$status] + 1 : 1;
490
		}
491
492
		$statusElements = [];
493
		foreach ( $statuses as $status => $count ) {
494
			if ( $count > 0 ) {
495
				$statusElements[] =
496
					$this->formatStatus( $status )
497
					. ': '
498
					. $count;
499
			}
500
		}
501
502
		return Html::rawElement( 'p', [], implode( ', ', $statusElements ) );
503
	}
504
505
	/**
506
	 * Builds a html div element with given content and a tooltip with given tooltip content
507
	 * If $tooltipContent is null, no tooltip will be created
508
	 *
509
	 * @param string $content
510
	 * @param string $expandableContent
511
	 * @param string $indicator
512
	 *
513
	 * @throws InvalidArgumentException
514
	 *
515
	 * @return string HTML
516
	 */
517
	protected function buildExpandableElement( $content, $expandableContent, $indicator ) {
518
		if ( !is_string( $content ) ) {
519
			throw new InvalidArgumentException( '$content has to be string.' );
520
		}
521
		if ( $expandableContent && ( !is_string( $expandableContent ) ) ) {
522
			throw new InvalidArgumentException( '$tooltipContent, if provided, has to be string.' );
523
		}
524
525
		if ( empty( $expandableContent ) ) {
526
			return $content;
527
		}
528
529
		$tooltipIndicator = Html::element(
530
			'span',
531
			[
532
				'class' => 'wbqc-expandable-content-indicator wbqc-indicator'
533
			],
534
			$indicator
535
		);
536
537
		$expandableContent = Html::element(
538
			'div',
539
			[
540
				'class' => 'wbqc-expandable-content'
541
			],
542
			$expandableContent
543
		);
544
545
		return sprintf( '%s %s %s', $content, $tooltipIndicator, $expandableContent );
546
	}
547
548
	/**
549
	 * Formats given status to html
550
	 *
551
	 * @param string $status
552
	 *
553
	 * @throws InvalidArgumentException
554
	 *
555
	 * @return string HTML
556
	 */
557
	private function formatStatus( $status ) {
558
		$messageName = "wbqc-constraintreport-status-" . strtolower( $status );
559
		$statusIcons = [
560
			CheckResult::STATUS_WARNING => [
561
				'icon' => 'non-mandatory-constraint-violation',
562
			],
563
			CheckResult::STATUS_VIOLATION => [
564
				'icon' => 'mandatory-constraint-violation',
565
			],
566
			CheckResult::STATUS_BAD_PARAMETERS => [
567
				'icon' => 'alert',
568
				'flags' => 'warning',
569
			],
570
		];
571
572
		if ( array_key_exists( $status, $statusIcons ) ) {
573
			$iconWidget = new IconWidget( $statusIcons[$status] );
574
			$iconHtml = $iconWidget->toString() . ' ';
575
		} else {
576
			$iconHtml = '';
577
		}
578
579
		$labelWidget = new LabelWidget( [
580
			'label' => $this->msg( $messageName )->text(),
581
		] );
582
		$labelHtml = $labelWidget->toString();
583
584
		$formattedStatus =
585
			Html::rawElement(
586
				'span',
587
				[
588
					'class' => 'wbqc-status wbqc-status-' . $status
589
				],
590
				$iconHtml . $labelHtml
591
			);
592
593
		return $formattedStatus;
594
	}
595
596
	/**
597
	 * Parses data values to human-readable string
598
	 *
599
	 * @param DataValue|array $dataValues
600
	 * @param string $separator
601
	 *
602
	 * @throws InvalidArgumentException
603
	 *
604
	 * @return string HTML
605
	 */
606
	protected function formatDataValues( $dataValues, $separator = ', ' ) {
607
		if ( $dataValues instanceof DataValue ) {
608
			$dataValues = [ $dataValues ];
609
		} elseif ( !is_array( $dataValues ) ) {
610
			throw new InvalidArgumentException( '$dataValues has to be instance of DataValue or an array of DataValues.' );
611
		}
612
613
		$formattedDataValues = [];
614
		foreach ( $dataValues as $dataValue ) {
615
			if ( !( $dataValue instanceof DataValue ) ) {
616
				throw new InvalidArgumentException( '$dataValues has to be instance of DataValue or an array of DataValues.' );
617
			}
618
			if ( $dataValue instanceof EntityIdValue ) {
619
				$formattedDataValues[ ] = $this->entityIdLabelFormatter->formatEntityId( $dataValue->getEntityId() );
620
			} else {
621
				$formattedDataValues[ ] = $this->dataValueFormatter->format( $dataValue );
622
			}
623
		}
624
625
		return implode( $separator, $formattedDataValues );
626
	}
627
628
	/**
629
	 * Returns html link to given entity with anchor to specified property.
630
	 *
631
	 * @param EntityId $entityId
632
	 * @param PropertyId $propertyId
633
	 * @param string $text HTML
634
	 *
635
	 * @return string HTML
636
	 */
637
	private function getClaimLink( EntityId $entityId, PropertyId $propertyId, $text ) {
638
		return Html::rawElement(
639
			'a',
640
			[
641
				'href' => $this->getClaimUrl( $entityId, $propertyId ),
642
				'target' => '_blank'
643
			],
644
			$text
645
		);
646
	}
647
648
	/**
649
	 * Returns url of given entity with anchor to specified property.
650
	 *
651
	 * @param EntityId $entityId
652
	 * @param PropertyId $propertyId
653
	 *
654
	 * @return string
655
	 */
656
	private function getClaimUrl( EntityId $entityId, PropertyId $propertyId ) {
657
		$title = $this->entityTitleLookup->getTitleForId( $entityId );
658
		$entityUrl = sprintf( '%s#%s', $title->getLocalURL(), $propertyId->getSerialization() );
659
660
		return $entityUrl;
661
	}
662
663
}
664