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

SpecialConstraintReport::execute()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 59
rs 7.9612
c 0
b 0
f 0
cc 7
nc 7
nop 1

How to fix   Long Method   

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:

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