Completed
Push — master ( 0dfa46...acf0fe )
by
unknown
02:06 queued 10s
created

SpecialConstraintReport::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 52
rs 9.0472
c 0
b 0
f 0
cc 1
nc 1
nop 9

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 OOUI\IconWidget;
12
use OOUI\LabelWidget;
13
use SpecialPage;
14
use UnexpectedValueException;
15
use ValueFormatters\FormatterOptions;
16
use ValueFormatters\ValueFormatter;
17
use Wikibase\DataModel\Entity\EntityId;
18
use Wikibase\DataModel\Entity\EntityIdParser;
19
use Wikibase\DataModel\Entity\EntityIdParsingException;
20
use Wikibase\DataModel\Entity\EntityIdValue;
21
use Wikibase\DataModel\Entity\ItemId;
22
use Wikibase\DataModel\Entity\PropertyId;
23
use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
24
use Wikibase\DataModel\Services\Lookup\EntityLookup;
25
use Wikibase\Lib\Formatters\OutputFormatValueFormatterFactory;
26
use Wikibase\Lib\Formatters\SnakFormatter;
27
use Wikibase\Lib\Store\EntityTitleLookup;
28
use Wikibase\Repo\EntityIdLabelFormatterFactory;
29
use Wikibase\Repo\WikibaseRepo;
30
use Wikibase\View\EntityIdFormatterFactory;
31
use WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker;
32
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\MultilingualTextViolationMessageRenderer;
33
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRenderer;
34
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
35
use WikibaseQuality\ConstraintReport\ConstraintParameterRenderer;
36
use WikibaseQuality\ConstraintReport\Html\HtmlTableBuilder;
37
use WikibaseQuality\ConstraintReport\Html\HtmlTableCellBuilder;
38
use WikibaseQuality\ConstraintReport\Html\HtmlTableHeaderBuilder;
39
40
/**
41
 * Special page that displays all constraints that are defined on an Entity with additional information
42
 * (whether it complied or was a violation, which parameters the constraint has etc.).
43
 *
44
 * @author BP2014N1
45
 * @license GPL-2.0-or-later
46
 */
