Completed
Push — master ( f9ad25...2b8b2c )
by
unknown
06:14 queued 11s
created

checkAgainstConstraintsOnClaimId()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
cc 3
nc 3
nop 3
1
<?php
2
3
namespace WikibaseQuality\ConstraintReport\ConstraintCheck;
4
5
use InvalidArgumentException;
6
use LogicException;
7
use Wikibase\DataModel\Entity\EntityDocument;
8
use Wikibase\DataModel\Entity\PropertyId;
9
use Wikibase\DataModel\Reference;
10
use Wikibase\DataModel\Services\Lookup\EntityLookup;
11
use Wikibase\DataModel\Services\Statement\StatementGuidParser;
12
use Wikibase\DataModel\Statement\Statement;
13
use Wikibase\DataModel\Statement\StatementListProvider;
14
use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\DependencyMetadata;
15
use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\Metadata;
16
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
17
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\EntityContextCursor;
18
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\MainSnakContext;
19
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\QualifierContext;
20
use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\ReferenceContext;
21
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException;
22
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
23
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\LoggingHelper;
24
use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelperException;
25
use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
26
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
27
use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\NullResult;
28
use WikibaseQuality\ConstraintReport\ConstraintLookup;
29
use WikibaseQuality\ConstraintReport\Constraint;
30
use Wikibase\DataModel\Entity\EntityId;
31
32
/**
33
 * Used to start the constraint-check process and to delegate
34
 * the statements that has to be checked to the corresponding checkers
35
 *
36
 * @author BP2014N1
37
 * @license GPL-2.0-or-later
38
 */
