Passed
Push — master ( 80ad96...45165d )
by Alexey
03:00
created

AppDetailScraper::__invoke()   F

Complexity

Conditions 40
Paths > 20000

Size

Total Lines 228
Code Lines 181

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 181
dl 0
loc 228
rs 0
c 0
b 0
f 0
cc 40
nc 229383
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

315
                /** @scrutinizer ignore-type */ $date,
Loading history...
316
                $score,
317
                $likeCount,
318
                $reply
319
            );
320
        }
321
        return $reviews;
322
    }
323
}
324