Completed
Push — master ( 44374d...601753 )
by Dispositif
05:03
created

OuvrageComplete::authorsFromBook()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 20
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 27
rs 9.6
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
157
            return;
158
        }
159
160
        $originAuteur1 = $this->concatParamsAuteur1($this->origin);
161
        $bookAuteur1 = $this->concatParamsAuteur1($this->book);
162
        //        dump($originAuteur1,$bookAuteur1, strpos($originAuteur1, $this->book->getParam('nom1')));
163
        // WP:"Paul Durand" — Bnf "Paul Durand,..."
164
        if (!empty($bookAuteur1) && !empty($originAuteur1)
165
            && (mb_strtolower($bookAuteur1) === mb_strtolower($originAuteur1)
166
                || strpos($originAuteur1, $this->book->getParam('nom1')) !== false)
167
        ) {
168
            $this->origin->setParam('lien auteur1', $lienAuteur1);
169
            $this->log('+lien auteur1');
170
            $this->notCosmetic = true;
171
            $this->major = true;
172
        } else {
173
            echo 'DEBUG : auteur1 pas identifié\n';
174
        }
175
        // todo: gérer "not same book" avec inversion auteur1/2 avant d'implémenter +lien auteur2
176
    }
177
178
    /**
179
     * Concaténation auteur/prénom/nom pour comparaison de wiki-modèles.
180
     *
181
     * @param OuvrageTemplate $ouvrage
182
     * @param int|null        $num
183
     *
184
     * @return string|null
185
     * @throws Exception
186
     */
187
    private function concatParamsAuteur1(OuvrageTemplate $ouvrage, ?int $num = 1): ?string
188
    {
189
        $auteur = $ouvrage->getParam('auteur'.$num) ?? '';
190
        $prenom = $ouvrage->getParam('prénom'.$num) ?? '';
191
        $nom = $ouvrage->getParam('nom'.$num) ?? '';
192
193
        return trim($auteur.' '.$prenom.' '.$nom);
194
    }
195
196
    /**
197
     * Complétion lire/présentation en ligne, si vide.
198
     * Passe Google Book en accès partiel en 'lire en ligne' (sondage)
199
     *
200
     * @throws Exception
201
     */
202
    private function googleBookProcess()
203
    {
204
        // si déjà lire/présentation en ligne => on touche à rien
205
        if (!empty($this->origin->getParam('lire en ligne'))
206
            || !empty($this->origin->getParam('présentation en ligne'))
207
        ) {
208
            return;
209
        }
210
211
        // completion basique
212
        $booklire = $this->book->getParam('lire en ligne');
213
        if ($booklire) {
214
            $this->origin->setParam('lire en ligne', $booklire);
215
            $this->log('+lire en ligne');
216
            $this->notCosmetic = true;
217
            $this->major = true;
218
219
            return;
220
        }
221
222
        $presentation = $this->book->getParam('présentation en ligne') ?? false;
223
        // Ajout du partial Google => mis en lire en ligne
224
        // plutôt que 'présentation en ligne' selon sondage
225
        if (!empty($presentation) && GoogleLivresTemplate::isGoogleBookValue($presentation)) {
226
            $this->origin->setParam('lire en ligne', $presentation);
227
            $this->log('+lire en ligne');
228
            $this->notCosmetic = true;
229
            $this->major = true;
230
        }
231
    }
232
233
    /**
234
     * @return bool
235
     * @throws Exception
236
     */
237
    private function predictSameBook()
238
    {
239
        if ($this->hasSameISBN() && ($this->hasSameBookTitles() || $this->hasSameAuthors())) {
240
            return true;
241
        }
242
        if ($this->hasSameBookTitles() && $this->hasSameAuthors()) {
243
            return true;
244
        }
245
246
        return false;
247
    }
248
249
    /**
250
     * @return bool
251
     * @throws Exception
252
     */
253
    private function hasSameAuthors(): bool
254
    {
255
        if ($this->authorsFromBook($this->origin) === $this->authorsFromBook($this->book)) {
256
            return true;
257
        }
258
259
        // if there is only 2 char of difference (i.e. typo error)
260
        if (levenshtein($this->authorsFromBook($this->origin), $this->authorsFromBook($this->book)) <= 2) {
261
            $this->log('typo auteurs?');
262
263
            return true;
264
        }
265
266
        // Si auteur manquant sur wikipedia
267
        if (empty($this->authorsFromBook($this->origin))) {
268
            return true;
269
        }
270
271
        return false;
272
    }
273
274
    /**
275
     * @param OuvrageTemplate $ouv
276
     *
277
     * @return string
278
     * @throws Exception
279
     */
