MockRepository::saveEntity()   B
last analyzed

Complexity

Conditions 11
Paths 20

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 7.3166
c 0
b 0
f 0
cc 11
nc 20
nop 6

How to fix   Complexity   

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 Wikibase\Lib\Tests;
4
5
use InvalidArgumentException;
6
use Status;
7
use User;
8
use Wikibase\DataModel\Entity\BasicEntityIdParser;
9
use Wikibase\DataModel\Entity\EntityDocument;
10
use Wikibase\DataModel\Entity\EntityId;
11
use Wikibase\DataModel\Entity\EntityRedirect;
12
use Wikibase\DataModel\Entity\Int32EntityId;
13
use Wikibase\DataModel\Entity\Item;
14
use Wikibase\DataModel\Entity\ItemId;
15
use Wikibase\DataModel\Entity\Property;
16
use Wikibase\DataModel\Entity\PropertyId;
17
use Wikibase\DataModel\Services\Lookup\EntityLookup;
18
use Wikibase\DataModel\Services\Lookup\EntityRedirectLookup;
19
use Wikibase\DataModel\Services\Lookup\EntityRedirectLookupException;
20
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
21
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookupException;
22
use Wikibase\DataModel\SiteLink;
23
use Wikibase\DataModel\Term\LabelsProvider;
24
use Wikibase\Lib\Store\EntityRevision;
25
use Wikibase\Lib\Store\EntityRevisionLookup;
26
use Wikibase\Lib\Store\EntityStore;
27
use Wikibase\Lib\Store\HashSiteLinkStore;
28
use Wikibase\Lib\Store\LatestRevisionIdResult;
29
use Wikibase\Lib\Store\LookupConstants;
30
use Wikibase\Lib\Store\RedirectRevision;
31
use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException;
32
use Wikibase\Lib\Store\SiteLinkLookup;
33
use Wikibase\Lib\Store\SiteLinkStore;
34
use Wikibase\Lib\Store\StorageException;
35
36
/**
37
 * @deprecated Try to use a simpler fake. The complexity and coupling of this
38
 * test double are very high, so it is good to avoid binding to it.
39
 *
40
 * Mock repository for use in tests.
41
 *
42
 * @license GPL-2.0-or-later
43
 * @author Daniel Kinzler
44
 * @author Thiemo Kreuz
45
 */