39
class DelegatingConstraintChecker {
40
41
	/**
42
	 * Wikibase entity lookup.
43
	 *
44
	 * @var EntityLookup
45
	 */
46
	private $entityLookup;
47
48
	/**
49
	 * @var ConstraintChecker[]
50
	 */
51
	private $checkerMap;
52
53
	/**
54
	 * @var ConstraintLookup
55
	 */
56
	private $constraintLookup;
57
58
	/**
59
	 * @var ConstraintParameterParser
60
	 */
61
	private $constraintParameterParser;
62
63
	/**
64
	 * @var StatementGuidParser
65
	 */
66
	private $statementGuidParser;
67
68
	/**
69
	 * @var LoggingHelper
70
	 */
71
	private $loggingHelper;
72
73
	/**
74
	 * @var bool
75
	 */
76
	private $checkQualifiers;
77
78
	/**
79
	 * @var bool
80
	 */
81
	private $checkReferences;
82
83
	/**
84
	 * @var string[]
85
	 */
86
	private $propertiesWithViolatingQualifiers;
87
88
	/**
89
	 * @param EntityLookup $lookup
90
	 * @param ConstraintChecker[] $checkerMap
91
	 * @param ConstraintLookup $constraintRepository
92
	 * @param ConstraintParameterParser $constraintParameterParser
93
	 * @param StatementGuidParser $statementGuidParser
94
	 * @param LoggingHelper $loggingHelper
95
	 * @param bool $checkQualifiers whether to check qualifiers
96
	 * @param bool $checkReferences whether to check references
97
	 * @param string[] $propertiesWithViolatingQualifiers on statements of these properties,
98
	 * qualifiers will not be checked
99
	 */
100
	public function __construct(
101
		EntityLookup $lookup,
102
		array $checkerMap,
103
		ConstraintLookup $constraintRepository,
104
		ConstraintParameterParser $constraintParameterParser,
105
		StatementGuidParser $statementGuidParser,
106
		LoggingHelper $loggingHelper,
107
		$checkQualifiers,
108
		$checkReferences,
109
		array $propertiesWithViolatingQualifiers
110
	) {
111
		$this->entityLookup = $lookup;
112
		$this->checkerMap = $checkerMap;
113
		$this->constraintLookup = $constraintRepository;
114
		$this->constraintParameterParser = $constraintParameterParser;
115
		$this->statementGuidParser = $statementGuidParser;
116
		$this->loggingHelper = $loggingHelper;
117
		$this->checkQualifiers = $checkQualifiers;
118
		$this->checkReferences = $checkReferences;
119
		$this->propertiesWithViolatingQualifiers = $propertiesWithViolatingQualifiers;
120
	}
121
122
	/**
123
	 * Starts the whole constraint-check process for entity or constraint ID on entity.
124
	 * Statements of the entity will be checked against every constraint that is defined on the property.
125
	 *
126
	 * @param EntityId $entityId
127
	 * @param string[]|null $constraintIds
128
	 * @param callable|null $defaultResultsPerContext
129
	 * Optional function to pre-populate the check results per context.
130
	 * For each {@link Context} where constraints will be checked,
131
	 * this function (if not null) is first called with that context as argument,
132
	 * and may return an array of check results to which the regular results are appended.
133
	 * @param callable|null $defaultResultsPerEntity
134
	 * Optional function to pre-populate the check results per entity.
135
	 * This function (if not null) is called once with $entityId as argument,
136
	 * and may return an array of check results to which the regular results are appended.
137
	 *
138
	 * @return CheckResult[]
139
	 */
140
	public function checkAgainstConstraintsOnEntityId(
141
		EntityId $entityId,
142
		array $constraintIds = null,
143
		callable $defaultResultsPerContext = null,
144
		callable $defaultResultsPerEntity = null
145
	) {
146
		$checkResults = [];
147
		$entity = $this->entityLookup->getEntity( $entityId );
148
149
		if ( $entity instanceof StatementListProvider ) {
150
			$startTime = microtime( true );
151
152
			$checkResults = $this->checkEveryStatement(
153
				$this->entityLookup->getEntity( $entityId ),
0 ignored issues
show
Bug introduced by
It seems like $this->entityLookup->getEntity($entityId) can be null; however, checkEveryStatement() 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...
154
				$constraintIds,
155
				$defaultResultsPerContext
156
			);
157
158
			$endTime = microtime( true );
159
160
			if ( $constraintIds === null ) { // only log full constraint checks
161
				$this->loggingHelper->logConstraintCheckOnEntity(
162
					$entityId,
163
					$checkResults,
164
					$endTime - $startTime,
165
					__METHOD__
166
				);
167
			}
168
		}
169
170
		if ( $defaultResultsPerEntity !== null ) {
171
			$checkResults = array_merge( $defaultResultsPerEntity( $entityId ), $checkResults );
172
		}
173
174
		return $this->sortResult( $checkResults );
175
	}
176
177
	/**
178
	 * Starts the whole constraint-check process.
179
	 * Statements of the entity will be checked against every constraint that is defined on the claim.
180
	 *
181
	 * @param string $guid
182
	 * @param string[]|null $constraintIds
183
	 * @param callable|null $defaultResults Optional function to pre-populate the check results.
184
	 * For each {@link Context} where constraints will be checked,
185
	 * this function (if not null) is first called with that context as argument,
186
	 * and may return an array of check results to which the regular results are appended.
187
	 *
188
	 * @return CheckResult[]
189
	 */
190
	public function checkAgainstConstraintsOnClaimId(
191
		$guid,
192
		array $constraintIds = null,
193
		callable $defaultResults = null
194
	) {
195
196
		$parsedGuid = $this->statementGuidParser->parse( $guid );
197
		$entityId = $parsedGuid->getEntityId();
198
		$entity = $this->entityLookup->getEntity( $entityId );
199
		if ( $entity instanceof StatementListProvider ) {
200
			$statement = $entity->getStatements()->getFirstStatementWithGuid( $guid );
201
			if ( $statement ) {
202
				$result = $this->checkStatement(
203
					$entity,
204
					$statement,
205
					$constraintIds,
206
					$defaultResults
207
				);
208
				$output = $this->sortResult( $result );
209
				return $output;
210
			}
211
		}
212
213
		return [];
214
	}
215
216
	private function getAllowedContextTypes( Constraint $constraint ) {
217
		if ( !array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) {
218
			return [
219
				Context::TYPE_STATEMENT,
220
				Context::TYPE_QUALIFIER,
221
				Context::TYPE_REFERENCE,
222
			];
223
		}
224
225
		return array_keys( array_filter(
226
			$this->checkerMap[$constraint->getConstraintTypeItemId()]->getSupportedContextTypes(),
227
			function ( $resultStatus ) {
228
				return $resultStatus !== CheckResult::STATUS_NOT_IN_SCOPE;
229
			}
230
		) );
231
	}
232
233
	/**
234
	 * Like ConstraintChecker::checkConstraintParameters,
235
	 * but for meta-parameters common to all checkers.
236
	 *
237
	 * @param Constraint $constraint
238
	 *
239
	 * @return ConstraintParameterException[]
240
	 */
241
	private function checkCommonConstraintParameters( Constraint $constraint ) {
242
		$constraintParameters = $constraint->getConstraintParameters();
243
		try {
244
			$this->constraintParameterParser->checkError( $constraintParameters );
245
		} catch ( ConstraintParameterException $e ) {
246
			return [ $e ];
247
		}
248
249
		$problems = [];
250
		try {
251
			$this->constraintParameterParser->parseExceptionParameter( $constraintParameters );
252
		} catch ( ConstraintParameterException $e ) {
253
			$problems[] = $e;
254
		}
255
		try {
256
			$this->constraintParameterParser->parseConstraintStatusParameter( $constraintParameters );
257
		} catch ( ConstraintParameterException $e ) {
258
			$problems[] = $e;
259
		}
260
		try {
261
			$this->constraintParameterParser->parseConstraintScopeParameter(
262
				$constraintParameters,
263
				$constraint->getConstraintTypeItemId(),
264
				$this->getAllowedContextTypes( $constraint )
265
			);
266
		} catch ( ConstraintParameterException $e ) {
267
			$problems[] = $e;
268
		}
269
		return $problems;
270
	}
271
272
	/**
273
	 * Check the constraint parameters of all constraints for the given property ID.
274
	 *
275
	 * @param PropertyId $propertyId
276
	 * @return ConstraintParameterException[][] first level indexed by constraint ID,
277
	 * second level like checkConstraintParametersOnConstraintId (but without possibility of null)
278
	 */
279
	public function checkConstraintParametersOnPropertyId( PropertyId $propertyId ) {
280
		$constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId );
281
		$result = [];
282
283 View Code Duplication
		foreach ( $constraints as $constraint ) {
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...
284
			$problems = $this->checkCommonConstraintParameters( $constraint );
285
286
			if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) {
287
				$checker = $this->checkerMap[$constraint->getConstraintTypeItemId()];
288
				$problems = array_merge( $problems, $checker->checkConstraintParameters( $constraint ) );
289
			}
290
291
			$result[$constraint->getConstraintId()] = $problems;
292
		}
