Completed
Push — master ( 9ce8bc...16ffe4 )
by
unknown
02:18
created

SpecialConstraintReport::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 55
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 55
rs 9.7692
c 0
b 0
f 0
cc 1
eloc 39
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
		$labelLookup = $fallbackLabelDescLookupFactory->newLabelDescriptionLookup( $language );
155
156
		$this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter(
157
			$labelLookup
158
		);
159
160
		$this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter(
161
			$labelLookup
162
		);
163
164
		$this->constraintChecker = $constraintChecker;
165
166
		$this->constraintParameterRenderer = new ConstraintParameterRenderer(
167
			$this->entityIdLabelFormatter,
168
			$this->dataValueFormatter,
169
			$this->getContext(),
170
			$config
171
		);
172
		$this->violationMessageRenderer = new MultilingualTextViolationMessageRenderer(
173
			$this->entityIdLinkFormatter,
174
			$this->dataValueFormatter,
175
			$this->getContext(),
176
			$config
177
		);
178
179
		$this->config = $config;
180
		$this->dataFactory = $dataFactory;
181
	}
182
183
	/**
184
	 * Returns array of modules that should be added
185
	 *
186
	 * @return array
187
	 */
188
	private function getModules() {
189
		return [
190
			'SpecialConstraintReportPage',
191
			'wikibase.quality.constraints.icon',
192
		];
193
	}
194
195
	/**
196
	 * @see SpecialPage::getGroupName
197
	 *
198
	 * @return string
199
	 */
200
	protected function getGroupName() {
201
		return 'wikibasequality';
202
	}
203
204
	/**
205
	 * @see SpecialPage::getDescription
206
	 *
207
	 * @return string
208
	 */
209
	public function getDescription() {
210
		return $this->msg( 'wbqc-constraintreport' )->escaped();
211
	}
212
213
	/**
214
	 * @see SpecialPage::execute
215
	 *
216
	 * @param string|null $subPage
217
	 *
218
	 * @throws InvalidArgumentException
219
	 * @throws EntityIdParsingException
220
	 * @throws UnexpectedValueException
221
	 */
222
	public function execute( $subPage ) {
223
		$out = $this->getOutput();
224
225
		$postRequest = $this->getContext()->getRequest()->getVal( 'entityid' );
226
		if ( $postRequest ) {
227
			$out->redirect( $this->getPageTitle( strtoupper( $postRequest ) )->getLocalURL() );
228
			return;
229
		}
230
231
		$out->enableOOUI();
232
		$out->addModules( $this->getModules() );
233
234
		$this->setHeaders();
235
236
		$out->addHTML( $this->getExplanationText() );
237
		$this->buildEntityIdForm();
238
239
		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...
240
			return;
241
		}
242
243
		if ( !is_string( $subPage ) ) {
244
			throw new InvalidArgumentException( '$subPage must be string.' );
245
		}
246
247
		try {
248
			$entityId = $this->entityIdParser->parse( $subPage );
249
		} catch ( EntityIdParsingException $e ) {
250
			$out->addHTML(
251
				$this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true )
252
			);
253
			return;
254
		}
255
256
		if ( !$this->entityLookup->hasEntity( $entityId ) ) {
257
			$out->addHTML(
258
				$this->buildNotice( 'wbqc-constraintreport-not-existent-entity', true )
259
			);
260
			return;
261
		}
262
263
		$this->dataFactory->increment(
264
			'wikibase.quality.constraints.specials.specialConstraintReport.executeCheck'
265
		);
266
		$results = $this->constraintChecker->checkAgainstConstraintsOnEntityId( $entityId );
267
268
		if ( count( $results ) > 0 ) {
269
			$out->addHTML(
270
				$this->buildResultHeader( $entityId )
271
				. $this->buildSummary( $results )
272
				. $this->buildResultTable( $entityId, $results )
273
			);
274
		} else {
275
			$out->addHTML(
276
				$this->buildResultHeader( $entityId )
277
				. $this->buildNotice( 'wbqc-constraintreport-empty-result' )
278
			);
279
		}
280
	}
281
282
	/**
283
	 * Builds html form for entity id input
284
	 */
