Passed
Push — master ( d0e7a6...44374d )
by Dispositif
02:28
created

OuvrageComplete::processLienAuteur()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 24
rs 9.1111
1
<?php
2
/**
3
 * This file is part of dispositif/wikibot application
4
 * 2019 : Philippe M. <[email protected]>
5
 * For the full copyright and MIT license information, please view the LICENSE file.
6
 */
7
8
declare(strict_types=1);
9
10
namespace App\Domain;
11
12
use App\Domain\Models\Wiki\GoogleLivresTemplate;
13
use App\Domain\Models\Wiki\OuvrageTemplate;
14
use App\Domain\Utils\TextUtil;
15
use App\Domain\Utils\WikiTextUtil;
16
use Exception;
17
use Normalizer;
18
19
class OuvrageComplete
20
{
21
    const ADD_PRESENTATION_EN_LIGNE = true;
22
23
    const WIKI_LANGUAGE = 'fr';
24
25
    /**
26
     * @var OuvrageTemplate
27
     */
28
    private $origin;
29
30
    private $book;
31
32
    public $major = false;
33
34
    public $notCosmetic = false;
35
36
    private $log = [];
37
38
    private $sameBook;
39
40
    //todo: injection référence base ou mapping ? (Google
41
    public function __construct(OuvrageTemplate $origin, OuvrageTemplate $book)
42
    {
43
        $this->origin = clone $origin;
44
        $this->book = $book;
45
    }
46
47
    public function getLog(): array
48
    {
49
        return $this->log;
50
    }
51
52
    /**
53
     * @return OuvrageTemplate
54
     * @throws Exception
55
     */
56
    public function getResult()
57
    {
58
        $this->complete();
59
60
        return $this->origin;
61
    }
62
63
    /**
64
     * @return bool
65
     * @throws Exception
66
     */
67
    private function complete()
68
    {
69
        // si livre suspect, on stoppe
70
        $this->sameBook = $this->predictSameBook();
71
        if (!$this->sameBook) {
72
            dump('not same book');
73
74
            return false;
75
        }
76
77
        $skipParam = [
78
            'isbn invalide',
79
            'auteurs',
80
            'auteur1',
81
            'prénom1',
82
            'nom1',
83
            'auteur2',
84
            'prénom2',
85
            'nom2',
86
            'auteur3',
87
            'prénom3',
88
            'nom3',
89
            'auteur4',
90
            'prénom4',
91
            'nom4',
92
            'lire en ligne',
93
            'présentation en ligne',
94
            'date',
95
            'sous-titre',
96
            'lien auteur1',
97
            'lien auteur2',
98
        ];
99
100
        // completion automatique
101
        foreach ($this->book->toArray() as $param => $value) {
102
            if (empty($this->origin->getParam($param))) {
103
                if (in_array($param, $skipParam)) {
104
                    continue;
105
                }
106
                // skip 'année' if 'date' not empty
107
                if ('année' === $param && !empty($this->origin->getParam('date'))) {
108
                    continue;
109
                }
110
111
                $this->origin->setParam($param, $value);
112
113
                if ('langue' === $param && static::WIKI_LANGUAGE === $value) {
114
                    //$this->log('fr'.$param);
115
                    continue;
116
                }
117
118
                $this->log('++'.$param);
119
                $this->major = true;
120
                $this->notCosmetic = true;
121
            }
122
        }
123
124
        $this->processLienAuteur();
125
        $this->googleBookProcess();
126
        $this->processSousTitre();
127
128
        if ($this->notCosmetic && 'BnF' === $this->book->getSource()) {
129
            $this->log('(BnF)');
130
        }
131
132
        return true;
133
    }
134
135
    private function log(string $string): void
136
    {
137
        if (!empty($string)) {
138
            $this->log[] = trim($string);
139
        }
140
    }
141
142
    /**
143
     * Complétion 'lien auteur1' d'après Wikidata et BnF.
144
     * Logique : faut pas confondre auteur1/auteur2 pour le lien auteur1.
145
     *
146
     * @throws Exception
147
     */
148
    private function processLienAuteur()
149
    {
150
        $lienAuteur1 = $this->book->getParam('lien auteur1');
151
        if (empty($lienAuteur1)) {
152
            return;
153
        }
154
        if(!empty($this->origin->getParam('lien auteur1'))){
155
            echo "lien auteur1 existe déjà\n";
156
            return;
157
        }
158
159
        $originAuteur1 = $this->concatParamsAuteur1($this->origin);
160
        $bookAuteur1 = $this->concatParamsAuteur1($this->book);
161
162
        // WP:"Paul Durand" — Bnf "Paul Durand,..."
163
        if (!empty($bookAuteur1) && !empty($originAuteur1)
164
            && mb_strpos($bookAuteur1, $originAuteur1) !== false
165
        ) {
166
            $this->origin->setParam('lien auteur1', $lienAuteur1);
167
            $this->log('+lien auteur1');
168
            $this->notCosmetic = true;
169
            $this->major = true;
170
        } else {
171
            echo 'DEBUG : auteur1 pas identifié\n';
172
        }
173
        // todo: gérer "not same book" avec inversion auteur1/2 avant d'implémenter +lien auteur2
174
    }
175
176
    /**
177
     * Concaténation auteur/prénom/nom pour comparaison de wiki-modèles.
178
     *
179
     * @param OuvrageTemplate $ouvrage
180
     * @param int|null        $num
181
     *
182
     * @return string|null
183
     * @throws Exception
184
     */
185
    private function concatParamsAuteur1(OuvrageTemplate $ouvrage, ?int $num = 1): ?string
186
    {
187
        $auteur = $ouvrage->getParam('auteur'.$num) ?? '';
188
        $prenom = $ouvrage->getParam('prénom'.$num) ?? '';
189
        $nom = $ouvrage->getParam('nom'.$num) ?? '';
190
191
        return trim($auteur.' '.$prenom.' '.$nom);
192
    }
193
194
    /**
195
     * Complétion lire/présentation en ligne, si vide.
196
     * Passe Google Book en accès partiel en 'lire en ligne' (sondage)
197
     *
198
     * @throws Exception
199
     */
200
    private function googleBookProcess()
201
    {
202
        // si déjà lire/présentation en ligne => on touche à rien
203
        if (!empty($this->origin->getParam('lire en ligne'))
204
            || !empty($this->origin->getParam('présentation en ligne'))
205
        ) {
206
            return;
207
        }
208
209
        // completion basique
210
        $booklire = $this->book->getParam('lire en ligne');
211
        if ($booklire) {
212
            $this->origin->setParam('lire en ligne', $booklire);
213
            $this->log('+lire en ligne');
214
            $this->notCosmetic = true;
215
            $this->major = true;
216
217
            return;
218
        }
219
220
        $presentation = $this->book->getParam('présentation en ligne') ?? false;
221
        // Ajout du partial Google => mis en lire en ligne
222
        // plutôt que 'présentation en ligne' selon sondage
223
        if (!empty($presentation) && GoogleLivresTemplate::isGoogleBookValue($presentation)) {
224
            $this->origin->setParam('lire en ligne', $presentation);
225
            $this->log('+lire en ligne');
226
            $this->notCosmetic = true;
227
            $this->major = true;
228
        }
229
    }
230
231
    /**
232
     * @return bool
233
     * @throws Exception
234
     */
235
    private function predictSameBook()
236
    {
237
        if ($this->hasSameISBN() && ($this->hasSameBookTitles() || $this->hasSameAuthors())) {
238
            return true;
239
        }
240
        if ($this->hasSameBookTitles() && $this->hasSameAuthors()) {
241
            return true;
242
        }
243
244
        return false;
245
    }
246
247
    /**
248
     * @return bool
249
     * @throws Exception
250
     */
251
    private function hasSameAuthors(): bool
252
    {
253
        if ($this->authorsFromBook($this->origin) === $this->authorsFromBook($this->book)) {
254
            return true;
255
        }
256
257
        // if there is only 2 char of difference (i.e. typo error)
258
        if (levenshtein($this->authorsFromBook($this->origin), $this->authorsFromBook($this->book)) <= 2) {
259
            $this->log('typo auteurs?');
260
261
            return true;
262
        }
263
264
        // Si auteur manquant sur wikipedia
265
        if (empty($this->authorsFromBook($this->origin))) {
266
            return true;
267
        }
268
269
        return false;
270
    }
271
272
    /**
273
     * @param OuvrageTemplate $ouv
274
     *
275
     * @return string
276
     * @throws Exception
277
     */
278
    private function authorsFromBook(OuvrageTemplate $ouv)
279
    {
280
        $text = '';
281
        $paramAuteurs = [
282
            'auteurs',
283
            'auteur1',
284
            'prénom1',
285
            'nom1',
286
            'auteur2',
287
            'prénom2',
288
            'nom2',
289
            'auteur3',
290
            'prénom3',
291
            'nom3',
292
            'auteur4',
293
            'prénom4',
294
            'nom4',
295
        ];
296
        foreach ($paramAuteurs as $param) {
297
            $value = str_replace(['.', ','], '', $ouv->getParam($param));
298
            // retire wikilien sur auteur
299
            if (!empty($value)) {
300
                $text .= WikiTextUtil::unWikify($value);
301
            }
302
        }
303
304
        return $this->stripAll($text);
305
    }
306
307
    /**
308
     * @return bool
309
     * @throws Exception
310
     */
311
    private function hasSameISBN(): bool
312
    {
313
        if (empty($this->origin->getParam('isbn')) || empty($this->book->getParam('isbn'))) {
314
            return false;
315
        }
316
        // TODO replace with calcul isbn13
317
        $isbn1 = IsbnFacade::isbn2ean($this->origin->getParam('isbn'));
318
        $isbn2 = IsbnFacade::isbn2ean($this->book->getParam('isbn'));
319
        if ($isbn1 === $isbn2) {
320
            return true;
321
        }
322
323
        return false;
324
    }
325
326
    /**
327
     * Add or extract subtitle like in second book.
328
     *
329
     * @throws Exception
330
     */
331
    private function processSousTitre()
332
    {
333
        if (empty($this->book->getParam('sous-titre'))) {
334
            return;
335
        }
336
337
        // Skip pour éviter conflit entre 'sous-titre' et 'collection' ou 'titre volume'
338
        if (!empty($this->origin->getParam('titre volume'))
339
            || !empty($this->origin->getParam('titre chapitre'))
340
            || !empty($this->origin->getParam('titre tome'))
341
            || !empty($this->origin->getParam('collection'))
342
            || !empty($this->origin->getParam('nature ouvrage'))
343
        ) {
344
            return;
345
        }
346
347
        // simple : titres identiques mais sous-titre manquant
348
        if ($this->stripAll($this->origin->getParam('titre')) === $this->stripAll($this->book->getParam('titre'))) {
0 ignored issues
show
Bug introduced by
It seems like $this->origin->getParam('titre') can also be of type null; however, parameter $text of App\Domain\OuvrageComplete::stripAll() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

348
        if ($this->stripAll(/** @scrutinizer ignore-type */ $this->origin->getParam('titre')) === $this->stripAll($this->book->getParam('titre'))) {
Loading history...
349
            // même titre mais sous-titre manquant
350
            if (empty($this->origin->getParam('sous-titre'))) {
351
                $this->origin->setParam('sous-titre', $this->book->getParam('sous-titre'));
0 ignored issues
show
Bug introduced by
It seems like $this->book->getParam('sous-titre') can also be of type null; however, parameter $value of App\Domain\Models\Wiki\A...ikiTemplate::setParam() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

351
                $this->origin->setParam('sous-titre', /** @scrutinizer ignore-type */ $this->book->getParam('sous-titre'));
Loading history...
352
                $this->log('++sous-titre');
353
                $this->major = true;
354
                $this->notCosmetic = true;
355
356
                return;
357
            }
358
        }
359
360
        // compliqué : sous-titre inclus dans titre original => on copie titre/sous-titre de book
361
        if ($this->charsFromBigTitle($this->origin) === $this->charsFromBigTitle($this->book)) {
362
            if (empty($this->origin->getParam('sous-titre'))) {
363
                $this->origin->setParam('titre', $this->book->getParam('titre'));
364
                $this->origin->setParam('sous-titre', $this->book->getParam('sous-titre'));
365
                $this->log('>sous-titre');
366
            }
367
        }
368
    }
369
370
    /**
371
     * @return bool
372
     * @throws Exception
373
     */
374
    private function hasSameBookTitles(): bool
375
    {
376
        $originBigTitle = $this->charsFromBigTitle($this->origin);
377
        $bookBigTitle = $this->charsFromBigTitle($this->book);
378
379
        if ($originBigTitle === $bookBigTitle) {
380
            return true;
381
        }
382
383
        // if there is only 2 chars of difference (i.e. typo error)
384
        // strlen for resource management
385
        if (strlen($originBigTitle) < 40 && strlen($bookBigTitle) < 40
386
            && levenshtein($originBigTitle, $bookBigTitle) <= 2
387
        ) {
388
            //            $this->log('typo titre?'); // TODO Normalize:: text from external API
389
390
            return true;
391
        }
392
393
        // si l'un des ouvrages ne comporte pas le sous-titre
394
        if ($this->stripAll($this->origin->getParam('titre')) === $this->stripAll($this->book->getParam('titre'))) {
0 ignored issues
show
Bug introduced by
It seems like $this->origin->getParam('titre') can also be of type null; however, parameter $text of App\Domain\OuvrageComplete::stripAll() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

394
        if ($this->stripAll(/** @scrutinizer ignore-type */ $this->origin->getParam('titre')) === $this->stripAll($this->book->getParam('titre'))) {
Loading history...
395
            return true;
396
        }
397
398
        // sous-titre inclus dans le titre
399
        // "Loiret : un département à l'élégance naturelle" <=> "Loiret"
400
        if ($this->stripAll($this->mainBookTitle($this->origin->getParam('titre'))) === $this->stripAll(
0 ignored issues
show
Bug introduced by
It seems like $this->origin->getParam('titre') can also be of type null; however, parameter $str of App\Domain\OuvrageComplete::mainBookTitle() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

400
        if ($this->stripAll($this->mainBookTitle(/** @scrutinizer ignore-type */ $this->origin->getParam('titre'))) === $this->stripAll(
Loading history...
401
                $this->mainBookTitle($this->origin->getParam('titre'))
402
            )
403
        ) {
404
            return true;
405
        }
406
        // titre manquant sur wiki
407
        if (empty($originBigTitle)) {
408
            return true;
409
        }
410
411
        return false;
412
    }
413
414
    /**
415
     * Give string before ":" (or same string if no ":").
416
     *
417
     * @param string $str
418
     *
419
     * @return string
420
     */
421
    private function mainBookTitle(string $str)
422
    {
423
        if (($pos = mb_strpos($str, ':'))) {
424
            $str = trim(mb_substr($str, 0, $pos));
425
        }
426
427
        return $str;
428
    }
429
430
    /**
431
     * @param OuvrageTemplate $ouvrage
432
     *
433
     * @return string
434
     * @throws Exception
435
     */
436
    private function charsFromBigTitle(OuvrageTemplate $ouvrage): string
437
    {
438
        $text = $ouvrage->getParam('titre').$ouvrage->getParam('sous-titre');
439
440
        return $this->stripAll(Normalizer::normalize($text));
441
    }
442
443
    /**
444
     * @param string $text
445
     *
446
     * @return string
447
     */
448
    private function stripAll(string $text): string
449
    {
450
        $text = str_replace([' and ', ' et ', '&'], '', $text);
451
        $text = str_replace(' ', '', $text);
452
        $text = mb_strtolower(TextUtil::stripPunctuation(TextUtil::stripAccents($text)));
453
454
        return $text;
455
    }
456
}
457