Passed
Branch develop (29ed49)
by Alexey
01:58
created

AppDetailScraper   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 96.63%

Importance

Changes 0
Metric Value
eloc 196
dl 0
loc 406
ccs 201
cts 208
cp 0.9663
rs 5.5199
c 0
b 0
f 0
wmc 56

21 Methods

Rating   Name   Duplication   Size   Complexity  
A extractTranslatedFromLocale() 0 8 2
A extractRecentChanges() 0 5 2
A extractDeveloper() 0 18 1
C getScriptData() 0 38 15
A extractPrice() 0 5 2
A extractDescription() 0 7 2
A extractSummary() 0 5 2
A extractHistogramRating() 0 8 1
A extractIcon() 0 5 2
A __invoke() 0 28 1
A extractReleaseDate() 0 6 2
A extractVideo() 0 13 4
A extractScreenshots() 0 5 2
A handlerDataPrice() 0 10 2
A handlerReviews() 0 9 2
A extractUpdatedDate() 0 6 2
A extractCover() 0 5 2
A handlerDataInfo() 0 48 1
A extractCategory() 0 8 4
A handlerDataVersion() 0 21 4
A handlerDataRating() 0 13 1

How to fix   Complexity   

Complex Class

Complex classes like AppDetailScraper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AppDetailScraper, and based on these observations, apply Extract Interface, too.

1
<?php
2
/** @noinspection MultiAssignmentUsageInspection */
3
declare(strict_types=1);
4
5
/**
6
 * @author   Ne-Lexa
7
 * @license  MIT
8
 * @link     https://github.com/Ne-Lexa/google-play-scraper
9
 */
10
11
namespace Nelexa\GPlay\Scraper;
12
13
use Nelexa\GPlay\Exception\GooglePlayException;
14
use Nelexa\GPlay\GPlayApps;
15
use Nelexa\GPlay\Http\ResponseHandlerInterface;
16
use Nelexa\GPlay\Model\AppDetail;
17
use Nelexa\GPlay\Model\Builder\AppBuilder;
18
use Nelexa\GPlay\Model\Category;
19
use Nelexa\GPlay\Model\Developer;
20
use Nelexa\GPlay\Model\GoogleImage;
21
use Nelexa\GPlay\Model\HistogramRating;
22
use Nelexa\GPlay\Model\Video;
23
use Nelexa\GPlay\Scraper\Extractor\ReviewsExtractor;
24
use Nelexa\GPlay\Util\DateStringFormatter;
25
use Nelexa\GPlay\Util\LocaleHelper;
26
use Nelexa\GPlay\Util\ScraperUtil;
27
use Psr\Http\Message\RequestInterface;
28
use Psr\Http\Message\ResponseInterface;
29
use function GuzzleHttp\Psr7\parse_query;
30
31
/**
32
 * @internal
33
 */
