Passed
Branch develop (bc8038)
by Alexey
02:01
created

AppDetailScraper   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 47
eloc 208
dl 0
loc 313
rs 8.64
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A extractDeveloper() 0 18 1
B extractReviews() 0 50 7
F __invoke() 0 177 29
A extractVideo() 0 13 4
A extractScreenshots() 0 5 2
A extractCategory() 0 8 4

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
namespace Nelexa\GPlay\Scraper;
6
7
use Nelexa\GPlay\Exception\GooglePlayException;
8
use Nelexa\GPlay\GPlayApps;
9
use Nelexa\GPlay\Http\ResponseHandlerInterface;
10
use Nelexa\GPlay\Model\AppDetail;
11
use Nelexa\GPlay\Model\Category;
12
use Nelexa\GPlay\Model\Developer;
13
use Nelexa\GPlay\Model\GoogleImage;
14
use Nelexa\GPlay\Model\HistogramRating;
15
use Nelexa\GPlay\Model\ReplyReview;
16
use Nelexa\GPlay\Model\Review;
17
use Nelexa\GPlay\Model\Video;
18
use Nelexa\GPlay\Util\DateStringFormatter;
19
use Nelexa\GPlay\Util\LocaleHelper;
20
use Nelexa\GPlay\Util\ScraperUtil;
21
use Psr\Http\Message\RequestInterface;
22
use Psr\Http\Message\ResponseInterface;
23
use function GuzzleHttp\Psr7\parse_query;
24
25
class AppDetailScraper implements ResponseHandlerInterface
26
{
27
    /**
28
     * @param RequestInterface $request
29
     * @param ResponseInterface $response
30
     * @return AppDetail
31
     * @throws GooglePlayException
32
     */
33
    public function __invoke(RequestInterface $request, ResponseInterface $response): AppDetail
34
    {
35
        $query = parse_query($request->getUri()->getQuery());
36
        $appId = $query[GPlayApps::REQ_PARAM_APP_ID];
37
        $locale = $query[GPlayApps::REQ_PARAM_LOCALE] ?? GPlayApps::DEFAULT_LOCALE;
38
        $url = GPlayApps::GOOGLE_PLAY_APPS_URL . '/details?' . http_build_query([
39
                GPlayApps::REQ_PARAM_APP_ID => $appId,
40
            ]);
41
42
        $scriptData = ScraperUtil::extractScriptData($response->getBody()->getContents());
43
44
        $scriptDataInfo = null;
45
        $scriptDataRating = null;
46
        $scriptDataPrice = null;
47
        $scriptDataVersion = null;
48
        $scriptDataReviews = [];
49
50
        foreach ($scriptData as $key => $scriptValue) {
51
            if (isset($scriptValue[0][12][5][5][4][2])) { // ds:5
52
                $scriptDataInfo = $scriptValue;
53
            } elseif (isset($scriptValue[0][2][0][0][0][1][0][0])) { // ds:3
54
                $scriptDataPrice = $scriptValue;
55
            } elseif (isset($scriptValue[0][0][0])
56
                && is_string($scriptValue[0][0][0])
57
                && strpos($scriptValue[0][0][0], 'gp:') === 0) { // ds:15
58
                $scriptDataReviews = $scriptValue;
59
            } elseif (isset($scriptValue[0][6][3][1])) { // ds:7
60
                $scriptDataRating = $scriptValue;
61
            } elseif (isset($scriptValue[0])
62
                && is_string($scriptValue[0])
63
                && count($scriptValue) === 3) { // ds:8
64
                $scriptDataVersion = $scriptValue;
65
            }
66
        }
67
68
        if (
69
            $scriptDataInfo === null ||
70
            $scriptDataRating === null ||
71
            $scriptDataPrice === null ||
72
            $scriptDataVersion === null
0 ignored issues
show
introduced by
The condition $scriptDataVersion === null is always true.
Loading history...
73
        ) {
74
            throw (new GooglePlayException('Unable to get data for this application.'))->setUrl($url);
75
        }
76
77
        $name = $scriptDataInfo[0][0][0];
78
        $descriptionHTML = $scriptDataInfo[0][10][0][1];
79
        $description = ScraperUtil::html2text($descriptionHTML);
80
81
        $developer = $this->extractDeveloper($scriptDataInfo);
82
        $category = $this->extractCategory($scriptDataInfo[0][12][13][0]);
83
84
        $summary = empty($scriptDataInfo[0][10][1][1]) ?
85
            null :
86
            ScraperUtil::html2text($scriptDataInfo[0][10][1][1]);
87
88
        $installs = $scriptDataInfo[0][12][9][2] ?? 0;
89
        $score = (float)($scriptDataRating[0][6][0][1] ?? 0);
90
        $numberVoters = (int)($scriptDataRating[0][6][2][1] ?? 0);
91
        $reviewsCount = (int)($scriptDataRating[0][6][3][1] ?? 0);
92
        $histogram = $scriptDataRating[0][6][1] ?? null;
93
94
        $histogramRating = new HistogramRating(
95
            $histogram[5][1] ?? 0,
96
            $histogram[4][1] ?? 0,
97
            $histogram[3][1] ?? 0,
98
            $histogram[2][1] ?? 0,
99
            $histogram[1][1] ?? 0
100
        );
101
102
        $price = isset($scriptDataPrice[0][2][0][0][0][1][0][0]) ?
103
            (float)($scriptDataPrice[0][2][0][0][0][1][0][0] / 1000000) :
104
            0;
105
        $currency = $scriptDataPrice[0][2][0][0][0][1][0][1];
106
        $priceText = $scriptDataPrice[0][2][0][0][0][1][0][2] ?: 'Free';
107
        $offersIAPCost = $scriptDataInfo[0][12][12][0] ?? null;
108
        $adSupported = (bool)$scriptDataInfo[0][12][14][0];
109
110
        [$size, $appVersion, $androidVersion] = $scriptDataVersion;
111
        if (LocaleHelper::isDependOnDevice($locale, $size)) {
112
            $size = null;
113
        }
114
        if (LocaleHelper::isDependOnDevice($locale, $appVersion)) {
115
            $appVersion = null;
116
        }
117
        if (LocaleHelper::isDependOnDevice($locale, $androidVersion)) {
118
            $androidVersion = null;
119
            $minAndroidVersion = null;
120
        } else {
121
            $minAndroidVersion = preg_replace('~.*?(\d+(\.\d+)?).*~', '$1', $androidVersion);
122
        }
123
124
        $editorsChoice = !empty($scriptDataInfo[0][12][15][1][1]);
125
        $privacyPoliceUrl = $scriptDataInfo[0][12][7][2];
126
127
        $categoryFamily = $this->extractCategory($scriptDataInfo[0][12][13][1]??[]);
128
129
        $icon = empty($scriptDataInfo[0][12][1][3][2]) ?
130
            null :
131
            new GoogleImage($scriptDataInfo[0][12][1][3][2]);
132
133
        $headerImage = empty($scriptDataInfo[0][12][2][3][2]) ?
134
            null :
135
            new GoogleImage($scriptDataInfo[0][12][2][3][2]);
136
137
        $screenshots = $this->extractScreenshots($scriptDataInfo);
138
        $video = $this->extractVideo($scriptDataInfo);
139
140
        $contentRating = $scriptDataInfo[0][12][4][0];
141
        $released = null;
142
        if (isset($scriptDataInfo[0][12][36]) && $scriptDataInfo[0][12][36] !== null) {
143
            $released = DateStringFormatter::formatted($locale, $scriptDataInfo[0][12][36]);
144
        }
145
        try {
146
            $updated = !empty($scriptDataInfo[0][12][8][0]) ?
147
                new \DateTimeImmutable('@' . $scriptDataInfo[0][12][8][0])
148
                : null;
149
        } catch (\Exception $e) {
150
            $updated = null;
151
        }
152
153
        $recentChanges = empty($scriptDataInfo[0][12][6][1]) ?
154
            null :
155
            ScraperUtil::html2text($scriptDataInfo[0][12][6][1]);
156
157
        $translatedFromLanguage = null;
158
        $translatedDescription = null;
159
        if (isset($scriptDataInfo[0][19][1])) {
160
            $translatedFromLanguage = LocaleHelper::findPreferredLanguage(
161
                $locale,
162
                $scriptDataInfo[0][19][1]
163
            );
164
            $translatedDescription = ScraperUtil::html2text($scriptDataInfo[0][19][0][0][1]);
165
        }
166
167
        $reviews = $this->extractReviews($url, $scriptDataReviews);
168
169
        return new AppDetail(
170
            AppDetail::newBuilder()
171
                ->setId($appId)
172
                ->setLocale($locale)
173
                ->setName($name)
174
                ->setDescription($description)
175
                ->setTranslated($translatedDescription, $translatedFromLanguage)
176
                ->setSummary($summary)
177
                ->setIcon($icon)
178
                ->setHeaderImage($headerImage)
179
                ->setScreenshots($screenshots)
180
                ->setDeveloper($developer)
181
                ->setCategory($category)
182
                ->setCategoryFamily($categoryFamily)
183
                ->setVideo($video)
184
                ->setRecentChanges($recentChanges)
185
                ->setEditorsChoice($editorsChoice)
186
                ->setPrivacyPoliceUrl($privacyPoliceUrl)
187
                ->setInstalls($installs)
188
                ->setScore($score)
189
                ->setRecentChanges($recentChanges)
190
                ->setEditorsChoice($editorsChoice)
191
                ->setPrivacyPoliceUrl($privacyPoliceUrl)
192
                ->setInstalls($installs)
193
                ->setScore($score)
194
                ->setNumberVoters($numberVoters)
195
                ->setHistogramRating($histogramRating)
196
                ->setPrice($price)
197
                ->setCurrency($currency)
198
                ->setPriceText($priceText)
199
                ->setOffersIAPCost($offersIAPCost)
200
                ->setAdSupported($adSupported)
201
                ->setAppSize($size)
202
                ->setAppVersion($appVersion)
203
                ->setAndroidVersion($androidVersion)
204
                ->setMinAndroidVersion($minAndroidVersion)
205
                ->setContentRating($contentRating)
206
                ->setReleased($released)
207
                ->setUpdated($updated)
208
                ->setReviewsCount($reviewsCount)
209
                ->setReviews($reviews)
210
        );
211
    }
212
213
    /**
214
     * @param array $scriptDataInfo
215
     * @return Developer
216
     */
217
    private function extractDeveloper(array $scriptDataInfo): Developer
218
    {
219
        $developerPage = GPlayApps::GOOGLE_PLAY_URL . $scriptDataInfo[0][12][5][5][4][2];
220
        $developerId = parse_query(parse_url($developerPage, PHP_URL_QUERY))['id'];
221
        $developerName = $scriptDataInfo[0][12][5][1];
222
        $developerEmail = $scriptDataInfo[0][12][5][2][0];
223
        $developerWebsite = $scriptDataInfo[0][12][5][3][5][2];
224
        $developerAddress = $scriptDataInfo[0][12][5][4][0];
225
//        $developerInternalID = (int)$scriptDataInfo[0][12][5][0][0];
226
227
        return new Developer(
228
            Developer::newBuilder()
229
                ->setId($developerId)
230
                ->setUrl($developerPage)
231
                ->setName($developerName)
232
                ->setEmail($developerEmail)
233
                ->setAddress($developerAddress)
234
                ->setWebsite($developerWebsite)
235
        );
236
    }
237
238
    /**
239
     * @param array $data
240
     * @return Category|null
241
     */
242
    private function extractCategory(array $data): ?Category
243
    {
244
        if (isset($data[0]) && $data[0] !== null && $data[2] !== null) {
245
            $genreId = (string)$data[2];
246
            $genreName = (string)$data[0];
247
            return new Category($genreId, $genreName);
248
        }
249
        return null;
250
    }
251
252
    /**
253
     * @param array $scriptDataInfo
254
     * @return GoogleImage[]
255
     */
256
    private function extractScreenshots(array $scriptDataInfo): array
257
    {
258
        return !empty($scriptDataInfo[0][12][0]) ? array_map(static function (array $v) {
259
            return new GoogleImage($v[3][2]);
260
        }, $scriptDataInfo[0][12][0]) : [];
261
    }
262
263
    /**
264
     * @param array $scriptDataInfo
265
     * @return Video|null
266
     */
267
    private function extractVideo(array $scriptDataInfo): ?Video
268
    {
269
        if (
270
            isset($scriptDataInfo[0][12][3][0][3][2]) &&
271
            $scriptDataInfo[0][12][3][0][3][2] !== null &&
272
            $scriptDataInfo[0][12][3][1][3][2] !== null
273
        ) {
274
            $videoThumb = (string)$scriptDataInfo[0][12][3][1][3][2];
275
            $videoUrl = (string)$scriptDataInfo[0][12][3][0][3][2];
276
277
            return new Video($videoThumb, $videoUrl);
278
        }
279
        return null;
280
    }
281
282
    /**
283
     * @param string $appUrl
284
     * @param array $scriptDataReviews
285
     * @param int $limit
286
     * @return Review[]
287
     */
288
    private function extractReviews(string $appUrl, array $scriptDataReviews, int $limit = 4): array
289
    {
290
        if (empty($scriptDataReviews)) {
291
            return [];
292
        }
293
        $reviews = [];
294
        $count = min($limit, count($scriptDataReviews[0]));
295
        for ($i = 0; $i < $count; $i++) {
296
            $reviewData = $scriptDataReviews[0][$i];
297
            $reviewId = $reviewData[0];
298
            $reviewUrl = $appUrl . '&reviewId=' . urlencode($reviewId);
299
            $userName = $reviewData[1][0];
300
            $avatar = new GoogleImage($reviewData[1][1][3][2]);
301
            $date = null;
302
            if (isset($reviewData[5][0])) {
303
                try {
304
                    $date = new \DateTimeImmutable('@' . $reviewData[5][0]);
305
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
306
                }
307
            }
308
            $score = $reviewData[2] ?? 0;
309
            $text = (string)($reviewData[4] ?? '');
310
            $likeCount = $reviewData[6];
311
312
            $reply = null;
313
            if (isset($reviewData[7][1])) {
314
                $replyText = $reviewData[7][1];
315
                try {
316
                    $replyDate = new \DateTimeImmutable('@' . $reviewData[7][2][0]);
317
                    $reply = new ReplyReview(
318
                        $replyDate,
319
                        $replyText
320
                    );
321
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
322
                }
323
            }
324
325
            $reviews[] = new Review(
326
                $reviewId,
327
                $reviewUrl,
328
                $userName,
329
                $text,
330
                $avatar,
331
                $date,
332
                $score,
333
                $likeCount,
334
                $reply
335
            );
336
        }
337
        return $reviews;
338
    }
339
}
340