Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.65% covered (success)
93.65%
177 / 189
87.50% covered (warning)
87.50%
28 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
Lexeme
93.65% covered (success)
93.65%
177 / 189
87.50% covered (warning)
87.50%
28 / 32
84.76
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
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 getLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setLanguage
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 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
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
5.20
 increaseNextSenseIdTo
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
5.20
 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
5
 assertCorrectNextSenseIdIsGiven
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
5
 clear
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Wikibase\Lexeme\Domain\Model;
4
5use InvalidArgumentException;
6use OutOfRangeException;
7use UnexpectedValueException;
8use Wikibase\DataModel\Entity\ClearableEntity;
9use Wikibase\DataModel\Entity\ItemId;
10use Wikibase\DataModel\Entity\StatementListProvidingEntity;
11use Wikibase\DataModel\Statement\StatementList;
12use Wikibase\DataModel\Term\TermList;
13use Wikibase\Lexeme\Domain\DummyObjects\BlankForm;
14use 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 */
24class 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}