293
294
		return $result;
295
	}
296
297
	/**
298
	 * Check the constraint parameters of the constraint with the given ID.
299
	 *
300
	 * @param string $constraintId
301
	 *
302
	 * @return ConstraintParameterException[]|null list of constraint parameter exceptions
303
	 * (empty means all parameters okay), or null if constraint is not found
304
	 */
305
	public function checkConstraintParametersOnConstraintId( $constraintId ) {
306
		$propertyId = $this->statementGuidParser->parse( $constraintId )->getEntityId();
307
		$constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId );
0 ignored issues
show
Compatibility introduced by
$propertyId of type object<Wikibase\DataModel\Entity\EntityId> is not a sub-type of object<Wikibase\DataModel\Entity\PropertyId>. It seems like you assume a child class of the class Wikibase\DataModel\Entity\EntityId to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
308
309 View Code Duplication
		foreach ( $constraints as $constraint ) {
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...
310
			if ( $constraint->getConstraintId() === $constraintId ) {
311
				$problems = $this->checkCommonConstraintParameters( $constraint );
312
313
				if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) {
314
					$checker = $this->checkerMap[$constraint->getConstraintTypeItemId()];
315
					$problems = array_merge( $problems, $checker->checkConstraintParameters( $constraint ) );
316
				}
317
318
				return $problems;
319
			}