285
	private function buildEntityIdForm() {
286
		$formDescriptor = [
287
			'entityid' => [
288
				'class' => 'HTMLTextField',
289
				'section' => 'section',
290
				'name' => 'entityid',
291
				'label-message' => 'wbqc-constraintreport-form-entityid-label',
292
				'cssclass' => 'wbqc-constraintreport-form-entity-id',
293
				'placeholder' => $this->msg( 'wbqc-constraintreport-form-entityid-placeholder' )->escaped()
294
			]
295
		];
296
		$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext(), 'wbqc-constraintreport-form' );
297
		$htmlForm->setSubmitText( $this->msg( 'wbqc-constraintreport-form-submit-label' )->escaped() );
298
		$htmlForm->setSubmitCallback( function() {
299
			return false;
300
		} );
301
		$htmlForm->setMethod( 'post' );
302
		$htmlForm->show();
303
	}
304
305
	/**
306
	 * Builds notice with given message. Optionally notice can be handles as error by settings $error to true
307
	 *
308
	 * @param string $messageKey
309
	 * @param bool $error
310
	 *
311
	 * @throws InvalidArgumentException
312
	 *
313
	 * @return string HTML
314
	 */
315
	private function buildNotice( $messageKey, $error = false ) {
316
		if ( !is_string( $messageKey ) ) {
317
			throw new InvalidArgumentException( '$message must be string.' );
318
		}
319
		if ( !is_bool( $error ) ) {
320
			throw new InvalidArgumentException( '$error must be bool.' );
321
		}
322
323
		$cssClasses = 'wbqc-constraintreport-notice';
324
		if ( $error ) {
325
			$cssClasses .= ' wbqc-constraintreport-notice-error';
326
		}
327
328
		return Html::rawElement(
329
				'p',
330
				[
331
					'class' => $cssClasses
332
				],
333
				$this->msg( $messageKey )->escaped()
334
			);
335
	}
336
337
	/**
338
	 * @return string HTML
339
	 */
340
	private function getExplanationText() {
341
		return Html::rawElement(
342
			'div',
343
			[ 'class' => 'wbqc-explanation' ],
344
			Html::rawElement(
345
				'p',
346
				[],
347
				$this->msg( 'wbqc-constraintreport-explanation-part-one' )->escaped()
348
			)
349
			. Html::rawElement(
350
				'p',
351
				[],
352
				$this->msg( 'wbqc-constraintreport-explanation-part-two' )->escaped()
353
			)
354
		);
355
	}
356
357
	/**
358
	 * @param EntityId $entityId
359
	 * @param CheckResult[] $results
360
	 *
361
	 * @return string HTML
362
	 */
363
	private function buildResultTable( EntityId $entityId, array $results ) {
364
		// Set table headers
365
		$table = new HtmlTableBuilder(
366
			[
367
				new HtmlTableHeaderBuilder(
368
					$this->msg( 'wbqc-constraintreport-result-table-header-status' )->escaped(),
369
					true
370
				),
371
				new HtmlTableHeaderBuilder(
372
					$this->msg( 'wbqc-constraintreport-result-table-header-property' )->escaped(),
373
					true
374
				),
375
				new HtmlTableHeaderBuilder(
376
					$this->msg( 'wbqc-constraintreport-result-table-header-message' )->escaped(),
377
					true
378
				),
379
				new HtmlTableHeaderBuilder(
380
					$this->msg( 'wbqc-constraintreport-result-table-header-constraint' )->escaped(),
381
					true
382
				)
383
			]
384
		);
385
386
		foreach ( $results as $result ) {
387
			$table = $this->appendToResultTable( $table, $entityId, $result );
388
		}
389
390
		return $table->toHtml();
391
	}