46
class MockRepository implements EntityLookup, EntityRedirectLookup,
47
	EntityRevisionLookup, EntityStore, PropertyDataTypeLookup, SiteLinkLookup {
48
49
	/**
50
	 * @var SiteLinkStore
51
	 */
52
	private $siteLinkStore;
53
54
	/**
55
	 * Entity id serialization => array of EntityRevision
56
	 *
57
	 * @var array[]
58
	 */
59
	private $entities = [];
60
61
	/**
62
	 * Log entries. Each entry has the following fields:
63
	 * revision, entity, summary, user, tags
64
	 *
65
	 * @var array[]
66
	 */
67
	private $log = [];
68
69
	/**
70
	 * Entity id serialization => EntityRedirect
71
	 *
72
	 * @var RedirectRevision[]
73
	 */
74
	private $redirects = [];
75
76
	/**
77
	 * User ID + Entity Id -> bool
78
	 *
79
	 * @var bool[]
80
	 */
81
	private $watchlist = [];
82
83
	/**
84
	 * @var int
85
	 */
86
	private $maxEntityId = 0;
87
88
	/**
89
	 * @var int
90
	 */
91
	private $maxRevisionId = 0;
92
93
	public function __construct() {
94
		$this->siteLinkStore = new HashSiteLinkStore();
95
	}
96
97
	/**
98
	 * @see EntityLookup::getEntity
99
	 *
100
	 * @param EntityId $entityId
101
	 *
102
	 * @return EntityDocument|null
103
	 * @throws StorageException
104
	 */
105
	public function getEntity( EntityId $entityId ) {
106
		$revision = $this->getEntityRevision( $entityId );
107
108
		return $revision === null ? null : $revision->getEntity()->copy();
109
	}
110
111
	/**
112
	 * @see EntityRevisionLookup::getEntityRevision
113
	 *
114
	 * @param EntityId $entityId
115
	 * @param int $revisionId The desired revision id, or 0 for the latest revision.
116
	 * @param string $mode LATEST_FROM_REPLICA, LATEST_FROM_REPLICA_WITH_FALLBACK or
117
	 *        LATEST_FROM_MASTER.
118
	 *
119
	 * @throws RevisionedUnresolvedRedirectException
120
	 * @throws StorageException
121
	 * @return EntityRevision|null
122
	 */
123
	public function getEntityRevision(
124
		EntityId $entityId,
125
		$revisionId = 0,
126
		$mode = LookupConstants::LATEST_FROM_REPLICA
127
	) {
128
		$key = $entityId->getSerialization();
129
130
		if ( isset( $this->redirects[$key] ) ) {
131
			$redirRev = $this->redirects[$key];
132
			throw new RevisionedUnresolvedRedirectException(
133
				$entityId,
134
				$redirRev->getRedirect()->getTargetId(),
135
				$redirRev->getRevisionId(),
136
				$redirRev->getTimestamp()
137
			);
138
		}
139
140
		if ( empty( $this->entities[$key] ) ) {
141
			return null;
142
		}
143
144
		if ( !is_int( $revisionId ) ) {
145
			wfWarn( 'getEntityRevision() called with $revisionId = false or a string, use 0 instead.' );
146
			$revisionId = 0;
147
		}
148
149
		/** @var EntityRevision[] $revisions */
150
		$revisions = $this->entities[$key];
151
152
		if ( $revisionId === 0 ) {
153
			$revisionIds = array_keys( $revisions );
154
			$revisionId = end( $revisionIds );
155
		} elseif ( !isset( $revisions[$revisionId] ) ) {
156
			throw new StorageException( "no such revision for entity $key: $revisionId" );
157
		}
158
159
		$revision = $revisions[$revisionId];
160
		$revision = new EntityRevision( // return a copy!
161
			$revision->getEntity()->copy(), // return a copy!
162
			$revision->getRevisionId(),
163
			$revision->getTimestamp()
164
		);
165
166
		return $revision;
167
	}
168
169
	/**
170
	 * See EntityLookup::hasEntity()
171
	 *
172
	 * @param EntityId $entityId
173
	 *
174
	 * @return bool
175
	 */
176
	public function hasEntity( EntityId $entityId ) {
177
		return $this->getEntity( $entityId ) !== null;
178
	}
179
180
	/**
181
	 * @see SiteLinkLookup::getItemIdForLink
182
	 *
183
	 * @param string $globalSiteId
184
	 * @param string $pageTitle
185
	 *
186
	 * @return ItemId|null
187
	 */
188
	public function getItemIdForLink( $globalSiteId, $pageTitle ) {
189
		return $this->siteLinkStore->getItemIdForLink( $globalSiteId, $pageTitle );
190
	}
191
192
	/**
193
	 * @see SiteLinkLookup::getItemIdForSiteLink
194
	 *
195
	 * @param SiteLink $siteLink
196
	 *
197
	 * @return ItemId|null
198
	 */
199
	public function getItemIdForSiteLink( SiteLink $siteLink ) {
200
		return $this->siteLinkStore->getItemIdForSiteLink( $siteLink );
201
	}
202
203
	/**
204
	 * Registers the sitelinks of the given Item so they can later be found with getLinks, etc
205
	 *
206
	 * @param Item $item
207
	 */
208
	private function registerSiteLinks( Item $item ) {
209
		$this->siteLinkStore->saveLinksOfItem( $item );
210
	}
211
212
	/**
213
	 * Unregisters the sitelinks of the given Item so they are no longer found with getLinks, etc
214
	 *
215
	 * @param ItemId $itemId
216
	 */
217
	private function unregisterSiteLinks( ItemId $itemId ) {
218
		$this->siteLinkStore->deleteLinksOfItem( $itemId );
219
	}
220
221
	/**
222
	 * Puts an entity into the mock repository. If there already is an entity with the same ID
223
	 * in the mock repository, it is not removed, but replaced as the current one. If a revision
224
	 * ID is given, the entity with the highest revision ID is considered the current one.
225
	 *
226
	 * @param EntityDocument $entity
227
	 * @param int $revisionId
228
	 * @param int|string $timestamp
229
	 * @param User|string|null $user
230
	 *
231
	 * @throws StorageException
232
	 * @return EntityRevision
233
	 */
234
	public function putEntity( EntityDocument $entity, $revisionId = 0, $timestamp = 0, $user = null ) {
235
		if ( $entity->getId() === null ) {
236
			$this->assignFreshId( $entity );
237
		}
238
239
		$oldEntity = $this->getEntity( $entity->getId() );
0 ignored issues
show
Bug introduced by
It seems like $entity->getId() can be null; however, getEntity() 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...
240
241
		if ( $oldEntity && ( $oldEntity instanceof Item ) ) {
242
			// clean up old sitelinks
243
			$this->unregisterSiteLinks( $entity->getId() );
0 ignored issues
show
Documentation introduced by
$entity->getId() is of type null|object<Wikibase\DataModel\Entity\EntityId>, but the function expects a object<Wikibase\DataModel\Entity\ItemId>.

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...
244
		}
245
246
		if ( $entity instanceof Item ) {
247
			// add new sitelinks
248
			$this->registerSiteLinks( $entity );
249
		}
250
251
		if ( $revisionId === 0 ) {
252
			$revisionId = ++$this->maxRevisionId;
253
		}
254
255
		$this->updateMaxNumericId( $entity->getId() );
0 ignored issues
show
Bug introduced by
It seems like $entity->getId() can be null; however, updateMaxNumericId() 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...
256
		$this->maxRevisionId = max( $this->maxRevisionId, $revisionId );
257
258
		$revision = new EntityRevision(
259
			$entity->copy(), // note: always clone
260
			$revisionId,
261
			wfTimestamp( TS_MW, $timestamp )
262
		);
263
264
		if ( $user !== null ) {
265
			if ( $user instanceof User ) {
0 ignored issues
show
Bug introduced by
The class User does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
266
				$user = $user->getName();
267
			}
268
269
			// just glue the user on here...
270
			$revision->user = $user;
0 ignored issues
show
Bug introduced by
The property user does not seem to exist in Wikibase\Lib\Store\EntityRevision.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
271
		}
272
273
		$key = $entity->getId()->getSerialization();
274
		unset( $this->redirects[$key] );
275
276
		if ( !array_key_exists( $key, $this->entities ) ) {
277
			$this->entities[$key] = [];
278
		}
279
		$this->entities[$key][$revisionId] = $revision;
280
		ksort( $this->entities[$key] );
281
282
		return $revision;
283
	}
284
285
	/**
286
	 * Puts a redirect into the mock repository. If there already is an entity with the same ID
287
	 * in the mock repository, it is replaced with the redirect.
288
	 *
289
	 * @param EntityRedirect $redirect
290
	 * @param int $revisionId
291
	 * @param string|int $timestamp
292
	 *
293
	 * @throws StorageException
294
	 */
295
	public function putRedirect( EntityRedirect $redirect, $revisionId = 0, $timestamp = 0 ) {
296
		$key = $redirect->getEntityId()->getSerialization();
297
298
		if ( isset( $this->entities[$key] ) ) {
299
			$this->removeEntity( $redirect->getEntityId() );
300
		}
301
302
		if ( $revisionId === 0 ) {
303
			$revisionId = ++$this->maxRevisionId;
304
		}
305
306
		$this->updateMaxNumericId( $redirect->getTargetId() );
307
		$this->maxRevisionId = max( $this->maxRevisionId, $revisionId );
308
309
		$this->redirects[$key] = new RedirectRevision(
310
			$redirect, // EntityRedirect is immutable
311
			$revisionId,
312
			wfTimestamp( TS_MW, $timestamp )
313
		);
314
	}
315
316
	/**
317
	 * Removes an entity from the mock repository.
318
	 *
319
	 * @param EntityId $entityId
320
	 *
321
	 * @return EntityDocument
322
	 */
323
	public function removeEntity( EntityId $entityId ) {
324
		try {
325
			$oldEntity = $this->getEntity( $entityId );
326
327
			if ( $oldEntity && ( $oldEntity instanceof Item ) ) {
328
				// clean up old sitelinks
329
				$this->unregisterSiteLinks( $entityId );
0 ignored issues
show
Compatibility introduced by
$entityId of type object<Wikibase\DataModel\Entity\EntityId> is not a sub-type of object<Wikibase\DataModel\Entity\ItemId>. 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...
330
			}
331
		} catch ( StorageException $ex ) {
332
			$oldEntity = null; // ignore
333
		}
334
335
		$key = $entityId->getSerialization();
336
		unset( $this->entities[$key] );
337
		unset( $this->redirects[$key] );
338
339
		return $oldEntity;
340
	}
341
342
	/**
343
	 * @see SiteLinkLookup::getLinks
344
	 *
345
	 * @param int[] $numericIds Numeric (unprefixed) item ids
346
	 * @param string[] $siteIds
347
	 * @param string[] $pageNames
348
	 *
349
	 * @return array[]
350
	 */
351
	public function getLinks( array $numericIds = [], array $siteIds = [], array $pageNames = [] ) {
352
		return $this->siteLinkStore->getLinks( $numericIds, $siteIds, $pageNames );
353
	}
354
355
	/**
356
	 * Fetches the entities with provided ids and returns them.
357
	 * The result array contains the prefixed entity ids as keys.
358
	 * The values are either an EntityDocument or null, if there is no entity with the associated id.
359
	 *
360
	 * The revisions can be specified as an array holding an integer element for each
361
	 * id in the $entityIds array or false for latest. If all should be latest, false
362
	 * can be provided instead of an array.
363
	 *
364
	 * @param EntityId[] $entityIds
365
	 *
366
	 * @return EntityDocument[]|null[]
367
	 */
368
	public function getEntities( array $entityIds ) {
369
		$entities = [];
370
371
		foreach ( $entityIds as $entityId ) {
372
			if ( is_string( $entityId ) ) {
373
				$entityId = $this->parseId( $entityId );
374
			}
375
376
			$entities[$entityId->getSerialization()] = $this->getEntity( $entityId );
377
		}
378
379
		return $entities;
380
	}
381
382
	/**
383
	 * @see SiteLinkLookup::getSiteLinksForItem
384
	 *
385
	 * @param ItemId $itemId
386
	 *
387
	 * @return SiteLink[]
388
	 */
389
	public function getSiteLinksForItem( ItemId $itemId ) {
390
		return $this->siteLinkStore->getSiteLinksForItem( $itemId );
391
	}
392
393
	/**
394
	 * @param string $propertyLabel
395
	 * @param string $languageCode
396
	 *
397
	 * @return EntityDocument|null
398
	 */
399
	public function getPropertyByLabel( $propertyLabel, $languageCode ) {
400
		foreach ( array_keys( $this->entities ) as $idString ) {
401
			$propertyId = $this->parseId( $idString );
402
403
			if ( !( $propertyId instanceof PropertyId ) ) {
404
				continue;
405
			}
406
407
			$property = $this->getEntity( $propertyId );
408
409
			if ( !( $property instanceof LabelsProvider ) ) {
410
				continue;
411
			}
412
413
			$labels = $property->getLabels();
414
415
			if ( $labels->hasTermForLanguage( $languageCode )
416
				&& $labels->getByLanguage( $languageCode )->getText() === $propertyLabel
417
			) {
418
				return $property;
419
			}
420
		}
421
422
		return null;
423
	}
424
425
	/**
426
	 * @see PropertyDataTypeLookup::getDataTypeIdForProperty
427
	 *
428
	 * @param PropertyId $propertyId
429
	 *
430
	 * @return string
431
	 * @throws PropertyDataTypeLookupException
432
	 */
433
	public function getDataTypeIdForProperty( PropertyId $propertyId ) {
434
		$entity = $this->getEntity( $propertyId );
435
436
		if ( $entity instanceof Property ) {
437
			return $entity->getDataTypeId();
438
		}
439
440
		throw new PropertyDataTypeLookupException( $propertyId );
441
	}
442
443
	/**
444
	 * @see EntityRevisionLookup::getLatestRevisionId
445
	 *
446
	 * @param EntityId $entityId
447
	 * @param string $mode
448
	 *
449
	 * @return LatestRevisionIdResult
450
	 */
451
	public function getLatestRevisionId( EntityId $entityId, $mode = LookupConstants::LATEST_FROM_REPLICA ) {
452
		try {
453
			$revision = $this->getEntityRevision( $entityId, 0, $mode );
454
		} catch ( RevisionedUnresolvedRedirectException $e ) {
455
			return LatestRevisionIdResult::redirect( $e->getRevisionId(), $e->getRedirectTargetId() );
456
		}
457
458
		return $revision === null
459
			? LatestRevisionIdResult::nonexistentEntity()
460
			: LatestRevisionIdResult::concreteRevision( $revision->getRevisionId() );
461
	}
462
463
	/**
464
	 * Stores the given Entity.
465
	 *
466
	 * @param EntityDocument $entity the entity to save.
467
	 * @param string $summary ignored
468
	 * @param User $user ignored
469
	 * @param int $flags EDIT_XXX flags, as defined for WikiPage::doEditContent.
470
	 * @param int|bool $baseRevisionId the revision ID $entity is based on. Saving should fail if
471
	 * $baseRevId is no longer the current revision.
472
	 * @param string[] $tags added to log entry
473
	 *
474
	 * @see WikiPage::doEditContent
475
	 *
476
	 * @return EntityRevision
477
	 * @throws StorageException
478
	 */
479
	public function saveEntity( EntityDocument $entity, $summary, User $user, $flags = 0, $baseRevisionId = false, array $tags = [] ) {
480
		$entityId = $entity->getId();
481
482
		$status = Status::newGood();
483
484
		if ( ( $flags & EDIT_NEW ) && $entityId && $this->hasEntity( $entityId ) ) {
485
			$status->fatal( 'edit-already-exists' );
486
		}
487
488
		if ( ( $flags & EDIT_UPDATE ) && !$this->hasEntity( $entityId ) ) {
0 ignored issues
show
Bug introduced by
It seems like $entityId defined by $entity->getId() on line 480 can be null; however, Wikibase\Lib\Tests\MockRepository::hasEntity() 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...
489
			$status->fatal( 'edit-gone-missing' );
490
		}
491
492
		if ( $baseRevisionId !== false && !$this->hasEntity( $entityId ) ) {
0 ignored issues
show
Bug introduced by
It seems like $entityId defined by $entity->getId() on line 480 can be null; however, Wikibase\Lib\Tests\MockRepository::hasEntity() 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...
493
			//TODO: find correct message key to use with status??
494
			throw new StorageException( 'No base revision found for ' . $entityId->getSerialization() );
495
		}
496
497
		if ( $baseRevisionId !== false && $this->getEntityRevision( $entityId )->getRevisionId() !== $baseRevisionId ) {
0 ignored issues
show
Bug introduced by
It seems like $entityId defined by $entity->getId() on line 480 can be null; however, Wikibase\Lib\Tests\MockR...ry::getEntityRevision() 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...
498
			$status->fatal( 'edit-conflict' );
499
		}
500
501
		if ( !$status->isOK() ) {
502
			throw new StorageException( $status );
503
		}
504
505
		$revision = $this->putEntity( $entity, 0, 0, $user );
506
507
		$this->putLog( $revision->getRevisionId(), $entity->getId(), $summary, $user->getName(), $tags );
508
		return $revision;
509
	}
510
511
	/**
512
	 * @see EntityStore::saveRedirect
513
	 *
514
	 * @param EntityRedirect $redirect
515
	 * @param string $summary
516
	 * @param User $user
517
	 * @param int $flags
518
	 * @param int|bool $baseRevisionId
519
	 *
520
	 * @throws StorageException If the given type of entity does not support redirects
521
	 * @return int The revision id created by storing the redirect
522
	 */
523
	public function saveRedirect( EntityRedirect $redirect, $summary, User $user, $flags = 0, $baseRevisionId = false ) {
524
		if ( !( $redirect->getEntityId() instanceof ItemId ) ) {
525
			throw new StorageException( 'Entity type does not support redirects: ' . $redirect->getEntityId()->getEntityType() );
526
		}
527
528
		$this->putRedirect( $redirect );
529
530
		$revisionId = ++$this->maxRevisionId;
531
		$this->putLog( $revisionId, $redirect->getEntityId(), $summary, $user->getName() );
532
533
		return $revisionId;
534
	}
535
536
	/**
537
	 * Deletes the given entity in some underlying storage mechanism.
538
	 *
539
	 * @param EntityId $entityId
540
	 * @param string $reason the reason for deletion
541
	 * @param User $user
542
	 */
543
	public function deleteEntity( EntityId $entityId, $reason, User $user ) {
544
		$this->removeEntity( $entityId );
545
	}
546
547
	/**
548
	 * Check if no edits were made by other users since the given revision.
549
	 * This makes the assumption that revision ids are monotonically increasing.
550
	 *
551
	 * @see EditPage::userWasLastToEdit
552
	 *
553
	 * @param User $user
554
	 * @param EntityId $entityId the entity to check
555
	 * @param int $lastRevisionId the revision to check from
556
	 *
557
	 * @return bool
558
	 */
559
	public function userWasLastToEdit( User $user, EntityId $entityId, $lastRevisionId ) {
560
		$key = $entityId->getSerialization();
561
		if ( !isset( $this->entities[$key] ) ) {
562
			return false;
563
		}
564
565
		/** @var EntityRevision $revision */
566
		foreach ( $this->entities[$key] as $revision ) {
567
			if ( $revision->getRevisionId() >= $lastRevisionId ) {
568
				if ( isset( $revision->user ) && $revision->user !== $user->getName() ) {
569
					return false;
570
				}
571
			}
572
		}
573
574
		return true;
575
	}
576
577
	/**
578
	 * Watches or unwatches the entity.
579
	 *
580
	 * @param User $user
581
	 * @param EntityId $entityId the entity to watch
582
	 * @param bool $watch whether to watch or unwatch the page.
583
	 */
584
	public function updateWatchlist( User $user, EntityId $entityId, $watch ) {
585
		if ( $watch ) {
586
			$this->watchlist[ $user->getName() ][ $entityId->getSerialization() ] = true;
587
		} else {
588
			unset( $this->watchlist[ $user->getName() ][ $entityId->getSerialization() ] );
589
		}
590
	}
591
592
	/**
593
	 * Determines whether the given user is watching the given item
594
	 *
595
	 * @param User $user
596
	 * @param EntityId $entityId the entity to watch
597
	 *
598
	 * @return bool
599
	 */
600
	public function isWatching( User $user, EntityId $entityId ) {
601
		return isset( $this->watchlist[ $user->getName() ][ $entityId->getSerialization() ] );
602
	}
603
604
	/**
605
	 * @param EntityId $id
606
	 *
607
	 * @throws StorageException
608
	 */
609
	private function updateMaxNumericId( EntityId $id ) {
610
		if ( !( $id instanceof Int32EntityId ) ) {
611
			throw new StorageException( 'This class does not support non-numeric entity types' );
612
		}
613
614
		$this->maxEntityId = max( $this->maxEntityId, $id->getNumericId() );
615
	}
616
617
	/**
618
	 * @see EntityStore::assignFreshId
619
	 *
620
	 * @param EntityDocument $entity
621
	 *
622
	 * @throws InvalidArgumentException when the entity type does not support setting numeric ids.
623
	 */
624
	public function assignFreshId( EntityDocument $entity ) {
625
		//TODO: Find a canonical way to generate an EntityId from the maxId number.
626
		//XXX: Using setId() with an integer argument is deprecated!
627
		$numericId = ++$this->maxEntityId;
628
629
		if ( $entity instanceof Item ) {
630
			$entity->setId( ItemId::newFromNumber( $numericId ) );
631
			return;
632
		}
633
634
		if ( $entity instanceof Property ) {
635
			$entity->setId( PropertyId::newFromNumber( $numericId ) );
636
			return;
637
		}
638
639
		throw new \RuntimeException( 'Cannot create a new ID for non-items and non-properties' );
640
	}
641
642
	/**
643
	 * @param string $idString
644
	 *
645
	 * @return ItemId|PropertyId
646
	 */
647
	private function parseId( $idString ) {
648
		$parser = new BasicEntityIdParser();
649
		return $parser->parse( $idString );
650
	}
651
652
	/**
653
	 * @param int $revisionId
654
	 * @param EntityId|string $entityId
655
	 * @param string $summary
656
	 * @param User|string $user
657
	 * @param string[] $tags
658
	 */
659
	private function putLog( $revisionId, $entityId, $summary, $user, array $tags = [] ) {
660
		if ( $entityId instanceof EntityId ) {
661
			$entityId = $entityId->getSerialization();
662
		}
663
664
		if ( $user instanceof User ) {
0 ignored issues
show
Bug introduced by
The class User does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
665
			$user = $user->getName();
666
		}
667
668
		$this->log[$revisionId] = [
669
			'revision' => $revisionId,
670
			'entity' => $entityId,
671
			'summary' => $summary,
672
			'user' => $user,
673
			'tags' => $tags,
674
		];
675
	}
676
677
	/**
678
	 * Returns the log entry for the given revision Id.
679
	 *
680
	 * @param int $revisionId
681
	 *
682
	 * @return array|null An associative array containing the fields
683
	 * 'revision', 'entity', 'summary', 'user', and 'tags'.
684
	 */
685
	public function getLogEntry( $revisionId ) {
686
		return array_key_exists( $revisionId, $this->log ) ? $this->log[$revisionId] : null;
687
	}
688
689
	/**
690
	 * Returns the newest (according to the revision id) log entry
691
	 * for the given entity.
692
	 *
693
	 * @param EntityId|string $entityId
694
	 *
695
	 * @return array|null An associative array containing the fields
696
	 * 'revision', 'entity', 'summary', 'user', and 'tags'.
697
	 */
698
	public function getLatestLogEntryFor( $entityId ) {
699
		if ( $entityId instanceof EntityId ) {
700
			$entityId = $entityId->getSerialization();
701
		}
702
703
		// log entries by revision id, largest id first.
704
		$log = $this->log;
705
		krsort( $log );
706
707
		foreach ( $log as $entry ) {
708
			if ( $entry['entity'] === $entityId ) {
709
				return $entry;
710
			}
711
		}
712
713
		return null;
714
	}
715
716
	/**
717
	 * Returns the IDs that redirect to (are aliases of) the given target entity.
718
	 *
719
	 * @param EntityId $targetId
720
	 *
721
	 * @return EntityId[]
722
	 */
723
	public function getRedirectIds( EntityId $targetId ) {
724
		$redirects = [];
725
726
		foreach ( $this->redirects as $redirRev ) {
727
			$redir = $redirRev->getRedirect();
728
			if ( $redir->getTargetId()->equals( $targetId ) ) {
729
				$redirects[] = $redir->getEntityId();
730
			}
731
		}
732
733
		return $redirects;
734
	}
735
736
	/**
737
	 * Returns the redirect target associated with the given redirect ID.
738
	 *
739
	 * @param EntityId $entityId
740
	 * @param string $forUpdate
741
	 *
742
	 * @return EntityId|null The ID of the redirect target, or null if $entityId
743
	 *         does not refer to a redirect
744
	 * @throws EntityRedirectLookupException
745
	 */
746
	public function getRedirectForEntityId( EntityId $entityId, $forUpdate = '' ) {
747
		$key = $entityId->getSerialization();
748
749
		if ( isset( $this->redirects[$key] ) ) {
750
			return $this->redirects[$key]->getRedirect()->getTargetId();
751
		}
752
753
		if ( isset( $this->entities[$key] ) ) {
754
			return null;
755
		}
756
757
		throw new EntityRedirectLookupException( $entityId );
758
	}
759
760
	/**
761
	 * @see EntityStore::canCreateWithCustomId
762
	 *
763
	 * @param EntityId $id
764
	 *
765
	 * @return bool
766
	 */
767
	public function canCreateWithCustomId( EntityId $id ) {
768
		return false;
769
	}
770
771
}
772