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