392
393
	private function appendToResultTable( HtmlTableBuilder $table, EntityId $entityId, CheckResult $result ) {
394
		$message = $result->getMessage();
395
		if ( $message === null ) {
396
			// no row for this result
397
			return $table;
398
		}
399
400
		// Status column
401
		$statusColumn = $this->formatStatus( $result->getStatus() );
402
403
		// Property column
404
		$propertyId = new PropertyId( $result->getContextCursor()->getSnakPropertyId() );
405
		$propertyColumn = $this->getClaimLink(
406
			$entityId,
407
			$propertyId,
408
			$this->entityIdLabelFormatter->formatEntityId( $propertyId )
409
		);
410
411
		// Message column
412
		$messageColumn = $this->violationMessageRenderer->render( $message );
413
414
		// Constraint column
415
		$constraintTypeItemId = $result->getConstraint()->getConstraintTypeItemId();
416
		try {
417
			$constraintTypeLabel = $this->entityIdLabelFormatter->formatEntityId( new ItemId( $constraintTypeItemId ) );
418
		} catch ( InvalidArgumentException $e ) {
419
			$constraintTypeLabel = htmlspecialchars( $constraintTypeItemId );
420
		}
421
		$constraintLink = $this->getClaimLink(
422
			$propertyId,
423
			new PropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) ),
424
			$constraintTypeLabel
425
		);
426
		$constraintColumn = $this->buildExpandableElement(
427
			$constraintLink,
428
			$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...
429
			'[...]'
430
		);
431
432
		// Append cells
433
		$table->appendRow(
434
			[
435
				new HtmlTableCellBuilder(
436
					$statusColumn,
437
					[],
438
					true
439
				),
440
				new HtmlTableCellBuilder(
441
					$propertyColumn,
442
					[],
443
					true
444
				),
445
				new HtmlTableCellBuilder(
446
					$messageColumn,
447
					[],
448
					true
449
				),
450
				new HtmlTableCellBuilder(
451
					$constraintColumn,
452
					[],
453
					true
454
				)
455
			]
456
		);
457
458
		return $table;
459
	}
460
461
	/**
462
	 * Returns html text of the result header
463
	 *
464
	 * @param EntityId $entityId
465
	 *
466
	 * @return string HTML
467
	 */
468
	protected function buildResultHeader( EntityId $entityId ) {
469
		$entityLink = sprintf( '%s (%s)',
470
							   $this->entityIdLinkFormatter->formatEntityId( $entityId ),
471
							   htmlspecialchars( $entityId->getSerialization() ) );
472
473
		return Html::rawElement(
474
			'h3',
475
			[],
476
			sprintf( '%s %s', $this->msg( 'wbqc-constraintreport-result-headline' )->escaped(), $entityLink )
477
		);
478
	}
479
480
	/**
481
	 * Builds summary from given results
482
	 *
483
	 * @param CheckResult[] $results
484
	 *
485
	 * @return string HTML
486
	 */
487
	protected function buildSummary( array $results ) {
488
		$statuses = [];
489
		foreach ( $results as $result ) {
490
			$status = strtolower( $result->getStatus() );
491
			$statuses[$status] = isset( $statuses[$status] ) ? $statuses[$status] + 1 : 1;
492
		}
493
494
		$statusElements = [];
495
		foreach ( $statuses as $status => $count ) {
496
			if ( $count > 0 ) {
497
				$statusElements[] =
498
					$this->formatStatus( $status )
499
					. ': '
500
					. $count;
501
			}
502
		}
503
504
		return Html::rawElement( 'p', [], implode( ', ', $statusElements ) );
505
	}
506
507
	/**
508
	 * Builds a html div element with given content and a tooltip with given tooltip content
509
	 * If $tooltipContent is null, no tooltip will be created
510
	 *
511
	 * @param string $content
512
	 * @param string $expandableContent
513
	 * @param string $indicator
514
	 *
515
	 * @throws InvalidArgumentException
516
	 *
517
	 * @return string HTML
518
	 */
519
	protected function buildExpandableElement( $content, $expandableContent, $indicator ) {
520
		if ( !is_string( $content ) ) {
521
			throw new InvalidArgumentException( '$content has to be string.' );
522
		}
523
		if ( $expandableContent && ( !is_string( $expandableContent ) ) ) {
524
			throw new InvalidArgumentException( '$tooltipContent, if provided, has to be string.' );
525
		}
526
527
		if ( empty( $expandableContent ) ) {
528
			return $content;
529
		}
530
531
		$tooltipIndicator = Html::element(
532
			'span',
533
			[
534
				'class' => 'wbqc-expandable-content-indicator wbqc-indicator'
535
			],
536
			$indicator
537
		);
538
539
		$expandableContent = Html::element(
540
			'div',
541
			[
542
				'class' => 'wbqc-expandable-content'
543
			],
544
			$expandableContent
545
		);
546
547
		return sprintf( '%s %s %s', $content, $tooltipIndicator, $expandableContent );
548
	}