34
class AppDetailScraper implements ResponseHandlerInterface
35
{
36
    /**
37
     * @param RequestInterface $request
38
     * @param ResponseInterface $response
39
     * @return AppDetail
40
     * @throws GooglePlayException
41
     */
42 7
    public function __invoke(RequestInterface $request, ResponseInterface $response): AppDetail
43
    {
44
        [
45
            $scriptDataInfo,
46
            $scriptDataRating,
47
            $scriptDataPrice,
48
            $scriptDataVersion,
49
            $scriptDataReviews,
50 7
        ] = $this->getScriptData($request, $response);
51
52 7
        $query = parse_query($request->getUri()->getQuery());
53
54 7
        $id = $query[GPlayApps::REQ_PARAM_ID];
55 7
        $locale = $query[GPlayApps::REQ_PARAM_LOCALE] ?? GPlayApps::DEFAULT_LOCALE;
56 7
        $country = $query[GPlayApps::REQ_PARAM_COUNTRY] ?? GPlayApps::DEFAULT_COUNTRY;
57
58 7
        $appBuilder = AppDetail::newBuilder()
59 7
            ->setId($id)
60 7
            ->setLocale($locale)
61 7
            ->setCountry($country);
62
63 7
        $this->handlerDataInfo($appBuilder, $scriptDataInfo);
64 7
        $this->handlerDataRating($appBuilder, $scriptDataRating);
65 7
        $this->handlerDataPrice($appBuilder, $scriptDataPrice);
66 7
        $this->handlerDataVersion($appBuilder, $scriptDataVersion);
67 7
        $this->handlerReviews($appBuilder, $scriptDataReviews);
68
69 7
        return new AppDetail($appBuilder);
70
    }
71
72
    /**
73
     * @param RequestInterface $request
74
     * @param ResponseInterface $response
75
     * @return array
76
     * @throws GooglePlayException
77
     */
78 7
    private function getScriptData(RequestInterface $request, ResponseInterface $response): array
79
    {
80 7
        $scriptData = ScraperUtil::extractScriptData($response->getBody()->getContents());
81
82 7
        $scriptDataInfo = null;
83 7
        $scriptDataRating = null;
84 7
        $scriptDataPrice = null;
85 7
        $scriptDataVersion = null;
86 7
        $scriptDataReviews = [];
87
88 7
        foreach ($scriptData as $key => $scriptValue) {
89 7
            if (isset($scriptValue[0][12][5][5][4][2])) { // ds:5
90 7
                $scriptDataInfo = $scriptValue;
91 7
            } elseif (isset($scriptValue[0][2][0][0][0][1][0][0])) { // ds:3
92 7
                $scriptDataPrice = $scriptValue;
93 7
            } elseif (isset($scriptValue[0][0][0])
94 7
                && is_string($scriptValue[0][0][0])
95 7
                && strpos($scriptValue[0][0][0], 'gp:') === 0) { // ds:15
96 7
                $scriptDataReviews = $scriptValue;
97 7
            } elseif (isset($scriptValue[0][6][3][1])) { // ds:7
98 7
                $scriptDataRating = $scriptValue;
99 7
            } elseif (isset($scriptValue[0])
100 7
                && is_string($scriptValue[0])
101 7
                && count($scriptValue) === 3) { // ds:8
102 7
                $scriptDataVersion = $scriptValue;
103
            }
104
        }
105
106
        if (
107 7
            $scriptDataInfo === null ||
108 7
            $scriptDataRating === null ||
109 7
            $scriptDataPrice === null ||
110 7
            $scriptDataVersion === null
0 ignored issues
show
introduced by
The condition $scriptDataVersion === null is always true.
Loading history...
111
        ) {
112
            throw (new GooglePlayException('Unable to get data for this application.'))
113
                ->setUrl($request->getUri()->__toString());
114
        }
115 7
        return [$scriptDataInfo, $scriptDataRating, $scriptDataPrice, $scriptDataVersion, $scriptDataReviews];
116
    }
117
118
    /**
119
     * @param AppBuilder $appBuilder
120
     * @param array $scriptDataInfo
121
     */
122 7
    private function handlerDataInfo(AppBuilder $appBuilder, array $scriptDataInfo): void
123
    {
124 7
        $name = $scriptDataInfo[0][0][0];
125 7
        $description = $this->extractDescription($scriptDataInfo);
126 7
        $translatedFromLocale = $this->extractTranslatedFromLocale($scriptDataInfo, $appBuilder->getLocale());
0 ignored issues
show
Bug introduced by
It seems like $appBuilder->getLocale() can also be of type null; however, parameter $locale of Nelexa\GPlay\Scraper\App...tTranslatedFromLocale() 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

126
        $translatedFromLocale = $this->extractTranslatedFromLocale($scriptDataInfo, /** @scrutinizer ignore-type */ $appBuilder->getLocale());
Loading history...
127 7
        $developer = $this->extractDeveloper($scriptDataInfo);
128 7
        $category = $this->extractCategory($scriptDataInfo[0][12][13][0]);
129 7
        $summary = $this->extractSummary($scriptDataInfo);
130 7
        $installs = $scriptDataInfo[0][12][9][2] ?? 0;
131 7
        $offersIAPCost = $scriptDataInfo[0][12][12][0] ?? null;
132 7
        $containsAds = (bool)$scriptDataInfo[0][12][14][0];
133 7
        $editorsChoice = !empty($scriptDataInfo[0][12][15][1][1]);
134 7
        $privacyPoliceUrl = $scriptDataInfo[0][12][7][2] ?? '';
135 7
        $categoryFamily = $this->extractCategory($scriptDataInfo[0][12][13][1] ?? []);
136 7
        $icon = $this->extractIcon($scriptDataInfo);
137 7
        $cover = $this->extractCover($scriptDataInfo);
138 7
        $screenshots = $this->extractScreenshots($scriptDataInfo);
139 7
        $video = $this->extractVideo($scriptDataInfo);
140 7
        $contentRating = $scriptDataInfo[0][12][4][0] ?? '';
141 7
        $released = $this->extractReleaseDate($scriptDataInfo, $appBuilder->getLocale());
0 ignored issues
show
Bug introduced by
It seems like $appBuilder->getLocale() can also be of type null; however, parameter $locale of Nelexa\GPlay\Scraper\App...r::extractReleaseDate() 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

141
        $released = $this->extractReleaseDate($scriptDataInfo, /** @scrutinizer ignore-type */ $appBuilder->getLocale());
Loading history...
142 7
        $updated = $this->extractUpdatedDate($scriptDataInfo);
143 7
        $recentChanges = $this->extractRecentChanges($scriptDataInfo);
144
145
        $appBuilder
146 7
            ->setName($name)
147 7
            ->setDescription($description)
148 7
            ->setTranslatedFromLocale($translatedFromLocale)
149 7
            ->setSummary($summary)
150 7
            ->setIcon($icon)
151 7
            ->setCover($cover)
152 7
            ->setScreenshots($screenshots)
153 7
            ->setDeveloper($developer)
154 7
            ->setCategory($category)
155 7
            ->setCategoryFamily($categoryFamily)
156 7
            ->setVideo($video)
157 7
            ->setRecentChanges($recentChanges)
158 7
            ->setEditorsChoice($editorsChoice)
159 7
            ->setPrivacyPoliceUrl($privacyPoliceUrl)
160 7
            ->setInstalls($installs)
161 7
            ->setRecentChanges($recentChanges)
162 7
            ->setEditorsChoice($editorsChoice)
163 7
            ->setPrivacyPoliceUrl($privacyPoliceUrl)
164 7
            ->setInstalls($installs)
165 7
            ->setOffersIAPCost($offersIAPCost)
166 7
            ->setContainsAds($containsAds)
167 7
            ->setContentRating($contentRating)
168 7
            ->setReleased($released)
169 7
            ->setUpdated($updated);
170 7
    }
171
172
    /**
173
     * @param array $scriptDataInfo
174
     * @return string
175
     */
176 7
    private function extractDescription(array $scriptDataInfo): string
177
    {
178 7
        if (isset($scriptDataInfo[0][19][0][0][1])) {
179 2
            return ScraperUtil::html2text($scriptDataInfo[0][19][0][0][1]);
180
        }
181
182 7
        return ScraperUtil::html2text($scriptDataInfo[0][10][0][1]);
183
    }
184
185
    /**
186
     * @param $scriptDataInfo
187
     * @param string $locale
188
     * @return string|null
189
     */
190 7
    private function extractTranslatedFromLocale(array $scriptDataInfo, string $locale): ?string
191
    {
192 7
        return isset($scriptDataInfo[0][19][1]) ?
193 2
            LocaleHelper::findPreferredLanguage(
194 2
                $locale,
195 2
                $scriptDataInfo[0][19][1]
196
            ) :
197 7
            null;
198
    }
199
200
    /**
201
     * @param array $scriptDataInfo
202
     * @return Developer
203
     */
204 7
    private function extractDeveloper(array $scriptDataInfo): Developer
205
    {
206 7
        $developerPage = GPlayApps::GOOGLE_PLAY_URL . $scriptDataInfo[0][12][5][5][4][2];
207 7
        $developerId = parse_query(parse_url($developerPage, PHP_URL_QUERY))[GPlayApps::REQ_PARAM_ID];
208 7
        $developerName = $scriptDataInfo[0][12][5][1];
209 7
        $developerEmail = $scriptDataInfo[0][12][5][2][0];
210 7
        $developerWebsite = $scriptDataInfo[0][12][5][3][5][2];
211 7
        $developerAddress = $scriptDataInfo[0][12][5][4][0];
212
//        $developerInternalID = (int)$scriptDataInfo[0][12][5][0][0];
213
214 7
        return new Developer(
215 7
            Developer::newBuilder()
216 7
                ->setId($developerId)
217 7
                ->setUrl($developerPage)
218 7
                ->setName($developerName)
219 7
                ->setEmail($developerEmail)
220 7
                ->setAddress($developerAddress)
221 7
                ->setWebsite($developerWebsite)
222
        );
223
    }
224
225
    /**
226
     * @param array $data
227
     * @return Category|null
228
     */
229 7
    private function extractCategory(array $data): ?Category
230
    {
231 7
        if (isset($data[0]) && $data[0] !== null && $data[2] !== null) {
232 7
            $genreId = (string)$data[2];
233 7
            $genreName = (string)$data[0];
234 7
            return new Category($genreId, $genreName);
235
        }
236 7
        return null;
237
    }
238
239
    /**
240
     * @param $scriptDataInfo
241
     * @return string|null
242
     */
243 7
    private function extractSummary(array $scriptDataInfo): ?string
244
    {
245 7
        return empty($scriptDataInfo[0][10][1][1]) ?
246
            null :
247 7
            ScraperUtil::html2text($scriptDataInfo[0][10][1][1]);
248
    }
249
250
    /**
251
     * @param array $scriptDataInfo
252
     * @return GoogleImage|null
253
     */
254 7
    private function extractIcon(array $scriptDataInfo): ?GoogleImage
255
    {
256 7
        return empty($scriptDataInfo[0][12][1][3][2]) ?
257
            null :
258 7
            new GoogleImage($scriptDataInfo[0][12][1][3][2]);
259
    }
260
261
    /**
262
     * @param array $scriptDataInfo
263
     * @return GoogleImage|null
264
     */
265 7
    private function extractCover(array $scriptDataInfo): ?GoogleImage
266
    {
267 7
        return empty($scriptDataInfo[0][12][2][3][2]) ?
268
            null :
269 7
            new GoogleImage($scriptDataInfo[0][12][2][3][2]);
270
    }
271
272
    /**
273
     * @param array $scriptDataInfo
274
     * @return GoogleImage[]
275
     */
276 7
    private function extractScreenshots(array $scriptDataInfo): array
277
    {
278
        return !empty($scriptDataInfo[0][12][0]) ? array_map(static function (array $v) {
279 7
            return new GoogleImage($v[3][2]);
280 7
        }, $scriptDataInfo[0][12][0]) : [];
281
    }
282
283
    /**
284
     * @param array $scriptDataInfo
285
     * @return Video|null
286
     */
287 7
    private function extractVideo(array $scriptDataInfo): ?Video
288
    {
289
        if (
290 7
            isset($scriptDataInfo[0][12][3][0][3][2]) &&
291 7
            $scriptDataInfo[0][12][3][0][3][2] !== null &&
292 7
            $scriptDataInfo[0][12][3][1][3][2] !== null
293
        ) {
294 2
            $videoThumb = (string)$scriptDataInfo[0][12][3][1][3][2];
295 2
            $videoUrl = (string)$scriptDataInfo[0][12][3][0][3][2];
296
297 2
            return new Video($videoThumb, $videoUrl);
298
        }
299 6
        return null;
300
    }
301
302
    /**
303
     * @param array $scriptDataInfo
304
     * @param string $locale
305
     * @return \DateTimeInterface|null
306
     */
307 7
    private function extractReleaseDate(array $scriptDataInfo, string $locale): ?\DateTimeInterface
308
    {
309 7
        if (isset($scriptDataInfo[0][12][36])) {
310 6
            return DateStringFormatter::formatted($locale, $scriptDataInfo[0][12][36]);
311
        }
312 1
        return null;
313
    }
314
315
    /**
316
     * @param array $scriptDataInfo
317
     * @return \DateTimeInterface|null
318
     */
319 7
    private function extractUpdatedDate(array $scriptDataInfo): ?\DateTimeInterface
320
    {
321 7
        if (isset($scriptDataInfo[0][12][8][0])) {
322 7
            return DateStringFormatter::unixTimeToDateTime($scriptDataInfo[0][12][8][0]);
323
        }
324
        return null;
325
    }
326
327
    /**
328
     * @param $scriptDataInfo
329
     * @return string|null
330
     */
331 7
    private function extractRecentChanges($scriptDataInfo): ?string
332
    {
333 7
        return empty($scriptDataInfo[0][12][6][1]) ?
334
            null :
335 7
            ScraperUtil::html2text($scriptDataInfo[0][12][6][1]);
336
    }
337
338
    /**
339
     * @param AppBuilder $appBuilder
340
     * @param array $scriptDataRating
341
     */
342 7
    private function handlerDataRating(AppBuilder $appBuilder, array $scriptDataRating): void
343
    {
344 7
        $score = (float)($scriptDataRating[0][6][0][1] ?? 0);
345 7
        $numberVoters = (int)($scriptDataRating[0][6][2][1] ?? 0);
346 7
        $numberReviews = (int)($scriptDataRating[0][6][3][1] ?? 0);
347 7
        $histogramRating = $this->extractHistogramRating($scriptDataRating);
348
349
        $appBuilder
350 7
            ->setScore($score)
351 7
            ->setScore($score)
352 7
            ->setNumberVoters($numberVoters)
353 7
            ->setHistogramRating($histogramRating)
354 7
            ->setNumberReviews($numberReviews);
355 7
    }
356
357
    /**
358
     * @param array $scriptDataRating
359
     * @return HistogramRating
360
     */
361 7
    private function extractHistogramRating(array $scriptDataRating): HistogramRating
362
    {
363 7
        return new HistogramRating(
364 7
            $scriptDataRating[0][6][1][5][1] ?? 0,
365 7
            $scriptDataRating[0][6][1][4][1] ?? 0,
366 7
            $scriptDataRating[0][6][1][3][1] ?? 0,
367 7
            $scriptDataRating[0][6][1][2][1] ?? 0,
368 7
            $scriptDataRating[0][6][1][1][1] ?? 0
369
        );
370
    }
371
372
    /**
373
     * @param AppBuilder $appBuilder
374
     * @param array $scriptDataPrice
375
     */
376 7
    private function handlerDataPrice(AppBuilder $appBuilder, array $scriptDataPrice): void
377
    {
378 7
        $price = $this->extractPrice($scriptDataPrice);
379 7
        $currency = $scriptDataPrice[0][2][0][0][0][1][0][1];
380 7
        $priceText = $scriptDataPrice[0][2][0][0][0][1][0][2] ?: null;
381
382
        $appBuilder
383 7
            ->setPrice($price)
384 7
            ->setCurrency($currency)
385 7
            ->setPriceText($priceText);
386 7
    }
387
388
    /**
389
     * @param array $scriptDataPrice
390
     * @return float
391
     */
392 7
    private function extractPrice(array $scriptDataPrice): ?float
393
    {
394 7
        return isset($scriptDataPrice[0][2][0][0][0][1][0][0]) ?
395 7
            (float)($scriptDataPrice[0][2][0][0][0][1][0][0] / 1000000) :
396 7
            0.0;
397
    }
398
399
    /**
400
     * @param AppBuilder $appBuilder
401
     * @param $scriptDataVersion
402
     */
403 7
    private function handlerDataVersion(AppBuilder $appBuilder, $scriptDataVersion): void
404
    {
405 7
        [$size, $appVersion, $androidVersion] = $scriptDataVersion;
406 7
        if (LocaleHelper::isDependOnDevice($appBuilder->getLocale(), $size)) {
0 ignored issues
show
Bug introduced by
It seems like $appBuilder->getLocale() can also be of type null; however, parameter $locale of Nelexa\GPlay\Util\LocaleHelper::isDependOnDevice() 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

406
        if (LocaleHelper::isDependOnDevice(/** @scrutinizer ignore-type */ $appBuilder->getLocale(), $size)) {
Loading history...
407 5
            $size = null;
408
        }
409 7
        if (LocaleHelper::isDependOnDevice($appBuilder->getLocale(), $appVersion)) {
410 3
            $appVersion = null;
411
        }
412 7
        if (LocaleHelper::isDependOnDevice($appBuilder->getLocale(), $androidVersion)) {
413 3
            $androidVersion = null;
414 3
            $minAndroidVersion = null;
415
        } else {
416 4
            $minAndroidVersion = preg_replace('~.*?(\d+(\.\d+)*).*~', '$1', $androidVersion);
417
        }
418
419
        $appBuilder
420 7
            ->setSize($size)
421 7
            ->setAppVersion($appVersion)
422 7
            ->setAndroidVersion($androidVersion)
423 7
            ->setMinAndroidVersion($minAndroidVersion);
424 7
    }
425
426
    /**
427
     * @param AppBuilder $appBuilder
428
     * @param array $scriptDataReviews
429
     * @param int $limit
430
     */
431 7
    private function handlerReviews(AppBuilder $appBuilder, array $scriptDataReviews, int $limit = 4): void
432
    {
433 7
        if (!empty($scriptDataReviews[0])) {
434 7
            $reviews = ReviewsExtractor::extractReviews(
435 7
                $appBuilder->buildAppId(),
436 7
                array_slice($scriptDataReviews[0], 0, $limit)
437
            );
438
439 7
            $appBuilder->setReviews($reviews);
440
        }
441 7
    }
442
}
443