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 | } |