Filler::fill()   B
last analyzed

Complexity

Conditions 5
Paths 2

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 36
c 0
b 0
f 0
ccs 0
cts 27
cp 0
rs 8.439
cc 5
eloc 23
nc 2
nop 1
crap 30
1
<?php
2
/**
3
 * AnimeDb package.
4
 *
5
 * @author    Peter Gribanov <[email protected]>
6
 * @copyright Copyright (c) 2011, Peter Gribanov
7
 * @license   http://opensource.org/licenses/GPL-3.0 GPL v3
8
 */
9
namespace AnimeDb\Bundle\AniDbFillerBundle\Service;
10
11
use AnimeDb\Bundle\CatalogBundle\Plugin\Fill\Filler\Filler as FillerPlugin;
12
use AnimeDb\Bundle\AniDbBrowserBundle\Service\Browser;
13
use Doctrine\Bundle\DoctrineBundle\Registry;
14
use AnimeDb\Bundle\AppBundle\Service\Downloader;
15
use AnimeDb\Bundle\CatalogBundle\Entity\Item;
16
use AnimeDb\Bundle\CatalogBundle\Entity\Name;
17
use AnimeDb\Bundle\CatalogBundle\Entity\Source;
18
use AnimeDb\Bundle\CatalogBundle\Entity\Genre;
19
use AnimeDb\Bundle\AniDbFillerBundle\Form\Type\Filler as FillerForm;
20
use Symfony\Component\DomCrawler\Crawler;
21
use Knp\Menu\ItemInterface;
22
use AnimeDb\Bundle\AppBundle\Service\Downloader\Entity\EntityInterface;
23
24
class Filler extends FillerPlugin
25
{
26
    /**
27
     * @var string
28
     */
29
    const NAME = 'anidb';
30
31
    /**
32
     * @var string
33
     */
34
    const TITLE = 'AniDB.net';
35
36
    /**
37
     * RegExp for get item id.
38
     *
39
     * @var string
40
     */
41
    const REG_ITEM_ID = '#/perl\-bin/animedb\.pl\?show=anime&aid=(?<id>\d+)#';
42
43
    /**
44
     * @var Browser
45
     */
46
    protected $browser;
47
48
    /**
49
     * @var Registry
50
     */
51
    protected $doctrine;
52
53
    /**
54
     * @var Downloader
55
     */
56
    protected $downloader;
57
58
    /**
59
     * @var SummaryCleaner
60
     */
61
    protected $cleaner;
62
63
    /**
64
     * @var string
65
     */
66
    protected $locale;
67
68
    /**
69
     * AniDB category to genre.
70
     *
71
     * <code>
72
     *     { from: to, ... }
73
     * </code>
74
     *
75
     * @var array
76
     */
77
    protected $category_to_genre = [
78
        'Alternative History' => 'History',
79
        'Anti-War' => 'War',
80
        'Badminton' => 'Sport',
81
        'Bakumatsu - Meiji Period' => 'History',
82
        'Band' => 'Music',
83
        'Baseball' => 'Sport',
84
        'Basketball' => 'Sport',
85
        'Battle Royale' => 'War',
86
        'Board Games' => 'Game',
87
        'Boxing' => 'Sport',
88
        'Catholic School' => 'School',
89
        'Chess' => 'Sport',
90
        'Clubs' => 'School',
91
        'College' => 'School',
92
        'Combat' => 'Action',
93
        'Conspiracy' => 'Thriller',
94
        'Contemporary Fantasy' => 'Fantasy',
95
        'Cops' => 'Police',
96
        'Daily Life' => 'Slice of life',
97
        'Dark Elf' => 'Fantasy',
98
        'Dark Fantasy' => 'Fantasy',
99
        'Dodgeball' => 'Sport',
100
        'Dragon' => 'Fantasy',
101
        'Edo Period' => 'Fantasy',
102
        'Elementary School' => 'School',
103
        'Elf' => 'Fantasy',
104
        'Fairies' => 'Fantasy',
105
        'Fantasy World' => 'Fantasy',
106
        'Feudal Warfare' => 'War',
107
        'Football' => 'Sport',
108
        'Formula Racing' => 'Sport',
109
        'Ghost' => 'Supernatural',
110
        'Go' => 'Game',
111
        'Golf' => 'Sport',
112
        'Gunfights' => 'War',
113
        'Gymnastics' => 'Sport',
114
        'Heian Period' => 'History',
115
        'High Fantasy' => 'Fantasy',
116
        'High School' => 'School',
117
        'Historical' => 'History',
118
        'Ice Skating' => 'Sport',
119
        'Inline Skating' => 'Sport',
120
        'Jousting' => 'Sport',
121
        'Judo' => 'Sport',
122
        'Kendo' => 'Sport',
123
        'Law and Order' => 'Police',
124
        'Magic Circles' => 'Magic',
125
        'Mahjong' => 'Game',
126
        'Mahou Shoujo' => 'Mahoe shoujo',
127
        'Martial Arts' => 'Martial arts',
128
        'Military' => 'War',
129
        'Motorsport' => 'Sport',
130
        'Muay Thai' => 'Sport',
131
        'Ninja' => 'Samurai',
132
        'Pirate' => 'Adventure',
133
        'Post-apocalypse' => 'Apocalyptic fiction',
134
        'Post-War' => 'War',
135
        'Proxy Battles' => 'War',
136
        'Reverse Harem' => 'Harem',
137
        'Rugby' => 'Sport',
138
        'School Dormitory' => 'School',
139
        'School Excursion' => 'School',
140
        'School Festival' => 'School',
141
        'School Life' => 'School',
142
        'School Sports Festival' => 'School',
143
        'Sci-Fi' => 'Sci-fi',
144
        'Sengoku Period' => 'History',
145
        'Shougi' => 'Game',
146
        'Shoujo Ai' => 'Shoujo-ai',
147
        'Shounen Ai' => 'Shounen-ai',
148
        'Spellcasting' => 'Magic',
149
        'Sports' => 'Sport',
150
        'Street Racing' => 'Cars',
151
        'Swimming' => 'Sport',
152
        'Swordplay' => 'Sport',
153
        'Tennis' => 'Sport',
154
        'Victorian Period' => 'History',
155
        'Volleyball' => 'Sport',
156
        'Witch' => 'Magic',
157
        'World War I' => 'War',
158
        'World War II' => 'War',
159
        'Wrestling' => 'Action',
160
    ];
161
162
    /**
163
     * @param Browser $browser
164
     * @param Registry $doctrine
165
     * @param Downloader $downloader
166
     * @param SummaryCleaner $cleaner
167
     * @param $locale
168
     */
169
    public function __construct(
170
        Browser $browser,
171
        Registry $doctrine,
172
        Downloader $downloader,
173
        SummaryCleaner $cleaner,
174
        $locale
175
    ) {
176
        $this->browser = $browser;
177
        $this->doctrine = $doctrine;
178
        $this->downloader = $downloader;
179
        $this->cleaner = $cleaner;
180
        $this->locale = $locale;
181
    }
182
183
    /**
184
     * @return string
185
     */
186
    public function getName()
187
    {
188
        return self::NAME;
189
    }
190
191
    /**
192
     * @return string
193
     */
194
    public function getTitle()
195
    {
196
        return self::TITLE;
197
    }
198
199
    /**
200
     * @return FillerForm
201
     */
202
    public function getForm()
203
    {
204
        return new FillerForm($this->browser->getHost());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \AnimeDb\Bund...s->browser->getHost()); (AnimeDb\Bundle\AniDbFillerBundle\Form\Type\Filler) is incompatible with the return type declared by the interface AnimeDb\Bundle\CatalogBu...illerInterface::getForm of type AnimeDb\Bundle\CatalogBu...ugin\Fill\Filler\Filler.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
205
    }
