Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.56% |
172 / 180 |
|
93.75% |
30 / 32 |
CRAP | |
0.00% |
0 / 1 |
| Lexeme | |
95.56% |
172 / 180 |
|
93.75% |
30 / 32 |
74 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
| getId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getStatements | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| isEmpty | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| equals | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
14 | |||
| copy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| __clone | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| getLemmas | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setLemmas | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getLexicalCategory | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| setLexicalCategory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getLanguage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| setLanguage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getForms | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getSenses | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isSufficientlyInitialized | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| getNextFormId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getNextSenseId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getForm | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| getSense | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| addOrUpdateForm | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| addOrUpdateSense | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| removeForm | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| removeSense | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| increaseNextFormIdTo | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
3.19 | |||
| increaseNextSenseIdTo | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
3.19 | |||
| patch | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
1 | |||
| assertCorrectNextFormIdIsGiven | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
4 | |||
| assertCorrectNextSenseIdIsGiven | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
4 | |||
| clear | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare( strict_types = 1 ); |
| 4 | |
| 5 | namespace Wikibase\Lexeme\Domain\Model; |
| 6 | |
| 7 | use InvalidArgumentException; |
| 8 | use OutOfRangeException; |
| 9 | use UnexpectedValueException; |
| 10 | use Wikibase\DataModel\Entity\ClearableEntity; |
| 11 | use Wikibase\DataModel\Entity\ItemId; |
| 12 | use Wikibase\DataModel\Entity\StatementListProvidingEntity; |
| 13 | use Wikibase\DataModel\Statement\StatementList; |
| 14 | use Wikibase\DataModel\Term\TermList; |
| 15 | use Wikibase\Lexeme\Domain\DummyObjects\BlankForm; |
| 16 | use Wikibase\Lexeme\Domain\DummyObjects\BlankSense; |
| 17 | |
| 18 | /** |
| 19 | * Mutable (e.g. the provided StatementList can be changed) implementation of a Lexeme in the |
| 20 | * lexicographical data model. |
| 21 | * |
| 22 | * @see https://www.mediawiki.org/wiki/Extension:WikibaseLexeme/Data_Model#Lexeme |
| 23 | * |
| 24 | * @license GPL-2.0-or-later |
| 25 | */ |
| 26 | class Lexeme implements StatementListProvidingEntity, ClearableEntity { |
| 27 | |
| 28 | public const ENTITY_TYPE = 'lexeme'; |
| 29 | |
| 30 | private ?LexemeId $id; |
| 31 | |
| 32 | private StatementList $statements; |
| 33 | |
| 34 | private TermList $lemmas; |
| 35 | |
| 36 | private ?ItemId $lexicalCategory; |
| 37 | |
| 38 | private ?ItemId $language; |
| 39 | |
| 40 | private FormSet $forms; |
| 41 | |
| 42 | private SenseSet $senses; |
| 43 | |
| 44 | private int $nextFormId; |
| 45 | |
| 46 | private int $nextSenseId; |
| 47 | |
| 48 | /** |
| 49 | * Note that $lexicalCategory and $language can only be null during construction time. Their |
| 50 | * setters can not be called with null, and their getters will throw an exception if the |
| 51 | * corresponding field was never initialized. |
| 52 | */ |
| 53 | public function __construct( |
| 54 | ?LexemeId $id = null, |
| 55 | ?TermList $lemmas = null, |
| 56 | ?ItemId $lexicalCategory = null, |
| 57 | ?ItemId $language = null, |
| 58 | ?StatementList $statements = null, |
| 59 | int $nextFormId = 1, |
| 60 | ?FormSet $forms = null, |
| 61 | int $nextSenseId = 1, |
| 62 | ?SenseSet $senses = null |
| 63 | ) { |
| 64 | $this->id = $id; |
| 65 | $this->lemmas = $lemmas ?: new TermList(); |
| 66 | $this->lexicalCategory = $lexicalCategory; |
| 67 | $this->language = $language; |
| 68 | $this->statements = $statements ?: new StatementList(); |
| 69 | $this->forms = $forms ?: new FormSet( [] ); |
| 70 | $this->senses = $senses ?: new SenseSet( [] ); |
| 71 | |
| 72 | $this->assertCorrectNextFormIdIsGiven( $nextFormId, $this->forms ); |
| 73 | $this->nextFormId = $nextFormId; |
| 74 | $this->assertCorrectNextSenseIdIsGiven( $nextSenseId, $this->senses ); |
| 75 | $this->nextSenseId = $nextSenseId; |
| 76 | } |
| 77 | |
| 78 | public function getId(): ?LexemeId { |
| 79 | return $this->id; |
| 80 | } |
| 81 | |
| 82 | public function getType(): string { |
| 83 | return self::ENTITY_TYPE; |
| 84 | } |
| 85 | |
| 86 | public function getStatements(): StatementList { |
| 87 | return $this->statements; |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * @param LexemeId $id |
| 92 | * |
| 93 | * @throws InvalidArgumentException |
| 94 | */ |
| 95 | public function setId( $id ) { |
| 96 | if ( $id instanceof LexemeId ) { |
| 97 | $this->id = $id; |
| 98 | } else { |
| 99 | throw new InvalidArgumentException( |
| 100 | '$id must be an instance of LexemeId.' |
| 101 | ); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * @return bool A entity is empty if it does not contain any content that can be removed. Note |
| 107 | * that neither ID nor lexical category nor language can be set to null, and are therefor not |
| 108 | * taken into account. |
| 109 | */ |
| 110 | public function isEmpty(): bool { |
| 111 | return $this->lemmas->isEmpty() |
| 112 | && $this->statements->isEmpty() |
| 113 | && $this->forms->isEmpty() |
| 114 | && $this->senses->isEmpty(); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * @see EntityDocument::equals |
| 119 | * |
| 120 | * @param mixed $target |
| 121 | * |
| 122 | * @return bool |
| 123 | */ |
| 124 | public function equals( $target ): bool { |
| 125 | if ( $this === $target ) { |
| 126 | return true; |
| 127 | } |
| 128 | |
| 129 | if ( !( $target instanceof self ) ) { |
| 130 | return false; |
| 131 | } |
| 132 | |
| 133 | $sameLexicalCategory = $this->lexicalCategory === $target->lexicalCategory |
| 134 | || ( $this->lexicalCategory !== null |
| 135 | && $this->lexicalCategory->equals( $target->lexicalCategory ) ); |
| 136 | |
| 137 | $sameLanguage = $this->language === $target->language |
| 138 | || ( $this->language !== null |
| 139 | && $this->language->equals( $target->language ) ); |
| 140 | |
| 141 | $sameFormIdCounter = $this->nextFormId === $target->nextFormId; |
| 142 | $sameForms = $this->forms->equals( $target->forms ); |
| 143 | $sameSenseIdCounter = $this->nextSenseId === $target->nextSenseId; |
| 144 | $sameSenses = $this->senses->equals( $target->senses ); |
| 145 | |
| 146 | return $this->lemmas->equals( $target->lemmas ) |
| 147 | && $sameLexicalCategory |
| 148 | && $sameLanguage |
| 149 | && $sameFormIdCounter |
| 150 | && $sameForms |
| 151 | && $sameSenseIdCounter |
| 152 | && $sameSenses |
| 153 | && $this->statements->equals( $target->statements ); |
| 154 | } |
| 155 | |
| 156 | public function copy(): self { |
| 157 | return clone $this; |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * @see http://php.net/manual/en/language.oop5.cloning.php |
| 162 | */ |
| 163 | public function __clone() { |
| 164 | // TermList is mutable, but Term is not. No deeper cloning necessary. |
| 165 | $this->lemmas = clone $this->lemmas; |
| 166 | $this->statements = clone $this->statements; |
| 167 | $this->forms = clone $this->forms; |
| 168 | $this->senses = clone $this->senses; |
| 169 | } |
| 170 | |
| 171 | public function getLemmas(): TermList { |
| 172 | return $this->lemmas; |
| 173 | } |
| 174 | |
| 175 | public function setLemmas( TermList $lemmas ): void { |
| 176 | $this->lemmas = $lemmas; |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * @throws UnexpectedValueException when the object was constructed with $lexicalCategory set to |
| 181 | * null, and the field was never initialized since then. |
| 182 | */ |
| 183 | public function getLexicalCategory(): ItemId { |
| 184 | if ( !$this->lexicalCategory ) { |
| 185 | throw new UnexpectedValueException( 'Can not access uninitialized field' ); |
| 186 | } |
| 187 | |
| 188 | return $this->lexicalCategory; |
| 189 | } |
| 190 | |
| 191 | public function setLexicalCategory( ?ItemId $lexicalCategory ): void { |
| 192 | $this->lexicalCategory = $lexicalCategory; |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * @throws UnexpectedValueException when the object was constructed with $language set to null, |
| 197 | * and the field was never initialized since then. |
| 198 | */ |
| 199 | public function getLanguage(): ItemId { |
| 200 | if ( !$this->language ) { |
| 201 | throw new UnexpectedValueException( 'Can not access uninitialized field' ); |
| 202 | } |
| 203 | |
| 204 | return $this->language; |
| 205 | } |
| 206 | |
| 207 | public function setLanguage( ?ItemId $language ): void { |
| 208 | $this->language = $language; |
| 209 | } |
| 210 | |
| 211 | public function getForms(): FormSet { |
| 212 | return $this->forms; |
| 213 | } |
| 214 | |
| 215 | public function getSenses(): SenseSet { |
| 216 | return $this->senses; |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * @return bool False if a non-optional field was never initialized, true otherwise. |
| 221 | */ |
| 222 | public function isSufficientlyInitialized(): bool { |
| 223 | return $this->id !== null |
| 224 | && $this->language !== null |
| 225 | && $this->lexicalCategory !== null |
| 226 | && !$this->lemmas->isEmpty(); |
| 227 | } |
| 228 | |
| 229 | public function getNextFormId(): int { |
| 230 | return $this->nextFormId; |
| 231 | } |
| 232 | |
| 233 | public function getNextSenseId(): int { |
| 234 | return $this->nextSenseId; |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * @throws OutOfRangeException |
| 239 | */ |
| 240 | public function getForm( FormId $formId ): Form { |
| 241 | $form = $this->forms->getById( $formId ); |
| 242 | |
| 243 | if ( $form === null ) { |
| 244 | throw new OutOfRangeException( |
| 245 | "Lexeme {$this->id->getSerialization()} doesn't have Form " . |
| 246 | $formId->getSerialization() |
| 247 | ); |
| 248 | } |
| 249 | |
| 250 | return $form; |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * @throws OutOfRangeException if no sense by that ID exists |
| 255 | */ |
| 256 | public function getSense( SenseId $senseId ): Sense { |
| 257 | $sense = $this->senses->getById( $senseId ); |
| 258 | |
| 259 | if ( $sense === null ) { |
| 260 | $lexemeId = $this->id->getSerialization(); |
| 261 | throw new OutOfRangeException( |
| 262 | "Lexeme {$lexemeId} doesn't have sense {$senseId->getSerialization()}" |
| 263 | ); |
| 264 | } |
| 265 | |
| 266 | return $sense; |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Replace the form identified by $form->getId() with the given one or add it. |
| 271 | * |
| 272 | * New form ids are generated for forms with a NullFormId or an unknown DummyFormId. |
| 273 | */ |
| 274 | public function addOrUpdateForm( Form $form ): void { |
| 275 | if ( !$this->id ) { |
| 276 | throw new \LogicException( 'Can not add forms to a lexeme with no ID' ); |
| 277 | } |
| 278 | |
| 279 | if ( $form instanceof BlankForm && !$this->forms->hasFormWithId( $form->getId() ) ) { |
| 280 | $form->setId( |
| 281 | new FormId( |
| 282 | LexemeSubEntityId::formatSerialization( $this->id, 'F', $this->nextFormId++ ) |
| 283 | ) |
| 284 | ); |
| 285 | } |
| 286 | |
| 287 | $this->forms->put( $form ); |
| 288 | |
| 289 | $this->assertCorrectNextFormIdIsGiven( $this->getNextFormId(), $this->getForms() ); |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Replace the sense identified by $sense->getId() with the given one or add it. |
| 294 | */ |
| 295 | public function addOrUpdateSense( Sense $sense ): void { |
| 296 | if ( !$this->id ) { |
| 297 | throw new \LogicException( 'Cannot add sense to a lexeme with no ID' ); |
| 298 | } |
| 299 | |
| 300 | if ( $sense instanceof BlankSense && !$this->senses->hasSenseWithId( $sense->getId() ) ) { |
| 301 | $sense->setId( |
| 302 | new SenseId( |
| 303 | LexemeSubEntityId::formatSerialization( $this->id, 'S', $this->nextSenseId++ ) |
| 304 | ) |
| 305 | ); |
| 306 | } |
| 307 | |
| 308 | $this->senses->put( $sense ); |
| 309 | $this->assertCorrectNextSenseIdIsGiven( $this->getNextSenseId(), $this->getSenses() ); |
| 310 | } |
| 311 | |
| 312 | public function removeForm( FormId $formId ): void { |
| 313 | $this->forms->remove( $formId ); |
| 314 | } |
| 315 | |
| 316 | public function removeSense( SenseId $senseId ): void { |
| 317 | $this->senses->remove( $senseId ); |
| 318 | } |
| 319 | |
| 320 | private function increaseNextFormIdTo( int $number ): void { |
| 321 | if ( $number < $this->nextFormId ) { |
| 322 | throw new \LogicException( |
| 323 | "Cannot increase `nextFormId` because given number is less than counter value " . |
| 324 | "of this Lexeme. Current=`{$this->nextFormId}`, given=`{$number}`" |
| 325 | ); |
| 326 | } |
| 327 | |
| 328 | $this->nextFormId = $number; |
| 329 | } |
| 330 | |
| 331 | private function increaseNextSenseIdTo( int $number ): void { |
| 332 | if ( $number < $this->nextSenseId ) { |
| 333 | throw new \LogicException( |
| 334 | "Cannot increase `nextSenseId` because given number is less than counter value " . |
| 335 | "of this Lexeme. Current=`{$this->nextSenseId}`, given=`{$number}`" |
| 336 | ); |
| 337 | } |
| 338 | |
| 339 | $this->nextSenseId = $number; |
| 340 | } |
| 341 | |
| 342 | public function patch( callable $patcher ): void { |
| 343 | $lexemePatchAccess = new LexemePatchAccess( |
| 344 | $this->nextFormId, |
| 345 | $this->forms, |
| 346 | $this->nextSenseId, |
| 347 | $this->senses |
| 348 | ); |
| 349 | try { |
| 350 | $patcher( $lexemePatchAccess ); |
| 351 | } finally { |
| 352 | $lexemePatchAccess->close(); |
| 353 | } |
| 354 | $newFormSet = $lexemePatchAccess->getForms(); |
| 355 | $newNextFormId = $lexemePatchAccess->getNextFormId(); |
| 356 | $newSenseSet = $lexemePatchAccess->getSenses(); |
| 357 | $newNextSenseId = $lexemePatchAccess->getNextSenseId(); |
| 358 | |
| 359 | $this->assertCorrectNextFormIdIsGiven( $newNextFormId, $newFormSet ); |
| 360 | $this->assertCorrectNextSenseIdIsGiven( $newNextSenseId, $newSenseSet ); |
| 361 | |
| 362 | $this->increaseNextFormIdTo( $newNextFormId ); |
| 363 | $this->forms = $newFormSet; |
| 364 | $this->increaseNextSenseIdTo( $newNextSenseId ); |
| 365 | $this->senses = $newSenseSet; |
| 366 | } |
| 367 | |
| 368 | private function assertCorrectNextFormIdIsGiven( int $nextFormId, FormSet $formSet ): void { |
| 369 | if ( $nextFormId < 1 ) { |
| 370 | throw new \InvalidArgumentException( '$nextFormId should be a positive integer' ); |
| 371 | } |
| 372 | |
| 373 | if ( $nextFormId <= $formSet->count() ) { |
| 374 | throw new \LogicException( |
| 375 | sprintf( |
| 376 | '$nextFormId must always be greater than the number of Forms. ' . |
| 377 | '$nextFormId = `%s`, number of forms = `%s`', |
| 378 | $nextFormId, |
| 379 | $formSet->count() |
| 380 | ) |
| 381 | ); |
| 382 | } |
| 383 | |
| 384 | if ( $nextFormId <= $formSet->maxFormIdNumber() ) { |
| 385 | throw new \LogicException( |
| 386 | sprintf( |
| 387 | '$nextFormId must always be greater than the max ID number of provided Forms. ' . |
| 388 | '$nextFormId = `%s`, max ID number of provided Forms = `%s`', |
| 389 | $nextFormId, |
| 390 | $formSet->maxFormIdNumber() |
| 391 | ) |
| 392 | ); |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | private function assertCorrectNextSenseIdIsGiven( int $nextSenseId, SenseSet $senseSet ): void { |
| 397 | if ( $nextSenseId < 1 ) { |
| 398 | throw new InvalidArgumentException( '$nextSenseId should be a positive integer' ); |
| 399 | } |
| 400 | |
| 401 | if ( $nextSenseId <= $senseSet->count() ) { |
| 402 | throw new \LogicException( |
| 403 | sprintf( |
| 404 | '$nextSenseId must always be greater than the number of senses. ' . |
| 405 | '$nextSenseId = `%s`, number of senses = `%s`', |
| 406 | $nextSenseId, |
| 407 | count( $senseSet ) |
| 408 | ) |
| 409 | ); |
| 410 | } |
| 411 | |
| 412 | if ( $nextSenseId <= $senseSet->maxSenseIdNumber() ) { |
| 413 | throw new \LogicException( |
| 414 | sprintf( |
| 415 | '$nextSenseId must always be greater than the max ID number of provided senses. ' . |
| 416 | '$nextSenseId = `%s`, max ID number of provided senses = `%s`', |
| 417 | $nextSenseId, |
| 418 | $senseSet->maxSenseIdNumber() |
| 419 | ) |
| 420 | ); |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | /** |
| 425 | * Clears lemmas, language, lexical category, statements, forms, and senses of the lexeme. |
| 426 | * Note that this leaves the lexeme in an insufficiently initialized state. |
| 427 | */ |
| 428 | public function clear(): void { |
| 429 | $this->lemmas = new TermList(); |
| 430 | $this->statements = new StatementList(); |
| 431 | $this->forms = new FormSet( [] ); |
| 432 | $this->senses = new SenseSet( [] ); |
| 433 | $this->language = null; |
| 434 | $this->lexicalCategory = null; |
| 435 | } |
| 436 | |
| 437 | } |