Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.72% covered (warning)
78.72%
74 / 94
30.00% covered (danger)
30.00%
3 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
LexemePatcher
78.72% covered (warning)
78.72%
74 / 94
30.00% covered (danger)
30.00%
3 / 10
51.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 canPatchEntityType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 patchEntity
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 getPatchedItemId
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 patchNextFormId
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 patchNextSenseId
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
14.72
 patchForms
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 patchAddForm
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 patchSenses
64.29% covered (warning)
64.29%
9 / 14
0.00% covered (danger)
0.00%
0 / 1
9.23
 patchAddSense
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2
3namespace Wikibase\Lexeme\Domain\Diff;
4
5use Diff\DiffOp\Diff\Diff;
6use Diff\DiffOp\DiffOpAdd;
7use Diff\DiffOp\DiffOpChange;
8use Diff\DiffOp\DiffOpRemove;
9use Diff\Patcher\PatcherException;
10use InvalidArgumentException;
11use Wikibase\DataModel\Entity\EntityDocument;
12use Wikibase\DataModel\Entity\ItemId;
13use Wikibase\DataModel\Services\Diff\EntityDiff;
14use Wikibase\DataModel\Services\Diff\EntityPatcherStrategy;
15use Wikibase\DataModel\Services\Diff\StatementListPatcher;
16use Wikibase\DataModel\Services\Diff\TermListPatcher;
17use Wikibase\Lexeme\Domain\Model\Exceptions\ConflictException;
18use Wikibase\Lexeme\Domain\Model\Lexeme;
19use Wikibase\Lexeme\Domain\Model\LexemePatchAccess;
20use Wikimedia\Assert\Assert;
21
22/**
23 * @license GPL-2.0-or-later
24 * @author Amir Sarabadani <ladsgroup@gmail.com>
25 * @author Thiemo Kreuz
26 */
27class LexemePatcher implements EntityPatcherStrategy {
28
29    /**
30     * @var TermListPatcher
31     */
32    private $termListPatcher;
33
34    /**
35     * @var StatementListPatcher
36     */
37    private $statementListPatcher;
38
39    /**
40     * @var FormPatcher
41     */
42    private $formPatcher;
43
44    /**
45     * @var SensePatcher
46     */
47    private $sensePatcher;
48
49    public function __construct() {
50        $this->termListPatcher = new TermListPatcher();
51        $this->statementListPatcher = new StatementListPatcher();
52        $this->formPatcher = new FormPatcher();
53        $this->sensePatcher = new SensePatcher();
54    }
55
56    /**
57     * @param string $entityType
58     *
59     * @return bool
60     */
61    public function canPatchEntityType( $entityType ) {
62        return $entityType === Lexeme::ENTITY_TYPE;
63    }
64
65    /**
66     * @param Lexeme $lexeme
67     * @param LexemeDiff $patch
68     *
69     * @throws InvalidArgumentException
70     */
71    public function patchEntity( EntityDocument $lexeme, EntityDiff $patch ) {
72        Assert::parameterType( Lexeme::class, $lexeme, '$lexeme' );
73        Assert::parameterType( LexemeDiff::class, $patch, '$patch' );
74
75        $this->termListPatcher->patchTermList( $lexeme->getLemmas(), $patch->getLemmasDiff() );
76
77        $this->statementListPatcher->patchStatementList(
78            $lexeme->getStatements(),
79            $patch->getClaimsDiff()
80        );
81
82        $itemId = $this->getPatchedItemId( $patch->getLexicalCategoryDiff() );
83        if ( $itemId !== false ) {
84            $lexeme->setLexicalCategory( $itemId );
85        }
86
87        $itemId = $this->getPatchedItemId( $patch->getLanguageDiff() );
88        if ( $itemId !== false ) {
89            $lexeme->setLanguage( $itemId );
90        }
91
92        $this->patchNextFormId( $lexeme, $patch );
93        $this->patchForms( $lexeme, $patch );
94
95        $this->patchNextSenseId( $lexeme, $patch );
96        $this->patchSenses( $lexeme, $patch );
97    }
98
99    /**
100     * @param Diff $patch
101     *
102     * @throws PatcherException
103     * @return ItemId|null|false False in case the diff is valid, but does not contain a change.
104     */
105    private function getPatchedItemId( Diff $patch ) {
106        if ( $patch->isEmpty() ) {
107            return false;
108        }
109
110        $diffOp = $patch['id'];
111
112        switch ( true ) {
113            case $diffOp instanceof DiffOpAdd:
114                return $diffOp->getNewValue();
115
116            case $diffOp instanceof DiffOpChange:
117                return $diffOp->getNewValue();
118
119            case $diffOp instanceof DiffOpRemove:
120                return null;
121        }
122
123        throw new PatcherException( 'Invalid ItemId diff' );
124    }
125
126    private function patchNextFormId( Lexeme $entity, LexemeDiff $patch ) {
127        // FIXME: Why is this a loop? The nextFormId field is not an array!
128        foreach ( $patch->getNextFormIdDiff() as $nextFormIdDiff ) {
129            if ( !( $nextFormIdDiff instanceof DiffOpChange ) ) {
130                throw new PatcherException( 'Invalid forms list diff' );
131            }
132
133            $newNumber = $nextFormIdDiff->getNewValue();
134            if ( $newNumber > $entity->getNextFormId() ) {
135                $entity->patch( static function ( LexemePatchAccess $patchAccess ) use ( $newNumber ) {
136                    $patchAccess->increaseNextFormIdTo( $newNumber );
137                } );
138            }
139        }
140    }
141
142    private function patchNextSenseId( Lexeme $entity, LexemeDiff $patch ) {
143        // FIXME: Same as above
144        foreach ( $patch->getNextSenseIdDiff() as $nextSenseIdDiff ) {
145            if ( !( $nextSenseIdDiff instanceof DiffOpChange ) ) {
146                throw new PatcherException( 'Invalid senses list diff' );
147            }
148
149            $newNumber = $nextSenseIdDiff->getNewValue();
150            if ( $newNumber > $entity->getNextSenseId() ) {
151                $entity->patch( static function ( LexemePatchAccess $patchAccess ) use ( $newNumber ) {
152                    $patchAccess->increaseNextSenseIdTo( $newNumber );
153                } );
154            }
155        }
156    }
157
158    private function patchForms( Lexeme $lexeme, LexemeDiff $patch ) {
159        foreach ( $patch->getFormsDiff() as $formDiff ) {
160            switch ( true ) {
161                case $formDiff instanceof AddFormDiff:
162                    $this->patchAddForm( $lexeme, $patch, $formDiff );
163                    break;
164
165                case $formDiff instanceof RemoveFormDiff:
166                    $lexeme->removeForm( $formDiff->getRemovedFormId() );
167                    break;
168
169                case $formDiff instanceof ChangeFormDiffOp:
170                    try {
171                        $form = $lexeme->getForm( $formDiff->getFormId() );
172                    } catch ( \OutOfRangeException ) {
173                        // form does not exist (anymore? may have been removed), nothing to patch (T326768)
174                        break;
175                    }
176                    $this->formPatcher->patch( $form, $formDiff );
177                    break;
178
179                default:
180                    throw new PatcherException( 'Invalid forms list diff: ' . get_class( $formDiff ) );
181            }
182        }
183    }
184
185    private function patchAddForm( Lexeme $lexeme, LexemeDiff $patch, AddFormDiff $formDiff ): void {
186        $form = $formDiff->getAddedForm();
187        $lexeme->patch(
188            static function ( LexemePatchAccess $patchAccess ) use ( $form, $patch ) {
189                try {
190                    $patchAccess->addForm( $form );
191                } catch ( ConflictException $e ) {
192                    if ( $patch->getNextFormIdDiff()->isEmpty() ) {
193                        // the patch was restoring, not newly adding, a form;
194                        // if a form by this ID already exists, there’s nothing else to do (T392372)
195                    } else {
196                        throw $e;
197                    }
198                }
199            }
200        );
201    }
202
203    private function patchSenses( Lexeme $lexeme, LexemeDiff $patch ) {
204        foreach ( $patch->getSensesDiff() as $senseDiff ) {
205            switch ( true ) {
206                case $senseDiff instanceof AddSenseDiff:
207                    $this->patchAddSense( $lexeme, $patch, $senseDiff );
208                    break;
209
210                case $senseDiff instanceof RemoveSenseDiff:
211                    $lexeme->removeSense( $senseDiff->getRemovedSenseId() );
212                    break;
213
214                case $senseDiff instanceof ChangeSenseDiffOp:
215                    try {
216                        $sense = $lexeme->getSense( $senseDiff->getSenseId() );
217                    } catch ( \OutOfRangeException ) {
218                        // sense does not exist (anymore? may have been removed), nothing to patch (T284061)
219                        break;
220                    }
221                    $this->sensePatcher->patchEntity( $sense, $senseDiff );
222                    break;
223
224                default:
225                    throw new PatcherException( 'Invalid senses list diff: ' . get_class( $senseDiff ) );
226            }
227        }
228    }
229
230    public function patchAddSense( Lexeme $lexeme, LexemeDiff $patch, AddSenseDiff $senseDiff ): void {
231        $sense = $senseDiff->getAddedSense();
232        $lexeme->patch(
233            static function ( LexemePatchAccess $patchAccess ) use ( $sense, $patch ) {
234                try {
235                    $patchAccess->addSense( $sense );
236                } catch ( ConflictException $e ) {
237                    if ( $patch->getNextSenseIdDiff()->isEmpty() ) {
238                        // the patch was restoring, not newly adding, a sense;
239                        // if a sense by this ID already exists, there’s nothing else to do (T392372)
240                    } else {
241                        throw $e;
242                    }
243                }
244            }
245        );
246    }
247
248}