206
207
    /**
208
     * Build menu for plugin.
209
     *
210
     * @param ItemInterface $item
211
     *
212
     * @return ItemInterface
213
     */
214
    public function buildMenu(ItemInterface $item)
215
    {
216
        return parent::buildMenu($item)
217
            ->setLinkAttribute('class', 'icon-label icon-label-plugin-anidb');
218
    }
219
220
    /**
221
     * Fill item from source.
222
     *
223
     * @param array $data
224
     *
225
     * @return Item|null
226
     */
227
    public function fill(array $data)
228
    {
229
        if (empty($data['url']) || !is_string($data['url']) ||
230
            strpos($data['url'], $this->browser->getHost()) !== 0 ||
231
            !preg_match(self::REG_ITEM_ID, $data['url'], $match)
232
        ) {
233
            return null;
234
        }
235
236
        $body = $this->browser->get('anime', ['aid' => $match['id']]);
237
238
        $item = new Item();
239
        $item->setEpisodesNumber($body->filter('episodecount')->text());
240
        $item->setDatePremiere(new \DateTime($body->filter('startdate')->text()));
241
        $item->setDateEnd(new \DateTime($body->filter('enddate')->text()));
242
        $item->setSummary($this->cleaner->clean($body->filter('description')->text()));
243
244
        // set main source
245
        $source = new Source();
246
        $source->setUrl($data['url']);
247
        $item->addSource($source);
248
249
        // add url to offsite
250
        $source = new Source();
251
        $source->setUrl($body->filter('url')->text());
252
        $item->addSource($source);
253
254
        // set complex data
255
        $this->setCover($item, $body, $match['id']);
256
        $this->setNames($item, $body);
257
        $this->setEpisodes($item, $body);
258
        $this->setType($item, $body);
259
        $this->setGenres($item, $body);
260
261
        return $item;
262
    }