320
		}
321
322
		return null;
323
	}
324
325
	/**
326
	 * @param EntityDocument|StatementListProvider $entity
327
	 * @param string[]|null $constraintIds list of constraints to check (if null: all constraints)
328
	 * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results
329
	 *
330
	 * @return CheckResult[]
331
	 */
332
	private function checkEveryStatement(
333
		EntityDocument $entity,
334
		array $constraintIds = null,
335
		callable $defaultResultsPerContext = null
336
	) {
337
		$result = [];
338
339
		/** @var Statement $statement */
340
		foreach ( $entity->getStatements() as $statement ) {
341
			$result = array_merge( $result,
342
				$this->checkStatement(
343
					$entity,
344
					$statement,
345
					$constraintIds,
346
					$defaultResultsPerContext
347
				) );
348
		}
349
350
		return $result;
351
	}
352
353
	/**
354
	 * @param EntityDocument|StatementListProvider $entity
355
	 * @param Statement $statement
356
	 * @param string[]|null $constraintIds list of constraints to check (if null: all constraints)
357
	 * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results
358
	 *
359
	 * @return CheckResult[]
360
	 */
361
	private function checkStatement(
362
		EntityDocument $entity,
363
		Statement $statement,
364
		array $constraintIds = null,
365
		callable $defaultResultsPerContext = null
366
	) {
367
		$result = [];
368
369
		$result = array_merge( $result,
370
			$this->checkConstraintsForMainSnak(
371
				$entity,
372
				$statement,
373
				$constraintIds,
374
				$defaultResultsPerContext
375
			) );
376
377
		if ( $this->checkQualifiers ) {
378
			$result = array_merge( $result,
379
				$this->checkConstraintsForQualifiers(
380
					$entity,
381
					$statement,
382
					$constraintIds,
383
					$defaultResultsPerContext
384
				) );
385
		}
386
387
		if ( $this->checkReferences ) {
388
			$result = array_merge( $result,
389
				$this->checkConstraintsForReferences(
390
					$entity,
391
					$statement,
392
					$constraintIds,
393
					$defaultResultsPerContext
394
				) );
395
		}
396
397
		return $result;
398
	}
399
400
	/**
401
	 * Get the constraints to actually check for a given property ID.
402
	 * If $constraintIds is not null, only check constraints with those constraint IDs,
403
	 * otherwise check all constraints for that property.
404
	 *
405
	 * @param PropertyId $propertyId
406
	 * @param string[]|null $constraintIds
407
	 * @return Constraint[]
408
	 */
409
	private function getConstraintsToUse( PropertyId $propertyId, array $constraintIds = null ) {
410
		$constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId );
411
		if ( $constraintIds !== null ) {
412
			$constraintsToUse = [];
413
			foreach ( $constraints as $constraint ) {
414
				if ( in_array( $constraint->getConstraintId(), $constraintIds ) ) {
415
					$constraintsToUse[] = $constraint;
416
				}
417
			}
418
			return $constraintsToUse;
419
		} else {
420
			return $constraints;
421
		}
422
	}
423
424
	/**
425
	 * @param EntityDocument $entity
426
	 * @param Statement $statement
427
	 * @param string[]|null $constraintIds list of constraints to check (if null: all constraints)
428
	 * @param callable|null $defaultResults optional function to pre-populate the check results
429
	 *
430
	 * @return CheckResult[]
431
	 */