549
550
	/**
551
	 * Formats given status to html
552
	 *
553
	 * @param string $status
554
	 *
555
	 * @throws InvalidArgumentException
556
	 *
557
	 * @return string HTML
558
	 */
559
	private function formatStatus( $status ) {
560
		$messageName = "wbqc-constraintreport-status-" . strtolower( $status );
561
		$statusIcons = [
562
			CheckResult::STATUS_WARNING => [
563
				'icon' => 'non-mandatory-constraint-violation',
564
			],
565
			CheckResult::STATUS_VIOLATION => [
566
				'icon' => 'mandatory-constraint-violation',
567
			],
568
			CheckResult::STATUS_BAD_PARAMETERS => [
569
				'icon' => 'alert',
570
				'flags' => 'warning',
571
			],
572
		];
573
574
		if ( array_key_exists( $status, $statusIcons ) ) {
575
			$iconWidget = new IconWidget( $statusIcons[$status] );
576
			$iconHtml = $iconWidget->toString() . ' ';
577
		} else {
578
			$iconHtml = '';
579
		}
580
581
		$labelWidget = new LabelWidget( [
582
			'label' => $this->msg( $messageName )->text(),
583
		] );
584
		$labelHtml = $labelWidget->toString();
585
586
		$formattedStatus =
587
			Html::rawElement(
588
				'span',
589
				[
590
					'class' => 'wbqc-status wbqc-status-' . $status
591
				],
592
				$iconHtml . $labelHtml
593
			);
594
595
		return $formattedStatus;
596
	}
597
598
	/**
599
	 * Parses data values to human-readable string
600
	 *
601
	 * @param DataValue|array $dataValues
602
	 * @param string $separator
603
	 *
604
	 * @throws InvalidArgumentException
605
	 *
606
	 * @return string HTML
607
	 */
608
	protected function formatDataValues( $dataValues, $separator = ', ' ) {
609
		if ( $dataValues instanceof DataValue ) {
610
			$dataValues = [ $dataValues ];
611
		} elseif ( !is_array( $dataValues ) ) {
612
			throw new InvalidArgumentException( '$dataValues has to be instance of DataValue or an array of DataValues.' );
613
		}
614
615
		$formattedDataValues = [];
616
		foreach ( $dataValues as $dataValue ) {
617
			if ( !( $dataValue instanceof DataValue ) ) {
618
				throw new InvalidArgumentException( '$dataValues has to be instance of DataValue or an array of DataValues.' );
619
			}
620
			if ( $dataValue instanceof EntityIdValue ) {
621
				$formattedDataValues[ ] = $this->entityIdLabelFormatter->formatEntityId( $dataValue->getEntityId() );
622
			} else {
623
				$formattedDataValues[ ] = $this->dataValueFormatter->format( $dataValue );
624
			}
625
		}
626
627
		return implode( $separator, $formattedDataValues );
628
	}
629
630
	/**
631
	 * Returns html link to given entity with anchor to specified property.
632
	 *
633
	 * @param EntityId $entityId
634
	 * @param PropertyId $propertyId
635
	 * @param string $text HTML
636
	 *
637
	 * @return string HTML
638
	 */
639
	private function getClaimLink( EntityId $entityId, PropertyId $propertyId, $text ) {
640
		return Html::rawElement(
641
			'a',
642
			[
643
				'href' => $this->getClaimUrl( $entityId, $propertyId ),
644
				'target' => '_blank'
645
			],
646
			$text
647
		);
648
	}
649
650
	/**
651
	 * Returns url of given entity with anchor to specified property.
652
	 *
653
	 * @param EntityId $entityId
654
	 * @param PropertyId $propertyId
655
	 *
656
	 * @return string
657
	 */
658
	private function getClaimUrl( EntityId $entityId, PropertyId $propertyId ) {
659
		$title = $this->entityTitleLookup->getTitleForId( $entityId );
660
		$entityUrl = sprintf( '%s#%s', $title->getLocalURL(), $propertyId->getSerialization() );
661
662
		return $entityUrl;
663
	}
664
665
}
666