Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.56% covered (success)
95.56%
172 / 180
93.75% covered (success)
93.75%
30 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
Lexeme
95.56% covered (success)
95.56%
172 / 180
93.75% covered (success)
93.75%
30 / 32
74
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatements
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isEmpty
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 equals
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
14
 copy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __clone
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getLemmas
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLemmas
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLexicalCategory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setLexicalCategory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getForms
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSenses
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSufficientlyInitialized
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getNextFormId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNextSenseId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getForm
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getSense
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addOrUpdateForm
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addOrUpdateSense
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 removeForm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeSense
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 increaseNextFormIdTo
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 increaseNextSenseIdTo
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 patch
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 assertCorrectNextFormIdIsGiven
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 assertCorrectNextSenseIdIsGiven
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 clear
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikibase\Lexeme\Domain\Model;
6
7use InvalidArgumentException;
8use OutOfRangeException;
9use UnexpectedValueException;
10use Wikibase\DataModel\Entity\ClearableEntity;
11use Wikibase\DataModel\Entity\ItemId;
12use Wikibase\DataModel\Entity\StatementListProvidingEntity;
13use Wikibase\DataModel\Statement\StatementList;
14use Wikibase\DataModel\Term\TermList;
15use Wikibase\Lexeme\Domain\DummyObjects\BlankForm;
16use 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 */
26class 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}