432
	private function checkConstraintsForMainSnak(
433
		EntityDocument $entity,
434
		Statement $statement,
435
		array $constraintIds = null,
436
		callable $defaultResults = null
437
	) {
438
		$context = new MainSnakContext( $entity, $statement );
439
		$constraints = $this->getConstraintsToUse(
440
			$statement->getPropertyId(),
441
			$constraintIds
442
		);
443
		$result = $defaultResults !== null ? $defaultResults( $context ) : [];
444
445
		foreach ( $constraints as $constraint ) {
446
			$parameters = $constraint->getConstraintParameters();
447
			try {
448
				$exceptions = $this->constraintParameterParser->parseExceptionParameter( $parameters );
449
			} catch ( ConstraintParameterException $e ) {
450
				$result[] = new CheckResult( $context, $constraint, [], CheckResult::STATUS_BAD_PARAMETERS, $e->getViolationMessage() );
451
				continue;
452
			}
453
454
			if ( in_array( $entity->getId(), $exceptions ) ) {
455
				$message = new ViolationMessage( 'wbqc-violation-message-exception' );
456
				$result[] = new CheckResult( $context, $constraint, [], CheckResult::STATUS_EXCEPTION, $message );
457
				continue;
458
			}
459
460
			$result[] = $this->getCheckResultFor( $context, $constraint );
461
		}
462
463
		return $result;
464
	}
465
466
	/**
467
	 * @param EntityDocument $entity
468
	 * @param Statement $statement
469
	 * @param string[]|null $constraintIds list of constraints to check (if null: all constraints)
470
	 * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results
471
	 *
472
	 * @return CheckResult[]
473
	 */
474 View Code Duplication
	private function checkConstraintsForQualifiers(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
475
		EntityDocument $entity,
476
		Statement $statement,
477
		array $constraintIds = null,
478
		callable $defaultResultsPerContext = null
479
	) {
480
		$result = [];
481
482
		if ( in_array(
483
			$statement->getPropertyId()->getSerialization(),
484
			$this->propertiesWithViolatingQualifiers
485
		) ) {
486
			return $result;
487
		}
488
489
		foreach ( $statement->getQualifiers() as $qualifier ) {
490
			$qualifierContext = new QualifierContext( $entity, $statement, $qualifier );
491
			if ( $defaultResultsPerContext !== null ) {
492
				$result = array_merge( $result, $defaultResultsPerContext( $qualifierContext ) );
493
			}
494
			$qualifierConstraints = $this->getConstraintsToUse(
495
				$qualifierContext->getSnak()->getPropertyId(),
496
				$constraintIds
497
			);
498
			foreach ( $qualifierConstraints as $qualifierConstraint ) {
499
				$result[] = $this->getCheckResultFor( $qualifierContext, $qualifierConstraint );
500
			}
501
		}
502
503
		return $result;
504
	}
505
506
	/**
507
	 * @param EntityDocument $entity
508
	 * @param Statement $statement
509
	 * @param string[]|null $constraintIds list of constraints to check (if null: all constraints)
510
	 * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results
511
	 *
512
	 * @return CheckResult[]
513
	 */
514 View Code Duplication
	private function checkConstraintsForReferences(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
515
		EntityDocument $entity,
516
		Statement $statement,
517
		array $constraintIds = null,
518
		callable $defaultResultsPerContext = null
519
	) {
520
		$result = [];
521
522
		/** @var Reference $reference */
523
		foreach ( $statement->getReferences() as $reference ) {
524
			foreach ( $reference->getSnaks() as $snak ) {
525
				$referenceContext = new ReferenceContext(
526
					$entity, $statement, $reference, $snak
527
				);
528
				if ( $defaultResultsPerContext !== null ) {
529
					$result = array_merge( $result, $defaultResultsPerContext( $referenceContext ) );
530
				}
531
				$referenceConstraints = $this->getConstraintsToUse(
532
					$referenceContext->getSnak()->getPropertyId(),
533
					$constraintIds
534
				);
535
				foreach ( $referenceConstraints as $referenceConstraint ) {
536
					$result[] = $this->getCheckResultFor(
537
						$referenceContext,
538
						$referenceConstraint
539
					);
540
				}
541
			}
542
		}
543
544
		return $result;
545
	}
546
547
	/**
548
	 * @param Context $context
549
	 * @param Constraint $constraint
550
	 *
551
	 * @throws InvalidArgumentException
552
	 * @return CheckResult
553
	 */