47
class SpecialConstraintReport extends SpecialPage {
48
49
	/**
50
	 * @var EntityIdParser
51
	 */
52
	private $entityIdParser;
53
54
	/**
55
	 * @var EntityLookup
56
	 */
57
	private $entityLookup;
58
59
	/**
60
	 * @var EntityTitleLookup
61
	 */
62
	private $entityTitleLookup;
63
64
	/**
65
	 * @var ValueFormatter
66
	 */
67
	private $dataValueFormatter;
68
69
	/**
70
	 * @var EntityIdFormatter
71
	 */
72
	private $entityIdLabelFormatter;
73
74
	/**
75
	 * @var EntityIdFormatter
76
	 */
77
	private $entityIdLinkFormatter;
78
79
	/**
80
	 * @var DelegatingConstraintChecker
81
	 */
82
	private $constraintChecker;
83
84
	/**
85
	 * @var ConstraintParameterRenderer
86
	 */
87
	private $constraintParameterRenderer;
88
89
	/**
90
	 * @var ViolationMessageRenderer
91
	 */
92
	private $violationMessageRenderer;
93
94
	/**
95
	 * @var Config
96
	 */
97
	private $config;
98
99
	/**
100
	 * @var IBufferingStatsdDataFactory
101
	 */
102
	private $dataFactory;
103
104
	public static function factory(
105
		Config $config,
106
		IBufferingStatsdDataFactory $dataFactory,
107
		EntityIdParser $entityIdParser,
108
		EntityLookup $entityLookup,
109
		DelegatingConstraintChecker $delegatingConstraintChecker
110
	): self {
111
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
112
113
		return new self(
114
			$entityLookup,
115
			$wikibaseRepo->getEntityTitleLookup(),
116
			new EntityIdLabelFormatterFactory(),
117
			$wikibaseRepo->getEntityIdHtmlLinkFormatterFactory(),
118
			$entityIdParser,
119
			$wikibaseRepo->getValueFormatterFactory(),
120
			$delegatingConstraintChecker,
121
			$config,
122
			$dataFactory
123
		);
124
	}
125
126
	public function __construct(
127
		EntityLookup $entityLookup,
128
		EntityTitleLookup $entityTitleLookup,
129
		EntityIdLabelFormatterFactory $entityIdLabelFormatterFactory,
130
		EntityIdFormatterFactory $entityIdHtmlLinkFormatterFactory,
131
		EntityIdParser $entityIdParser,
132
		OutputFormatValueFormatterFactory $valueFormatterFactory,
133
		DelegatingConstraintChecker $constraintChecker,
134
		Config $config,
135
		IBufferingStatsdDataFactory $dataFactory
136
	) {
137
		parent::__construct( 'ConstraintReport' );
138
139
		$this->entityLookup = $entityLookup;
140
		$this->entityTitleLookup = $entityTitleLookup;
141
		$this->entityIdParser = $entityIdParser;
142
143
		$language = $this->getLanguage();
144
145
		$formatterOptions = new FormatterOptions();
146
		$formatterOptions->setOption( SnakFormatter::OPT_LANG, $language->getCode() );
147
		$this->dataValueFormatter = $valueFormatterFactory->getValueFormatter(
148
			SnakFormatter::FORMAT_HTML,
149
			$formatterOptions
150
		);
151
152
		$this->entityIdLabelFormatter = $entityIdLabelFormatterFactory->getEntityIdFormatter(
153
			$language
154
		);
155
156
		$this->entityIdLinkFormatter = $entityIdHtmlLinkFormatterFactory->getEntityIdFormatter(
157
			$language
158
		);
159
160
		$this->constraintChecker = $constraintChecker;
161
162
		$this->constraintParameterRenderer = new ConstraintParameterRenderer(
163
			$this->entityIdLabelFormatter,
164
			$this->dataValueFormatter,
165
			$this->getContext(),
166
			$config
167
		);
168
		$this->violationMessageRenderer = new MultilingualTextViolationMessageRenderer(
169
			$this->entityIdLinkFormatter,
170
			$this->dataValueFormatter,
171
			$this->getContext(),
172
			$config
173
		);
174
175
		$this->config = $config;
176
		$this->dataFactory = $dataFactory;
177
	}
178
179
	/**
180
	 * Returns array of modules that should be added
181
	 *
182
	 * @return string[]
183
	 */
184
	private function getModules() {
185
		return [
186
			'SpecialConstraintReportPage',
187
			'wikibase.quality.constraints.icon',
188
		];
189
	}
190
191
	/**
192
	 * @see SpecialPage::getGroupName
193
	 *
194
	 * @return string
195
	 */
196
	protected function getGroupName() {
197
		return 'wikibase';
198
	}
199
200
	/**
201
	 * @see SpecialPage::getDescription
202
	 *
203
	 * @return string
204
	 */
205
	public function getDescription() {
206
		return $this->msg( 'wbqc-constraintreport' )->escaped();
207
	}
208
209
	/**
210
	 * @see SpecialPage::execute
211
	 *
212
	 * @param string|null $subPage
213
	 *
214
	 * @throws InvalidArgumentException
215
	 * @throws EntityIdParsingException
216
	 * @throws UnexpectedValueException
217
	 */
218
	public function execute( $subPage ) {
219
		$out = $this->getOutput();
220
221
		$postRequest = $this->getContext()->getRequest()->getVal( 'entityid' );
222
		if ( $postRequest ) {
223
			$out->redirect( $this->getPageTitle( strtoupper( $postRequest ) )->getLocalURL() );
224
			return;
225
		}
226
227
		$out->enableOOUI();
228
		$out->addModules( $this->getModules() );
229
230
		$this->setHeaders();
231
232
		$out->addHTML( $this->getExplanationText() );
233
		$this->buildEntityIdForm();
234
235
		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...
236
			return;
237
		}
238
239
		if ( !is_string( $subPage ) ) {
240
			throw new InvalidArgumentException( '$subPage must be string.' );
241
		}
242
243
		try {
244
			$entityId = $this->entityIdParser->parse( $subPage );
245
		} catch ( EntityIdParsingException $e ) {
246
			$out->addHTML(
247
				$this->buildNotice( 'wbqc-constraintreport-invalid-entity-id', true )
248
			);
249
			return;
250
		}
251
252
		if ( !$this->entityLookup->hasEntity( $entityId ) ) {
253
			$out->addHTML(
254
				$this->buildNotice( 'wbqc-constraintreport-not-existent-entity', true )
255
			);
256
			return;
257
		}
258
259
		$this->dataFactory->increment(
260
			'wikibase.quality.constraints.specials.specialConstraintReport.executeCheck'
261
		);
262
		$results = $this->constraintChecker->checkAgainstConstraintsOnEntityId( $entityId );
263
264
		if ( $results !== [] ) {
265
			$out->addHTML(
266
				$this->buildResultHeader( $entityId )
267
				. $this->buildSummary( $results )
268
				. $this->buildResultTable( $entityId, $results )
269
			);
270
		} else {
271
			$out->addHTML(
272
				$this->buildResultHeader( $entityId )
273
				. $this->buildNotice( 'wbqc-constraintreport-empty-result' )
274
			);
275
		}
276
	}