280
    private function authorsFromBook(OuvrageTemplate $ouv)
281
    {
282
        $text = '';
283
        $paramAuteurs = [
284
            'auteurs',
285
            'auteur1',
286
            'prénom1',
287
            'nom1',
288
            'auteur2',
289
            'prénom2',
290
            'nom2',
291
            'auteur3',
292
            'prénom3',
293
            'nom3',
294
            'auteur4',
295
            'prénom4',
296
            'nom4',
297
        ];
298
        foreach ($paramAuteurs as $param) {
299
            $value = str_replace(['.', ','], '', $ouv->getParam($param));
300
            // retire wikilien sur auteur
301
            if (!empty($value)) {
302
                $text .= WikiTextUtil::unWikify($value);
303
            }
304
        }
305
306
        return $this->stripAll($text);
307
    }
308
309
    /**
310
     * @return bool
311
     * @throws Exception
312
     */
313
    private function hasSameISBN(): bool
314
    {
315
        if (empty($this->origin->getParam('isbn')) || empty($this->book->getParam('isbn'))) {
316
            return false;
317
        }
318
        // TODO replace with calcul isbn13
319
        $isbn1 = IsbnFacade::isbn2ean($this->origin->getParam('isbn'));
320
        $isbn2 = IsbnFacade::isbn2ean($this->book->getParam('isbn'));
321
322
        if ($isbn1 === $isbn2) {
323
            return true;
324
        }
325
326
        return false;
327
    }
328
329
    /**
330
     * Add or extract subtitle like in second book.
331
     *
332
     * @throws Exception
333
     */
334
    private function processSousTitre()
335
    {
336
        if (empty($this->book->getParam('sous-titre'))) {
337
            return;
338
        }
339
340
        // Skip pour éviter conflit entre 'sous-titre' et 'collection' ou 'titre volume'
341
        if (!empty($this->origin->getParam('titre volume'))
342
            || !empty($this->origin->getParam('titre chapitre'))
343
            || !empty($this->origin->getParam('titre tome'))
344
            || !empty($this->origin->getParam('collection'))
345
            || !empty($this->origin->getParam('nature ouvrage'))
346
        ) {
347
            return;
348
        }
349
350
        // simple : titres identiques mais sous-titre manquant
351
        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

351
        if ($this->stripAll(/** @scrutinizer ignore-type */ $this->origin->getParam('titre')) === $this->stripAll($this->book->getParam('titre'))) {
Loading history...
352
            // même titre mais sous-titre manquant
353
            if (empty($this->origin->getParam('sous-titre'))) {
354
                $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

354
                $this->origin->setParam('sous-titre', /** @scrutinizer ignore-type */ $this->book->getParam('sous-titre'));
Loading history...
355
                $this->log('++sous-titre');
356
                $this->major = true;
357
                $this->notCosmetic = true;
358
359
                return;
360
            }
361
        }
362
363
        // compliqué : sous-titre inclus dans titre original => on copie titre/sous-titre de book
364
        if ($this->charsFromBigTitle($this->origin) === $this->charsFromBigTitle($this->book)) {
365
            if (empty($this->origin->getParam('sous-titre'))) {
366
                $this->origin->setParam('titre', $this->book->getParam('titre'));
367
                $this->origin->setParam('sous-titre', $this->book->getParam('sous-titre'));
368
                $this->log('>sous-titre');
369
            }
370
        }
371
    }
372
373
    /**
374
     * @return bool
375
     * @throws Exception
376
     */
377
    private function hasSameBookTitles(): bool
378
    {
379
        $originBigTitle = $this->charsFromBigTitle($this->origin);
380
        $bookBigTitle = $this->charsFromBigTitle($this->book);
381
382
        if ($originBigTitle === $bookBigTitle) {
383
            return true;
384
        }
385
386
        // if there is only 2 chars of difference (i.e. typo error)
387
        // strlen for resource management
388
        if (strlen($originBigTitle) < 40 && strlen($bookBigTitle) < 40
389
            && levenshtein($originBigTitle, $bookBigTitle) <= 2
390
        ) {
391
            //            $this->log('typo titre?'); // TODO Normalize:: text from external API
392
393
            return true;
394
        }
395
396
        // si l'un des ouvrages ne comporte pas le sous-titre
397
        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

397
        if ($this->stripAll(/** @scrutinizer ignore-type */ $this->origin->getParam('titre')) === $this->stripAll($this->book->getParam('titre'))) {
Loading history...
398
            return true;
399
        }
400
401
        // sous-titre inclus dans le titre
402
        // "Loiret : un département à l'élégance naturelle" <=> "Loiret"
403
        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

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