554
	private function getCheckResultFor( Context $context, Constraint $constraint ) {
555
		if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) {
556
			$checker = $this->checkerMap[$constraint->getConstraintTypeItemId()];
557
			$result = $this->handleScope( $checker, $context, $constraint );
558
559
			if ( $result !== null ) {
560
				$this->addMetadata( $context, $result );
561
				return $result;
562
			}
563
564
			$startTime = microtime( true );
565
			try {
566
				$result = $checker->checkConstraint( $context, $constraint );
567
			} catch ( ConstraintParameterException $e ) {
568
				$result = new CheckResult( $context, $constraint, [], CheckResult::STATUS_BAD_PARAMETERS, $e->getViolationMessage() );
569
			} catch ( SparqlHelperException $e ) {
570
				$message = new ViolationMessage( 'wbqc-violation-message-sparql-error' );
571
				$result = new CheckResult( $context, $constraint, [], CheckResult::STATUS_TODO, $message );
572
			}
573
			$endTime = microtime( true );
574
575
			$this->addMetadata( $context, $result );
576
577
			$this->downgradeResultStatus( $context, $result );
578
579
			$this->loggingHelper->logConstraintCheck(
580
				$context,
581
				$constraint,
582
				$result,
583
				get_class( $checker ),
584
				$endTime - $startTime,
585
				__METHOD__
586
			);
587
588
			return $result;
589
		} else {
590
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_TODO, null );
591
		}
592
	}
593
594
	private function handleScope(
595
		ConstraintChecker $checker,
596
		Context $context,
597
		Constraint $constraint
598
	) {
599
		try {
600
			$checkedContextTypes = $this->constraintParameterParser->parseConstraintScopeParameter(
601
				$constraint->getConstraintParameters(),
602
				$constraint->getConstraintTypeItemId()
603
			);
604
		} catch ( ConstraintParameterException $e ) {
605
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_BAD_PARAMETERS, $e->getViolationMessage() );
606
		}
607
		if ( $checkedContextTypes === null ) {
608
			$checkedContextTypes = $checker->getDefaultContextTypes();
609
		}
610
		if ( !in_array( $context->getType(), $checkedContextTypes ) ) {
611
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_NOT_IN_SCOPE, null );
612
		}
613
		if ( $checker->getSupportedContextTypes()[$context->getType()] === CheckResult::STATUS_TODO ) {
614
			return new CheckResult( $context, $constraint, [], CheckResult::STATUS_TODO, null );
615
		}
616
		return null;
617
	}
618
619
	private function addMetadata( Context $context, CheckResult $result ) {
620
		$result->withMetadata( Metadata::merge( [
621
			$result->getMetadata(),
622
			Metadata::ofDependencyMetadata( DependencyMetadata::merge( [
623
				DependencyMetadata::ofEntityId( $context->getEntity()->getId() ),
0 ignored issues
show
Bug introduced by
It seems like $context->getEntity()->getId() can be null; however, ofEntityId() 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...
624
				DependencyMetadata::ofEntityId( $result->getConstraint()->getPropertyId() ),
625
			] ) ),
626
		] ) );
627
	}
628
629
	private function downgradeResultStatus( Context $context, CheckResult &$result ) {
630
		$constraint = $result->getConstraint();
631
		try {
632
			$constraintStatus = $this->constraintParameterParser
633
				->parseConstraintStatusParameter( $constraint->getConstraintParameters() );
634
		} catch ( ConstraintParameterException $e ) {
635
			$result = new CheckResult( $context, $constraint, [], CheckResult::STATUS_BAD_PARAMETERS, $e->getViolationMessage() );
636
			$constraintStatus = null;
637
		}
638
		if ( $constraintStatus === null ) {
639
			// downgrade violation to warning
640
			if ( $result->getStatus() === CheckResult::STATUS_VIOLATION ) {
641
				$result->setStatus( CheckResult::STATUS_WARNING );
642
			}
643
		} elseif ( $constraintStatus === 'suggestion' ) {
644
			// downgrade violation to suggestion
645
			if ( $result->getStatus() === CheckResult::STATUS_VIOLATION ) {
646
				$result->setStatus( CheckResult::STATUS_SUGGESTION );
647
			}
648
			$result->addParameter( 'constraint_status', $constraintStatus );
649
		} else {
650
			if ( $constraintStatus !== 'mandatory' ) {
651
				// @codeCoverageIgnoreStart
652
				throw new LogicException(
653
					"Unknown constraint status '$constraintStatus', " .
654
					"only known status is 'mandatory'"
655
				);
656
				// @codeCoverageIgnoreEnd
657
			}
658
			$result->addParameter( 'constraint_status', $constraintStatus );
659
		}
660
	}