277
278
	/**
279
	 * Builds html form for entity id input
280
	 */
281
	private function buildEntityIdForm() {
282
		$formDescriptor = [
283
			'entityid' => [
284
				'class' => 'HTMLTextField',
285
				'section' => 'section',
286
				'name' => 'entityid',
287
				'label-message' => 'wbqc-constraintreport-form-entityid-label',
288
				'cssclass' => 'wbqc-constraintreport-form-entity-id',
289
				'placeholder' => $this->msg( 'wbqc-constraintreport-form-entityid-placeholder' )->escaped()
290
			]
291
		];
292
		$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext(), 'wbqc-constraintreport-form' );
293
		$htmlForm->setSubmitText( $this->msg( 'wbqc-constraintreport-form-submit-label' )->escaped() );
294
		$htmlForm->setSubmitCallback( function() {
295
			return false;
296
		} );
297
		$htmlForm->setMethod( 'post' );
298
		$htmlForm->show();
299
	}
300
301
	/**
302
	 * Builds notice with given message. Optionally notice can be handles as error by settings $error to true
303
	 *
304
	 * @param string $messageKey
305
	 * @param bool $error
306
	 *
307
	 * @throws InvalidArgumentException
308
	 *
309
	 * @return string HTML
310
	 */
311
	private function buildNotice( $messageKey, $error = false ) {
312
		if ( !is_string( $messageKey ) ) {
313
			throw new InvalidArgumentException( '$message must be string.' );
314
		}
315
		if ( !is_bool( $error ) ) {
316
			throw new InvalidArgumentException( '$error must be bool.' );
317
		}
318
319
		$cssClasses = 'wbqc-constraintreport-notice';
320
		if ( $error ) {
321
			$cssClasses .= ' wbqc-constraintreport-notice-error';
322
		}
323
324
		return Html::rawElement(
325
				'p',
326
				[
327
					'class' => $cssClasses
328
				],
329
				$this->msg( $messageKey )->escaped()
330
			);
331
	}
332
333
	/**
334
	 * @return string HTML
335
	 */
336
	private function getExplanationText() {
337
		return Html::rawElement(
338
			'div',
339
			[ 'class' => 'wbqc-explanation' ],
340
			Html::rawElement(
341
				'p',
342
				[],
343
				$this->msg( 'wbqc-constraintreport-explanation-part-one' )->escaped()
344
			)
345
			. Html::rawElement(
346
				'p',
347
				[],
348
				$this->msg( 'wbqc-constraintreport-explanation-part-two' )->escaped()
349
			)
350
		);
351
	}
352
353
	/**
354
	 * @param EntityId $entityId
355
	 * @param CheckResult[] $results
356
	 *
357
	 * @return string HTML
358
	 * @suppress SecurityCheck-DoubleEscaped
359
	 */
360
	private function buildResultTable( EntityId $entityId, array $results ) {
361
		// Set table headers
362
		$table = new HtmlTableBuilder(
363
			[
364
				new HtmlTableHeaderBuilder(
365
					$this->msg( 'wbqc-constraintreport-result-table-header-status' )->escaped(),
366
					true
367
				),
368
				new HtmlTableHeaderBuilder(
369
					$this->msg( 'wbqc-constraintreport-result-table-header-property' )->escaped(),
370
					true
371
				),
372
				new HtmlTableHeaderBuilder(
373
					$this->msg( 'wbqc-constraintreport-result-table-header-message' )->escaped(),
374
					true
375
				),
376
				new HtmlTableHeaderBuilder(
377
					$this->msg( 'wbqc-constraintreport-result-table-header-constraint' )->escaped(),
378
					true
379
				)
380
			]
381
		);
382
383
		foreach ( $results as $result ) {
384
			$table = $this->appendToResultTable( $table, $entityId, $result );
385
		}
386
387
		return $table->toHtml();
388
	}
389
390
	private function appendToResultTable( HtmlTableBuilder $table, EntityId $entityId, CheckResult $result ) {
391
		$message = $result->getMessage();
392
		if ( $message === null ) {
393
			// no row for this result
394
			return $table;
395
		}
396
397
		// Status column
398
		$statusColumn = $this->formatStatus( $result->getStatus() );
399
400
		// Property column
401
		$propertyId = new PropertyId( $result->getContextCursor()->getSnakPropertyId() );
402
		$propertyColumn = $this->getClaimLink(
403
			$entityId,
404
			$propertyId,
405
			$this->entityIdLabelFormatter->formatEntityId( $propertyId )
406
		);
407
408
		// Message column
409
		$messageColumn = $this->violationMessageRenderer->render( $message );
410
411
		// Constraint column
412
		$constraintTypeItemId = $result->getConstraint()->getConstraintTypeItemId();
413
		try {
414
			$constraintTypeLabel = $this->entityIdLabelFormatter->formatEntityId( new ItemId( $constraintTypeItemId ) );
415
		} catch ( InvalidArgumentException $e ) {
416
			$constraintTypeLabel = htmlspecialchars( $constraintTypeItemId );
417
		}
418
		$constraintLink = $this->getClaimLink(
419
			$propertyId,
420
			new PropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) ),