263
264
    /**
265
     * @param Item $item
266
     * @param Crawler $body
267
     *
268
     * @return Item
269
     */
270
    public function setNames(Item $item, Crawler $body)
271
    {
272
        $titles = $body->filter('titles > title');
273
        $names = [];
274
        /* @var $title \DOMElement */
275
        foreach ($titles as $title) {
276
            $lang = substr($title->attributes->item(0)->nodeValue, 0, 2);
277
            if ($lang != 'x-') {
278
                $names[$lang][$title->getAttribute('type')] = $title->nodeValue;
279
            }
280
        }
281
282
        // set main name
283
        if (!empty($names[$this->locale])) {
284
            $item->setName($this->getNameForLocale($this->locale, $names));
285 View Code Duplication
        } elseif ($this->locale != 'en' && !empty($names['en'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
            $item->setName($this->getNameForLocale('en', $names));
287
        } else {
288
            $item->setName($this->getNameForLocale(array_keys($names)[0], $names));
289
        }
290
291
        // set other names
292
        $other = [];
293
        foreach ($names as $locales) {
294
            foreach ($locales as $name) {
295
                $other[] = $name;
296
            }
297
        }
298
        $other = array_unique($other);
299
        sort($other);
300
301
        foreach ($other as $name) {
302
            $item->addName((new Name())->setName($name));
303
        }
304
305
        return $item;
306
    }
307
308
    /**
309
     * @param Item $item
310
     * @param Crawler $body
311
     * @param string $id
312
     *
313
     * @return Item
314
     */
315
    public function setCover(Item $item, Crawler $body, $id)
316
    {
317
        if ($image = $body->filter('picture')->text()) {
318
            try {
319
                $image = $this->browser->getImageUrl($image);
320
                if ($path = parse_url($image, PHP_URL_PATH)) {
321
                    $ext = pathinfo($path, PATHINFO_EXTENSION);
322
                    $item->setCover(self::NAME.'/'.$id.'/cover.'.$ext);
323
                    $this->uploadImageFromUrl($image, $item);
324
                }
325
            } catch (\Exception $e) {
326
                // error while retrieving images is not critical
327
            }
328
        }
329
330
        return $item;
331
    }
332
333
    /**
334
     * @param Item $item
335
     * @param Crawler $body
336
     *
337
     * @return Item
338
     */
339
    public function setEpisodes(Item $item, Crawler $body)
340
    {
341
        $episodes = '';
342
        foreach ($body->filter('episodes > episode') as $episode) {
343
            $episode = new Crawler($episode);
344
            $episodes .= $episode->filter('epno')->text().'. '.$this->getEpisodeTitle($episode)."\n";
345
        }
346
        $item->setEpisodes(trim($episodes));
347
348
        return $item;
349
    }
350
351
    /**
352
     * @param Crawler $episode
353
     *
354
     * @return string
355
     */
356
    protected function getEpisodeTitle(Crawler $episode)
357
    {
358
        $titles = [];
359
        /* @var $title \DOMElement */
360
        foreach ($episode->filter('title') as $title) {
361
            $lang = substr($title->attributes->item(0)->nodeValue, 0, 2);
362
            if ($lang == $this->locale) {
363
                return $title->nodeValue;
364
            }
365
            if ($lang != 'x-') {
366
                $titles[$lang] = $title->nodeValue;
367
            }
368
        }
369
370
        // get EN lang or first
371
        if (!empty($titles['en'])) {
372
            return $titles['en'];
373
        } else {
374
            return array_shift($titles);
375
        }
376
    }
377
378
    /**
379
     * @param Item $item
380
     * @param Crawler $body
381
     *
382
     * @return Item
383
     */
384
    public function setType(Item $item, Crawler $body)
385
    {
386
        $rename = [
387
            'TV Series' => 'TV',
388
            'Movie' => 'Feature',
389
            'Web' => 'ONA',
390
        ];
391
        $type = $body->filter('anime > type')->text();
392
        $type = isset($rename[$type]) ? $rename[$type] : $type;
393
394
        return $item->setType(
395
            $this
396
                ->doctrine
397
                ->getRepository('AnimeDbCatalogBundle:Type')
398
                ->findOneBy(['name' => $type])
399
        );
400
    }
401
402
    /**
403
     * @param Item $item
404
     * @param Crawler $body
405
     *
406
     * @return Item
407
     */
408
    public function setGenres(Item $item, Crawler $body)
409
    {
410
        $repository = $this->doctrine->getRepository('AnimeDbCatalogBundle:Genre');
411
        $categories = $body->filter('categories > category > name');
412
        foreach ($categories as $category) {
413
            if (isset($this->category_to_genre[$category->nodeValue])) {
414
                $genre = $repository->findOneBy(['name' => $this->category_to_genre[$category->nodeValue]]);
415
            } else {
416
                $genre = $repository->findOneBy(['name' => $category->nodeValue]);
417
            }
418
            if ($genre instanceof Genre) {
419
                $item->addGenre($genre);
420
            }
421
        }
422
423
        return $item;
424
    }
425
426
    /**
427
     * @param string $url
428
     * @param EntityInterface $entity
429
     *
430
     * @return bool
431
     */
432
    protected function uploadImageFromUrl($url, EntityInterface $entity)
433
    {
434
        return $this->downloader->image($url, $this->downloader->getRoot().$entity->getWebPath());
435
    }
436
437
    /**
438
     * @param string $locale
439
     * @param array $names
440
     *
441
     * @return string
442
     */
443 View Code Duplication
    protected function getNameForLocale($locale, &$names)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
444
    {
445
        if (isset($names[$locale]['main'])) {
446
            $name = $names[$locale]['main'];
447
            unset($names[$locale]['main']);
448
        } elseif (isset($names[$locale]['official'])) {
449
            $name = $names[$locale]['official'];
450
            unset($names[$locale]['official']);
451
        } else {
452
            $name = array_shift($names[$locale]);
453
        }
454
455
        return $name;
456
    }
457
458
    /**
459
     * @param string $url
460
     *
461
     * @return bool
462
     */
463
    public function isSupportedUrl($url)
464
    {
465
        return strpos($url, $this->browser->getHost()) === 0;
466
    }
467
}
468