661
662
	/**
663
	 * @param CheckResult[] $result
664
	 *
665
	 * @return CheckResult[]
666
	 */
667
	private function sortResult( array $result ) {
668
		if ( count( $result ) < 2 ) {
669
			return $result;
670
		}
671
672
		$sortFunction = function ( CheckResult $a, CheckResult $b ) {
673
			$orderNum = 0;
674
			$order = [
675
				CheckResult::STATUS_BAD_PARAMETERS => $orderNum++,
676
				CheckResult::STATUS_VIOLATION => $orderNum++,
677
				CheckResult::STATUS_WARNING => $orderNum++,
678
				CheckResult::STATUS_SUGGESTION => $orderNum++,
679
				CheckResult::STATUS_EXCEPTION => $orderNum++,
680
				CheckResult::STATUS_COMPLIANCE => $orderNum++,
681
				CheckResult::STATUS_DEPRECATED => $orderNum++,
682
				CheckResult::STATUS_NOT_IN_SCOPE => $orderNum++,
683
				'other' => $orderNum++,
684
			];
685
686
			$statusA = $a->getStatus();
687
			$statusB = $b->getStatus();
688
689
			$orderA = array_key_exists( $statusA, $order ) ? $order[ $statusA ] : $order[ 'other' ];
690
			$orderB = array_key_exists( $statusB, $order ) ? $order[ $statusB ] : $order[ 'other' ];
691
692
			if ( $orderA === $orderB ) {
693
				$cursorA = $a->getContextCursor();
694
				$cursorB = $b->getContextCursor();
695
696
				if ( $cursorA instanceof EntityContextCursor ) {
697
					return $cursorB instanceof EntityContextCursor ? 0 : -1;
698
				}
699
				if ( $cursorB instanceof EntityContextCursor ) {
700
					return $cursorA instanceof EntityContextCursor ? 0 : 1;
701
				}
702
703
				$pidA = $cursorA->getSnakPropertyId();
704
				$pidB = $cursorB->getSnakPropertyId();
705
706
				if ( $pidA === $pidB ) {
707
					$hashA = $cursorA->getSnakHash();
708
					$hashB = $cursorB->getSnakHash();
709
710
					if ( $hashA === $hashB ) {
711
						if ( $a instanceof NullResult ) {
712
							return $b instanceof NullResult ? 0 : -1;
713
						}
714
						if ( $b instanceof NullResult ) {
715
							return $a instanceof NullResult ? 0 : 1;
716
						}
717
718
						$typeA = $a->getConstraint()->getConstraintTypeItemId();
719
						$typeB = $b->getConstraint()->getConstraintTypeItemId();
720
721
						if ( $typeA == $typeB ) {
722
							return 0;
723
						} else {
724
							return ( $typeA > $typeB ) ? 1 : -1;
725
						}
726
					} else {
727
						return ( $hashA > $hashB ) ? 1 : -1;
728
					}
729
				} else {
730
					return ( $pidA > $pidB ) ? 1 : -1;
731
				}
732
			} else {
733
				return ( $orderA > $orderB ) ? 1 : -1;
734
			}
735
		};
736
737
		uasort( $result, $sortFunction );
738
739
		return $result;
740
	}
741
742
}
743