421
			$constraintTypeLabel
422
		);
423
		$constraintColumn = $this->buildExpandableElement(
424
			$constraintLink,
425
			$this->constraintParameterRenderer->formatParameters( $result->getParameters() ),
426
			'[...]'
427
		);
428
429
		// Append cells
430
		$table->appendRow(
431
			[
432
				new HtmlTableCellBuilder(
433
					$statusColumn,
434
					[],
435
					true
436
				),
437
				new HtmlTableCellBuilder(
438
					$propertyColumn,
439
					[],
440
					true
441
				),
442
				new HtmlTableCellBuilder(
443
					$messageColumn,
444
					[],
445
					true
446
				),
447
				new HtmlTableCellBuilder(
448
					$constraintColumn,
449
					[],
450
					true
451
				)
452
			]
453
		);
454
455
		return $table;
456
	}
457
458
	/**
459
	 * Returns html text of the result header
460
	 *
461
	 * @param EntityId $entityId
462
	 *
463
	 * @return string HTML
464
	 */
465
	protected function buildResultHeader( EntityId $entityId ) {
466
		$entityLink = sprintf( '%s (%s)',
467
							   $this->entityIdLinkFormatter->formatEntityId( $entityId ),
468
							   htmlspecialchars( $entityId->getSerialization() ) );
469
470
		return Html::rawElement(
471
			'h3',
472
			[],
473
			sprintf( '%s %s', $this->msg( 'wbqc-constraintreport-result-headline' )->escaped(), $entityLink )
474
		);
475
	}
476
477
	/**
478
	 * Builds summary from given results
479
	 *
480
	 * @param CheckResult[] $results
481
	 *
482
	 * @return string HTML
483
	 */
484
	protected function buildSummary( array $results ) {
485
		$statuses = [];
486
		foreach ( $results as $result ) {
487
			$status = strtolower( $result->getStatus() );
488
			$statuses[$status] = isset( $statuses[$status] ) ? $statuses[$status] + 1 : 1;
489
		}
490
491
		$statusElements = [];
492
		foreach ( $statuses as $status => $count ) {
493
			if ( $count > 0 ) {
494
				$statusElements[] =
495
					$this->formatStatus( $status )
496
					. ': '
497
					. $count;
498
			}
499
		}
500
501
		return Html::rawElement( 'p', [], implode( ', ', $statusElements ) );
502
	}
503
504
	/**
505
	 * Builds a html div element with given content and a tooltip with given tooltip content
506
	 * If $tooltipContent is null, no tooltip will be created
507
	 *
508
	 * @param string $content
509
	 * @param string $expandableContent
510
	 * @param string $indicator
511
	 *
512
	 * @throws InvalidArgumentException
513
	 *
514
	 * @return string HTML
515
	 */
516
	protected function buildExpandableElement( $content, $expandableContent, $indicator ) {
517
		if ( !is_string( $content ) ) {
518
			throw new InvalidArgumentException( '$content has to be string.' );
519
		}
520
		if ( $expandableContent && ( !is_string( $expandableContent ) ) ) {
521
			throw new InvalidArgumentException( '$tooltipContent, if provided, has to be string.' );
522
		}
523
524
		if ( empty( $expandableContent ) ) {
525
			return $content;
526
		}
527
528
		$tooltipIndicator = Html::element(
529
			'span',
530
			[
531
				'class' => 'wbqc-expandable-content-indicator wbqc-indicator'
532
			],
533
			$indicator
534
		);
535
536
		$wrappedExpandableContent = Html::element(
537
			'div',
538
			[
539
				'class' => 'wbqc-expandable-content'
540
			],
541
			$expandableContent
542
		);
543
544
		return sprintf( '%s %s %s', $content, $tooltipIndicator, $wrappedExpandableContent );
545
	}
546
547
	/**
548
	 * Formats given status to html
549
	 *
550
	 * @param string $status
551
	 *
552
	 * @throws InvalidArgumentException
553
	 *
554
	 * @return string HTML
555
	 */
556
	private function formatStatus( $status ) {
557
		$messageName = "wbqc-constraintreport-status-" . strtolower( $status );
558
		$statusIcons = [
559
			CheckResult::STATUS_SUGGESTION => [
560
				'icon' => 'suggestion-constraint-violation',
561
			],
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