GPlayApps::saveGoogleImages()   B
last analyzed

Complexity

Conditions 10
Paths 5

Size

Total Lines 91
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 10.0929

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 53
c 1
b 0
f 0
dl 0
loc 91
ccs 37
cts 41
cp 0.9024
rs 7.1587
cc 10
nc 5
nop 3
crap 10.0929

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
3
declare(strict_types=1);
4
5
/*
6
 * Copyright (c) Ne-Lexa
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 *
11
 * @see https://github.com/Ne-Lexa/google-play-scraper
12
 */
13
14
namespace Nelexa\GPlay;
15
16
use GuzzleHttp\Promise\EachPromise;
17
use GuzzleHttp\Promise\FulfilledPromise;
18
use GuzzleHttp\Psr7\Query;
19
use GuzzleHttp\Psr7\Request as PsrRequest;
20
use GuzzleHttp\Psr7\Uri;
21
use GuzzleHttp\RequestOptions;
22
use Nelexa\GPlay\Enum\CategoryEnum;
23
use Nelexa\GPlay\Exception\GooglePlayException;
24
use Nelexa\GPlay\HttpClient\HttpClient;
25
use Nelexa\GPlay\HttpClient\Request;
26
use Nelexa\GPlay\Model\Category;
27
use Psr\Http\Message\ResponseInterface;
28
use Psr\SimpleCache\CacheInterface;
29
30
/**
31
 * Contains methods for extracting information about Android applications from the Google Play store.
32
 */
33
class GPlayApps
34
{
35
    /** @var string Default request locale. */
36
    public const DEFAULT_LOCALE = 'en_US';
37
38
    /** @var string Default request country. */
39
    public const DEFAULT_COUNTRY = 'us';
40
41
    /** @var string Google Play base url. */
42
    public const GOOGLE_PLAY_URL = 'https://play.google.com';
43
44
    /** @var string Google Play apps url. */
45
    public const GOOGLE_PLAY_APPS_URL = self::GOOGLE_PLAY_URL . '/store/apps';
46
47
    /** @var int Unlimit results. */
48
    public const UNLIMIT = -1;
49
50
    /** @internal */
51
    public const REQ_PARAM_LOCALE = 'hl';
52
53
    /** @internal */
54
    public const REQ_PARAM_COUNTRY = 'gl';
55
56
    /** @internal */
57
    public const REQ_PARAM_ID = 'id';
58
59
    /** @var string Locale (language) for HTTP requests to Google Play */
60
    protected $defaultLocale;
61
62
    /** @var string Country for HTTP requests to Google Play */
63
    protected $defaultCountry;
64
65
    /**
66
     * Creates an object to retrieve data about Android applications from the Google Play store.
67
     *
68
     * @param string $locale  locale (language) for HTTP requests to Google Play
69
     *                        or {@see GPlayApps::DEFAULT_LOCALE}
70
     * @param string $country country for HTTP requests to Google Play
71
     *                        or {@see GPlayApps::DEFAULT_COUNTRY}
72
     *
73
     * @see GPlayApps::DEFAULT_LOCALE Default request locale.
74
     * @see GPlayApps::DEFAULT_COUNTRY Default request country.
75
     */
76 63
    public function __construct(
77
        string $locale = self::DEFAULT_LOCALE,
78
        string $country = self::DEFAULT_COUNTRY
79
    ) {
80
        $this
81 63
            ->setDefaultLocale($locale)
82 63
            ->setDefaultCountry($country)
83
        ;
84
    }
85
86
    /**
87
     * Sets caching for HTTP requests.
88
     *
89
     * @param CacheInterface|null    $cache    PSR-16 Simple Cache instance
90
     * @param \DateInterval|int|null $cacheTtl TTL cached data
91
     *
92
     * @return GPlayApps returns the current class instance to allow method chaining
93
     */
94
    public function setCache(?CacheInterface $cache, $cacheTtl = null): self
95
    {
96
        $this->getHttpClient()->setCache($cache);
97
        $this->setCacheTtl($cacheTtl);
98
99
        return $this;
100
    }
101
102
    /**
103
     * Sets cache ttl.
104
     *
105
     * @param \DateInterval|int|null $cacheTtl TTL cached data
106
     *
107
     * @return GPlayApps returns the current class instance to allow method chaining
108
     */
109
    public function setCacheTtl($cacheTtl): self
110
    {
111
        $this->getHttpClient()->setOption('cache_ttl', $cacheTtl);
112
113
        return $this;
114
    }
115
116
    /**
117
     * Returns an instance of HTTP client.
118
     *
119
     * @return HttpClient http client
120
     */
121 50
    protected function getHttpClient(): HttpClient
122
    {
123
        static $httpClient;
124
125 50
        if ($httpClient === null) {
126 1
            $httpClient = new HttpClient();
127
        }
128
129 50
        return $httpClient;
130
    }
131
132
    /**
133
     * Sets the limit of concurrent HTTP requests.
134
     *
135
     * @param int $concurrency maximum number of concurrent HTTP requests
136
     *
137
     * @return GPlayApps returns the current class instance to allow method chaining
138
     */
139 3
    public function setConcurrency(int $concurrency): self
140
    {
141 3
        $this->getHttpClient()->setConcurrency($concurrency);
142
143 3
        return $this;
144
    }
145
146
    /**
147
     * Sets proxy for outgoing HTTP requests.
148
     *
149
     * @param string|null $proxy Proxy url, ex. socks5://127.0.0.1:9050 or https://116.90.233.2:47348
150
     *
151
     * @return GPlayApps returns the current class instance to allow method chaining
152
     *
153
     * @see https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html Description of proxy URL formats in CURL.
154
     */
155
    public function setProxy(?string $proxy): self
156
    {
157
        $this->getHttpClient()->setOption(RequestOptions::PROXY, $proxy);
158
159
        return $this;
160
    }
161
162
    /**
163
     * Returns the full detail of an application.
164
     *
165
     * For information, you must specify the application ID (android package name).
166
     * The application ID can be viewed in the Google Play store:
167
     * `https://play.google.com/store/apps/details?id=XXXXXX` , where
168
     * XXXXXX is the application id.
169
     *
170
     * Or it can be found in the APK file.
171
     * ```shell
172
     * aapt dump badging file.apk | grep package | awk '{print $2}' | sed s/name=//g | sed s/\'//g
173
     * ```
174
     *
175
     * @param string|Model\AppId $appId google play app id (Android package name)
176
     *
177
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
178
     *
179
     * @return Model\AppInfo full detail of an application or exception
180
     *
181
     * @api
182
     */
183 8
    public function getAppInfo($appId): Model\AppInfo
184
    {
185 8
        return $this->getAppsInfo([$appId])[0];
186
    }
187
188
    /**
189
     * Returns the full detail of multiple applications.
190
     *
191
     * The keys of the returned array matches to the passed array.
192
     * HTTP requests are executed in parallel.
193
     *
194
     * @param string[]|Model\AppId[] $appIds array of application ids
195
     *
196
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
197
     *
198
     * @return Model\AppInfo[] an array of detailed information for each application
199
     *
200
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
201
     *
202
     * @api
203
     */
204 14
    public function getAppsInfo(array $appIds): array
205
    {
206 14
        if (empty($appIds)) {
207 1
            return [];
208
        }
209
210 13
        $infoScraper = new Scraper\AppInfoScraper();
211 13
        $requests = [];
212
213 13
        foreach ($appIds as $key => $appId) {
214 13
            $fullUrl = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry)->getFullUrl();
215 11
            $psrRequest = new PsrRequest('GET', $fullUrl);
216 11
            $requests[$key] = new Request($psrRequest, [], $infoScraper);
0 ignored issues
show
Bug introduced by
$infoScraper of type Nelexa\GPlay\Scraper\AppInfoScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

216
            $requests[$key] = new Request($psrRequest, [], /** @scrutinizer ignore-type */ $infoScraper);
Loading history...
217
        }
218
219
        try {
220 11
            return $this->getHttpClient()->requestPool($requests);
221 2
        } catch (\Throwable $e) {
222 2
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
223
        }
224
    }
225
226
    /**
227
     * Returns the full details of an application in multiple languages.
228
     *
229
     * HTTP requests are executed in parallel.
230
     *
231
     * @param string|Model\AppId $appId   google Play app ID (Android package name)
232
     * @param string[]           $locales array of locales
233
     *
234
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
235
     *
236
     * @return array<string, Model\AppInfo> An array of detailed information for each locale.
237
     *                                      The array key is the locale.
238
     *
239
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
240
     *
241
     * @api
242
     */
243 3
    public function getAppInfoForLocales($appId, array $locales): array
244
    {
245 3
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
246 2
        $apps = [];
247
248 2
        foreach ($locales as $locale) {
249 2
            $apps[$locale] = new Model\AppId($appId->getId(), $locale, $appId->getCountry());
250
        }
251
252 2
        return $this->getAppsInfo($apps);
253
    }
254
255
    /**
256
     * Returns detailed application information for all available locales.
257
     *
258
     * Information is returned only for the description loaded by the developer.
259
     * All locales with automated translation from Google Translate will be ignored.
260
     * HTTP requests are executed in parallel.
261
     *
262
     * @param string|Model\AppId $appId application ID (Android package name) as
263
     *                                  a string or {@see Model\AppId} object
264
     *
265
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
266
     *
267
     * @return array<string, Model\AppInfo> An array with detailed information about the application
268
     *                                      on all available locales. The array key is the locale.
269
     *
270
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
271
     *
272
     * @api
273
     */
274 2
    public function getAppInfoForAvailableLocales($appId): array
275
    {
276 2
        return $this->getAppInfoForLocales($appId, Util\LocaleHelper::SUPPORTED_LOCALES);
277
    }
278
279
    /**
280
     * Checks if the specified application exists in the Google Play store.
281
     *
282
     * @param string|Model\AppId $appId application ID (Android package name) as
283
     *                                  a string or {@see Model\AppId} object
284
     *
285
     * @return bool returns `true` if the application exists, or `false` if not
286
     *
287
     * @api
288
     */
289 5
    public function existsApp($appId): bool
290
    {
291 5
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
292 5
        $fullUrl = $appId->getFullUrl();
293 5
        $psrRequest = new PsrRequest('HEAD', $fullUrl);
294 5
        $request = new Request($psrRequest, [
295
            RequestOptions::HTTP_ERRORS => false,
296 5
        ], new Scraper\ExistsAppScraper());
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\ExistsAppScraper() of type Nelexa\GPlay\Scraper\ExistsAppScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

296
        ], /** @scrutinizer ignore-type */ new Scraper\ExistsAppScraper());
Loading history...
297
298
        try {
299 5
            return (bool) $this->getHttpClient()->request($request);
300
        } catch (\Throwable $e) {
301
            return false;
302
        }
303
    }
304
305
    /**
306
     * Checks if the specified applications exist in the Google Play store.
307
     * HTTP requests are executed in parallel.
308
     *
309
     * @param string[]|Model\AppId[] $appIds Array of application identifiers.
310
     *                                       The keys of the returned array correspond to the transferred array.
311
     *
312
     * @throws Exception\GooglePlayException if an HTTP error other than 404 is received
313
     *
314
     * @return bool[] An array of information about the existence of each
315
     *                application in the store Google Play. The keys of the returned
316
     *                array matches to the passed array.
317
     *
318
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
319
     *
320
     * @api
321
     */
322 1
    public function existsApps(array $appIds): array
323
    {
324 1
        if (empty($appIds)) {
325
            return [];
326
        }
327
328 1
        $parseHandler = new Scraper\ExistsAppScraper();
329 1
        $requests = array_map(function ($appId) use ($parseHandler) {
330 1
            $fullUrl = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry)->getFullUrl();
331 1
            $psrRequest = new PsrRequest('HEAD', $fullUrl);
332
333 1
            return new Request($psrRequest, [
334
                RequestOptions::HTTP_ERRORS => false,
335
            ], $parseHandler);
0 ignored issues
show
Bug introduced by
$parseHandler of type Nelexa\GPlay\Scraper\ExistsAppScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

335
            ], /** @scrutinizer ignore-type */ $parseHandler);
Loading history...
336
        }, $appIds);
337
338
        try {
339 1
            return $this->getHttpClient()->requestPool($requests);
340
        } catch (\Throwable $e) {
341
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
342
        }
343
    }
344
345
    /**
346
     * Returns reviews of the Android app in the Google Play store.
347
     *
348
     * Getting a lot of reviews can take a lot of time.
349
     *
350
     * @param string|Model\AppId $appId application ID (Android package name) as
351
     *                                  a string or {@see Model\AppId} object
352
     * @param int                $limit Maximum number of reviews. To extract all
353
     *                                  reviews, use {@see GPlayApps::UNLIMIT}.
354
     * @param Enum\SortEnum|null $sort  Sort reviews of the application.
355
     *                                  If null, then sort by the newest reviews.
356
     *
357
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
358
     *
359
     * @return Model\Review[] app reviews
360
     *
361
     * @see Enum\SortEnum Contains all valid values for the "sort" parameter.
362
     * @see GPlayApps::UNLIMIT Limit for all available results.
363
     *
364
     * @api
365
     */
366 1
    public function getReviews($appId, int $limit = 100, ?Enum\SortEnum $sort = null): array
367
    {
368 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
369 1
        $sort = $sort ?? Enum\SortEnum::NEWEST();
370
371 1
        $allCount = 0;
372 1
        $token = null;
373 1
        $allReviews = [];
374
375 1
        $cacheTtl = $sort === Enum\SortEnum::NEWEST()
376 1
            ? \DateInterval::createFromDateString('1 min')
377 1
            : \DateInterval::createFromDateString('1 hour');
378
379
        try {
380
            do {
381 1
                $count = $limit === self::UNLIMIT
382
                    ? Scraper\PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE
383 1
                    : min(Scraper\PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE, max($limit - $allCount, 1));
384
385 1
                $psrRequest = Scraper\PlayStoreUiRequest::getReviewsRequest($appId, $count, $sort, $token);
386 1
                $request = new Request($psrRequest, [
387
                    'cache_ttl' => $cacheTtl,
388 1
                ], new Scraper\ReviewsScraper($appId));
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\ReviewsScraper($appId) of type Nelexa\GPlay\Scraper\ReviewsScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

388
                ], /** @scrutinizer ignore-type */ new Scraper\ReviewsScraper($appId));
Loading history...
389 1
                [$reviews, $token] = $this->getHttpClient()->request($request);
390 1
                $allCount += \count($reviews);
391 1
                $allReviews[] = $reviews;
392 1
            } while ($token !== null && ($limit === self::UNLIMIT || $allCount < $limit));
393
        } catch (\Throwable $e) {
394
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
395
        }
396
397 1
        $reviews = empty($allReviews) ? $allReviews : array_merge(...$allReviews);
398 1
        if ($limit !== self::UNLIMIT) {
399 1
            $reviews = \array_slice($reviews, 0, $limit);
400
        }
401
402 1
        return $reviews;
403
    }
404
405
    /**
406
     * @deprecated Feature no longer available
407
     *
408
     * @param mixed  $appId
409
     * @param string $reviewId
410
     *
411
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
412
     */
413
    public function getReviewById($appId, string $reviewId): Model\Review
414
    {
415
        throw new Exception\GooglePlayException('Feature no longer available', 0);
416
    }
417
418
    /**
419
     * Returns a list of permissions for the application.
420
     *
421
     * @param string|Model\AppId $appId application ID (Android package name) as
422
     *                                  a string or {@see Model\AppId} object
423
     *
424
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
425
     *
426
     * @return Model\Permission[] an array of permissions for the application
427
     *
428
     * @api
429
     */
430 1
    public function getPermissions($appId): array
431
    {
432 1
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
433
434
        try {
435 1
            $psrRequest = Scraper\PlayStoreUiRequest::getPermissionsRequest($appId);
436
437 1
            return $this->getHttpClient()->request(
438 1
                new Request(
439
                    $psrRequest,
440 1
                    [],
441 1
                    new Scraper\PermissionScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\PermissionScraper() of type Nelexa\GPlay\Scraper\PermissionScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

441
                    /** @scrutinizer ignore-type */ new Scraper\PermissionScraper()
Loading history...
442
                )
443
            );
444
        } catch (\Throwable $e) {
445
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
446
        }
447
    }
448
449
    /**
450
     * Returns an array of application categories from the Google Play store.
451
     *
452
     * @return Model\Category[] array of application categories
453
     *
454
     * @api
455
     */
456 1
    public function getCategories(): array
457
    {
458 1
        return array_map(static function (CategoryEnum $category) {
459 1
            $categoryName = $category->value();
460 1
            $categoryName = str_replace('_', ' ', $categoryName);
461 1
            $categoryName = ucfirst(strtolower($categoryName));
462 1
            $categoryName = str_replace(' and ', ' & ', $categoryName);
463
464 1
            return new Category($category->name(), $categoryName);
465 1
        }, CategoryEnum::values());
466
    }
467
468
    /**
469
     * Returns information about the developer: name, icon, cover, description and website address.
470
     *
471
     * @param string|Model\Developer|Model\App $developerId developer id as
472
     *                                                      string, {@see Model\Developer}
473
     *                                                      or {@see Model\App} object
474
     *
475
     * @throws Exception\GooglePlayException if HTTP error is received
476
     *
477
     * @return Model\Developer information about the application developer
478
     *
479
     * @see GPlayApps::getDeveloperInfoForLocales() Returns information about the developer for the locale array.
480
     *
481
     * @api
482
     */
483 5
    public function getDeveloperInfo($developerId): Model\Developer
484
    {
485 5
        $developerId = Util\Caster::castToDeveloperId($developerId);
486
487 5
        if (!is_numeric($developerId)) {
488 3
            throw new Exception\GooglePlayException(
489 3
                sprintf(
490
                    'Developer "%s" does not have a personalized page on Google Play.',
491
                    $developerId
492
                )
493
            );
494
        }
495
496 2
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev?' . http_build_query([
497
            self::REQ_PARAM_ID => $developerId,
498 2
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
499
        ]);
500
501
        try {
502 2
            return $this->getHttpClient()->request(
503 2
                new Request(
504 2
                    new PsrRequest('GET', $url),
505 2
                    [],
506 2
                    new Scraper\DeveloperInfoScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\DeveloperInfoScraper() of type Nelexa\GPlay\Scraper\DeveloperInfoScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

506
                    /** @scrutinizer ignore-type */ new Scraper\DeveloperInfoScraper()
Loading history...
507
                )
508
            );
509 1
        } catch (\Throwable $e) {
510 1
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
511
        }
512
    }
513
514
    /**
515
     * Returns information about the developer for the specified locales.
516
     *
517
     * @param string|Model\Developer|Model\App $developerId developer id as
518
     *                                                      string, {@see Model\Developer}
519
     *                                                      or {@see Model\App} object
520
     * @param string[]                         $locales     array of locales
521
     *
522
     * @throws Exception\GooglePlayException if HTTP error is received
523
     *
524
     * @return Model\Developer[] an array with information about the application developer
525
     *                           for each requested locale
526
     *
527
     * @see GPlayApps::setConcurrency() Sets the limit of concurrent HTTP requests.
528
     * @see GPlayApps::getDeveloperInfo() Returns information about the developer: name,
529
     *     icon, cover, description and website address.
530
     *
531
     * @api
532
     */
533 1
    public function getDeveloperInfoForLocales($developerId, array $locales = []): array
534
    {
535 1
        if (empty($locales)) {
536
            return [];
537
        }
538 1
        $locales = Util\LocaleHelper::getNormalizeLocales($locales);
539
540 1
        $id = Util\Caster::castToDeveloperId($developerId);
541
542 1
        if (!is_numeric($id)) {
543
            throw new Exception\GooglePlayException(
544
                sprintf(
545
                    'Developer "%s" does not have a personalized page on Google Play.',
546
                    $id
547
                )
548
            );
549
        }
550
551 1
        $requests = [];
552 1
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
553 1
        $parseHandler = new Scraper\DeveloperInfoScraper();
554
555 1
        foreach ($locales as $locale) {
556 1
            $requestUrl = $url . '?' . http_build_query(
557
                [
558 1
                    self::REQ_PARAM_ID => $id,
559
                    self::REQ_PARAM_LOCALE => $locale,
560
                ]
561
            );
562 1
            $requests[$locale] = new Request(
563 1
                new PsrRequest('GET', $requestUrl),
564 1
                [],
565
                $parseHandler
0 ignored issues
show
Bug introduced by
$parseHandler of type Nelexa\GPlay\Scraper\DeveloperInfoScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

565
                /** @scrutinizer ignore-type */ $parseHandler
Loading history...
566
            );
567
        }
568
569
        try {
570 1
            return $this->getHttpClient()->requestPool($requests);
571
        } catch (\Throwable $e) {
572
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
573
        }
574
    }
575
576
    /**
577
     * Returns an array of applications from the Google Play store by developer id.
578
     *
579
     * @param string|Model\Developer|Model\App $developerId developer id as
580
     *                                                      string, {@see Model\Developer}
581
     *                                                      or {@see Model\App} object
582
     *
583
     * @throws Exception\GooglePlayException if HTTP error is received
584
     *
585
     * @return Model\App[] an array of applications with basic information
586
     *
587
     * @api
588
     */
589 1
    public function getDeveloperApps($developerId): array
590
    {
591 1
        $developerId = Util\Caster::castToDeveloperId($developerId);
592
593
        $query = [
594
            self::REQ_PARAM_ID => $developerId,
595 1
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
596 1
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
597
        ];
598
599 1
        if (is_numeric($developerId)) {
600 1
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/dev?' . http_build_query($query);
601
602
            try {
603
                /**
604
                 * @var string|null $developerUrl
605
                 */
606 1
                $developerUrl = $this->getHttpClient()->request(
607 1
                    new Request(
608 1
                        new PsrRequest('GET', $developerUrl),
609 1
                        [],
610 1
                        new Scraper\FindDevAppsUrlScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\FindDevAppsUrlScraper() of type Nelexa\GPlay\Scraper\FindDevAppsUrlScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

610
                        /** @scrutinizer ignore-type */ new Scraper\FindDevAppsUrlScraper()
Loading history...
611
                    )
612
                );
613
614 1
                if ($developerUrl === null) {
615
                    return [];
616
                }
617
618 1
                $developerUrl .= '&' . self::REQ_PARAM_LOCALE . '=' . urlencode($this->defaultLocale)
619 1
                    . '&' . self::REQ_PARAM_COUNTRY . '=' . urlencode($this->defaultCountry);
620
            } catch (\Throwable $e) {
621 1
                throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
622
            }
623
        } else {
624 1
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/developer?' . http_build_query($query);
625
        }
626
627 1
        return $this->fetchAppsFromClusterPage(
628
            $developerUrl,
629
            self::UNLIMIT
630
        );
631
    }
632
633
    /**
634
     * Returns an iterator of applications from the Google Play store for the specified cluster page.
635
     *
636
     * @param string $clusterPageUrl cluster page url
637
     *
638
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
639
     *
640
     * @return \Generator<Model\App> an iterator with basic information about applications
641
     */
642 10
    public function getClusterApps(string $clusterPageUrl): \Generator
643
    {
644 10
        $clusterUri = new Uri($clusterPageUrl);
645 10
        $query = Query::parse($clusterUri->getQuery());
646
647 10
        if (!isset($query[self::REQ_PARAM_LOCALE])) {
648 7
            $query[self::REQ_PARAM_LOCALE] = $this->defaultLocale;
649
        }
650
651 10
        if (!isset($query[self::REQ_PARAM_COUNTRY])) {
652 8
            $query[self::REQ_PARAM_COUNTRY] = $this->defaultCountry;
653
        }
654
655 10
        $clusterUri = $clusterUri->withQuery(Query::build($query));
656 10
        $clusterPageUrl = (string) $clusterUri;
657
658
        try {
659 10
            [$apps, $token] = $this->getHttpClient()->request(
660 10
                new Request(
661 10
                    new PsrRequest('GET', $clusterPageUrl),
662 10
                    [],
663 10
                    new Scraper\ClusterAppsScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\ClusterAppsScraper() of type Nelexa\GPlay\Scraper\ClusterAppsScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

663
                    /** @scrutinizer ignore-type */ new Scraper\ClusterAppsScraper()
Loading history...
664
                )
665
            );
666
667 10
            foreach ($apps as $app) {
668 10
                yield $app;
669
            }
670
671 10
            while ($token !== null) {
672 6
                $request = Scraper\PlayStoreUiRequest::getAppsRequest(
673 6
                    $query[self::REQ_PARAM_LOCALE],
674 6
                    $query[self::REQ_PARAM_COUNTRY],
675
                    Scraper\PlayStoreUiRequest::LIMIT_APPS_ON_PAGE,
676
                    $token
677
                );
678
679 6
                [$apps, $token] = $this->getHttpClient()->request(
680 6
                    new Request(
681
                        $request,
682 6
                        [],
683 6
                        new Scraper\PlayStoreUiAppsScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\PlayStoreUiAppsScraper() of type Nelexa\GPlay\Scraper\PlayStoreUiAppsScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

683
                        /** @scrutinizer ignore-type */ new Scraper\PlayStoreUiAppsScraper()
Loading history...
684
                    )
685
                );
686
687 6
                foreach ($apps as $app) {
688 6
                    yield $app;
689
                }
690
            }
691
        } catch (\Throwable $e) {
692
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
693
        }
694
    }
695
696
    /**
697
     * Returns a list of applications with basic information.
698
     *
699
     * @param string $clusterPageUrl cluster page URL
700
     * @param int    $limit          Maximum number of applications. To extract all
701
     *                               applications, use {@see GPlayApps::UNLIMIT}.
702
     *
703
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
704
     *
705
     * @return Model\App[] array of applications with basic information about them
706
     *
707
     * @see GPlayApps::UNLIMIT Limit for all available results.
708
     */
709 3
    protected function fetchAppsFromClusterPage(
710
        string $clusterPageUrl,
711
        int $limit
712
    ): array {
713 3
        $apps = [];
714 3
        $count = 0;
715
716 3
        foreach ($this->getClusterApps($clusterPageUrl) as $app) {
717 3
            $apps[] = $app;
718 3
            ++$count;
719 3
            if ($count === $limit) {
720
                break;
721
            }
722
        }
723
724 3
        return $apps;
725
    }
726
727
    /**
728
     * Returns an array of similar applications with basic information about
729
     * them in the Google Play store.
730
     *
731
     * @param string|Model\AppId $appId application ID (Android package name)
732
     *                                  as a string or {@see Model\AppId} object
733
     * @param int                $limit The maximum number of similar applications.
734
     *                                  To extract all similar applications,
735
     *                                  use {@see GPlayApps::UNLIMIT}.
736
     *
737
     * @throws Exception\GooglePlayException if the application is not exists or other HTTP error
738
     *
739
     * @return Model\App[] an array of applications with basic information about them
740
     *
741
     * @see GPlayApps::UNLIMIT Limit for all available results.
742
     *
743
     * @api
744
     */
745 2
    public function getSimilarApps($appId, int $limit = 50): array
746
    {
747 2
        $appId = Util\Caster::castToAppId($appId, $this->defaultLocale, $this->defaultCountry);
748
749
        try {
750
            /** @var string|null $similarAppsUrl */
751 2
            $similarAppsUrl = $this->getHttpClient()->request(
752 2
                new Request(
753 2
                    new PsrRequest('GET', $appId->getFullUrl()),
754 2
                    [],
755 2
                    new Scraper\FindSimilarAppsUrlScraper($appId)
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper...rAppsUrlScraper($appId) of type Nelexa\GPlay\Scraper\FindSimilarAppsUrlScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

755
                    /** @scrutinizer ignore-type */ new Scraper\FindSimilarAppsUrlScraper($appId)
Loading history...
756
                )
757
            );
758
759 2
            if ($similarAppsUrl === null) {
760
                return [];
761
            }
762
763 2
            return $this->fetchAppsFromClusterPage(
764
                $similarAppsUrl,
765
                $limit
766
            );
767
        } catch (\Throwable $e) {
768
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
769
        }
770
    }
771
772
    /**
773
     * Returns an iterator of cluster pages.
774
     *
775
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
776
     *                                                               string, {@see Model\Category},
777
     *                                                               {@see Enum\CategoryEnum} or
778
     *                                                               `null` for all categories
779
     * @param Enum\AgeEnum|null                            $age      age limit or `null` for no limit
780
     *
781
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
782
     *
783
     * @return \Generator<Model\ClusterPage> an iterator of cluster pages
784
     */
785 8
    public function getClusterPages($category = null, ?Enum\AgeEnum $age = null): \Generator
786
    {
787
        $queryParams = [
788 8
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
789 8
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
790
        ];
791
792 8
        if ($age !== null) {
793
            $queryParams['age'] = $age->value();
794
        }
795
796 8
        $url = self::GOOGLE_PLAY_APPS_URL;
797
798 8
        if ($category !== null) {
799 6
            $url .= '/category/' . Util\Caster::castToCategoryId($category);
800
        }
801 8
        $url .= '?' . http_build_query($queryParams);
802
803 8
        ['results' => $results, 'token' => $token] = $this->getHttpClient()->request(
804 8
            new Request(
805 8
                new PsrRequest('GET', $url),
806 8
                [],
807 8
                new Scraper\ClusterPagesFromListAppsScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper...esFromListAppsScraper() of type Nelexa\GPlay\Scraper\Clu...agesFromListAppsScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

807
                /** @scrutinizer ignore-type */ new Scraper\ClusterPagesFromListAppsScraper()
Loading history...
808
            )
809
        );
810
811 8
        foreach ($results as $result) {
812 8
            yield $result;
813
        }
814
815 8
        while ($token !== null) {
816
            try {
817 5
                $psrRequest = Scraper\PlayStoreUiRequest::getClusterPagesRequest(
818
                    $token,
819 5
                    $this->defaultLocale,
820 5
                    $this->defaultCountry
821
                );
822
823 5
                ['results' => $results, 'token' => $token] = $this->getHttpClient()->request(
824 5
                    new Request(
825
                        $psrRequest,
826 5
                        [],
827 5
                        new Scraper\ClusterPagesFromClusterResponseScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper...lusterResponseScraper() of type Nelexa\GPlay\Scraper\Clu...mClusterResponseScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

827
                        /** @scrutinizer ignore-type */ new Scraper\ClusterPagesFromClusterResponseScraper()
Loading history...
828
                    )
829
                );
830
831 5
                foreach ($results as $result) {
832 5
                    yield $result;
833
                }
834
            } catch (\Throwable $e) {
835
                throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
836
            }
837
        }
838
    }
839
840
    /**
841
     * Returns the Google Play search suggests.
842
     *
843
     * @param string $query search query
844
     *
845
     * @throws Exception\GooglePlayException if HTTP error is received
846
     *
847
     * @return string[] array containing search suggestions
848
     *
849
     * @api
850
     */
851 2
    public function getSearchSuggestions(string $query): array
852
    {
853 2
        $query = trim($query);
854
855 2
        if ($query === '') {
856
            return [];
857
        }
858
859
        try {
860 2
            $psrRequest = Scraper\PlayStoreUiRequest::getSuggestRequest(
861
                $query,
862 2
                $this->defaultLocale,
863 2
                $this->defaultCountry
864
            );
865
866
            /** @var string[] $suggestions */
867 2
            $suggestions = $this->getHttpClient()->request(
868 2
                new Request(
869
                    $psrRequest,
870 2
                    [],
871 2
                    new Scraper\SuggestScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\SuggestScraper() of type Nelexa\GPlay\Scraper\SuggestScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

871
                    /** @scrutinizer ignore-type */ new Scraper\SuggestScraper()
Loading history...
872
                )
873
            );
874
        } catch (\Throwable $e) {
875
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
876
        }
877
878 2
        return $suggestions;
879
    }
880
881
    /**
882
     * Returns a list of applications from the Google Play store for a search query.
883
     *
884
     * @param string              $query search query
885
     * @param int                 $limit the limit on the number of search results
886
     * @param Enum\PriceEnum|null $price price category or `null`
887
     *
888
     * @throws Exception\GooglePlayException if HTTP error is received
889
     *
890
     * @return Model\App[] an array of applications with basic information
891
     *
892
     * @see Enum\PriceEnum Contains all valid values for the "price" parameter.
893
     * @see GPlayApps::UNLIMIT Limit for all available results.
894
     *
895
     * @api
896
     */
897 1
    public function search(string $query, int $limit = 50, ?Enum\PriceEnum $price = null): array
898
    {
899 1
        $query = trim($query);
900
901 1
        if (empty($query)) {
902
            throw new \InvalidArgumentException('Search query missing');
903
        }
904 1
        $price = $price ?? Enum\PriceEnum::ALL();
905
906
        $params = [
907
            'c' => 'apps',
908
            'q' => $query,
909 1
            self::REQ_PARAM_LOCALE => $this->defaultLocale,
910 1
            self::REQ_PARAM_COUNTRY => $this->defaultCountry,
911 1
            'price' => $price->value(),
912
        ];
913 1
        $clusterPageUrl = self::GOOGLE_PLAY_URL . '/store/search?' . http_build_query($params);
914
915 1
        $apps = [];
916 1
        $count = 0;
917
918 1
        foreach ($this->getClusterApps($clusterPageUrl) as $app) {
919 1
            $apps[] = $app;
920 1
            ++$count;
921 1
            if ($count === $limit) {
922
                break;
923
            }
924
        }
925
926 1
        return $apps;
927
    }
928
929
    /**
930
     * Returns an array of applications from the Google Play store for the specified category.
931
     *
932
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
933
     *                                                               string, {@see Model\Category},
934
     *                                                               {@see Enum\CategoryEnum} or
935
     *                                                               `null` for all categories
936
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
937
     * @param int                                          $limit    limit on the number of results
938
     *                                                               or {@see GPlayApps::UNLIMIT}
939
     *                                                               for no limit
940
     *
941
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
942
     *
943
     * @return Model\App[] an array of applications with basic information
944
     *
945
     * @api
946
     *
947
     * @see GPlayApps::UNLIMIT Limit for all available results.
948
     */
949 5
    public function getListApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
950
    {
951 5
        return $this->fetchAppsFromClusterPages($category, $age, $limit);
952
    }
953
954
    /**
955
     * Returns an array of **top apps** from the Google Play store for the specified category.
956
     *
957
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
958
     *                                                               string, {@see Model\Category},
959
     *                                                               {@see Enum\CategoryEnum} or
960
     *                                                               `null` for all categories
961
     * @param int                                          $limit    limit on the number of results
962
     *                                                               or {@see GPlayApps::UNLIMIT}
963
     *                                                               for no limit
964
     * @param Enum\AgeEnum|null                            $age
965
     *
966
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
967
     *
968
     * @return Model\App[] an array of applications with basic information
969
     *
970
     * @api
971
     *
972
     * @see GPlayApps::UNLIMIT Limit for all available results.
973
     * @deprecated Use {@see \Nelexa\GPlay\GPlayApps::getTopSellingFreeApps}, {@see \Nelexa\GPlay\GPlayApps::getTopSellingPaidApps} and {@see \Nelexa\GPlay\GPlayApps::getTopGrossingApps}
974
     */
975
    public function getTopApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
976
    {
977
        return $this->getTopSellingFreeApps($category, $limit);
978
    }
979
980
    /**
981
     * Returns an array of **top selling free apps** from the Google Play store for the specified category.
982
     *
983
     * @param string|Model\Category|Enum\CategoryEnum $category application category as string, {@see Model\Category}, {@see Enum\CategoryEnum}, ex. APPLICATION or GAME
984
     * @param int                                     $limit    Limit
985
     *
986
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
987
     *
988
     * @return Model\App[] App list
989
     */
990 4
    public function getTopSellingFreeApps($category = 'APPLICATION', int $limit = 500): array
991
    {
992 4
        return $this->fetchTopApps($category ?? 'APPLICATION', 'topselling_free', $limit);
993
    }
994
995
    /**
996
     * Returns an array of **top selling paid apps** from the Google Play store for the specified category.
997
     *
998
     * @param string|Model\Category|Enum\CategoryEnum $category application category as string, {@see Model\Category}, {@see Enum\CategoryEnum}, ex. APPLICATION or GAME
999
     * @param int                                     $limit    Limit
1000
     *
1001
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
1002
     *
1003
     * @return Model\App[] App list
1004
     */
1005 4
    public function getTopSellingPaidApps($category = 'APPLICATION', int $limit = 500): array
1006
    {
1007 4
        return $this->fetchTopApps($category, 'topselling_paid', $limit);
1008
    }
1009
1010
    /**
1011
     * Returns an array of **top grossing apps** from the Google Play store for the specified category.
1012
     *
1013
     * @param string|Model\Category|Enum\CategoryEnum $category application category as string, {@see Model\Category}, {@see Enum\CategoryEnum}, ex. APPLICATION or GAME
1014
     * @param int                                     $limit    Limit
1015
     *
1016
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
1017
     *
1018
     * @return Model\App[] App list
1019
     */
1020 4
    public function getTopGrossingApps($category = 'APPLICATION', int $limit = 500): array
1021
    {
1022 4
        return $this->fetchTopApps($category, 'topgrossing', $limit);
1023
    }
1024
1025
    /**
1026
     * @param string|Model\Category|Enum\CategoryEnum $category
1027
     * @param string                                  $topSlug
1028
     * @param int                                     $limit
1029
     *
1030
     * @throws Exception\GooglePlayException
1031
     *
1032
     * @return Model\App[]
1033
     */
1034 12
    protected function fetchTopApps($category, string $topSlug, int $limit = 1000): array
1035
    {
1036
        try {
1037 12
            $psrRequest = Scraper\PlayStoreUiRequest::getTopCategoryApps(
1038
                $topSlug,
1039 12
                Util\Caster::castToCategoryId($category),
1040 12
                $this->defaultLocale,
1041 12
                $this->defaultCountry,
1042
                $limit
1043
            );
1044
1045 12
            return $this->getHttpClient()->request(
1046 12
                new Request(
1047
                    $psrRequest,
1048 12
                    [],
1049 12
                    new Scraper\CategoryTopScraper()
0 ignored issues
show
Bug introduced by
new Nelexa\GPlay\Scraper\CategoryTopScraper() of type Nelexa\GPlay\Scraper\CategoryTopScraper is incompatible with the type Closure expected by parameter $parseHandler of Nelexa\GPlay\HttpClient\Request::__construct(). ( Ignorable by Annotation )

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

1049
                    /** @scrutinizer ignore-type */ new Scraper\CategoryTopScraper()
Loading history...
1050
                )
1051
            );
1052
        } catch (\Throwable $e) {
1053
            throw new Exception\GooglePlayException($e->getMessage(), 1, $e);
1054
        }
1055
    }
1056
1057
    /**
1058
     * Returns an array of **new apps** from the Google Play store for the specified category.
1059
     *
1060
     * @param string|Model\Category|Enum\CategoryEnum|null $category application category as
1061
     *                                                               string, {@see Model\Category},
1062
     *                                                               {@see Enum\CategoryEnum} or
1063
     *                                                               `null` for all categories
1064
     * @param Enum\AgeEnum|null                            $age      age limit or null for no limit
1065
     * @param int                                          $limit    limit on the number of results
1066
     *                                                               or {@see GPlayApps::UNLIMIT}
1067
     *                                                               for no limit
1068
     *
1069
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
1070
     *
1071
     * @return Model\App[] an array of applications with basic information
1072
     *
1073
     * @api
1074
     *
1075
     * @see GPlayApps::UNLIMIT Limit for all available results.
1076
     * @deprecated
1077
     */
1078
    public function getNewApps($category = null, ?Enum\AgeEnum $age = null, int $limit = self::UNLIMIT): array
1079
    {
1080
        return $this->getListApps($category, $age, $limit);
1081
    }
1082
1083
    /**
1084
     * @param string|Model\Category|Enum\CategoryEnum|null $category
1085
     * @param Enum\AgeEnum|null                            $age
1086
     * @param int                                          $limit
1087
     *
1088
     * @throws \Nelexa\GPlay\Exception\GooglePlayException
1089
     *
1090
     * @return Model\App[]
1091
     */
1092 5
    protected function fetchAppsFromClusterPages($category, ?Enum\AgeEnum $age, int $limit): array
1093
    {
1094 5
        $apps = [];
1095 5
        $count = 0;
1096 5
        foreach ($this->getClusterPages($category, $age) as $clusterPage) {
1097
            try {
1098 5
                foreach ($this->getClusterApps($clusterPage->getUrl()) as $app) {
1099 5
                    if (!isset($apps[$app->getId()])) {
1100 5
                        $apps[$app->getId()] = $app;
1101 5
                        ++$count;
1102 5
                        if ($count === $limit) {
1103
                            break 2;
1104
                        }
1105
                    }
1106
                }
1107
            } catch (GooglePlayException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1108
            }
1109
        }
1110
1111 5
        return $apps;
1112
    }
1113
1114
    /**
1115
     * Asynchronously saves images from googleusercontent.com and similar URLs to disk.
1116
     *
1117
     * Before use, you can set the parameters of the width-height of images.
1118
     *
1119
     * Example:
1120
     * ```php
1121
     * $gplay->saveGoogleImages(
1122
     *     $images,
1123
     *     static function (\Nelexa\GPlay\Model\GoogleImage $image): string {
1124
     *         $hash = $image->getHashUrl($hashAlgo = 'md5', $parts = 2, $partLength = 2);
1125
     *         return 'path/to/screenshots/' . $hash . '.{ext}';
1126
     *     },
1127
     *     $overwrite = false
1128
     * );
1129
     * ```
1130
     *
1131
     * @param Model\GoogleImage[] $images           array of {@see Model\GoogleImage} objects
1132
     * @param callable            $destPathCallback The function to which the
1133
     *                                              {@see Model\GoogleImage} object is
1134
     *                                              passed, and you must return the full
1135
     *                                              output. path to save this file.
1136
     * @param bool                $overwrite        overwrite files if exists
1137
     *
1138
     * @return Model\ImageInfo[] returns an array with information about saved images
1139
     *
1140
     * @see Model\GoogleImage Contains a link to the image, allows you to customize its size and download it.
1141
     * @see Model\ImageInfo Contains information about the image.
1142
     *
1143
     * @api
1144
     */
1145 2
    public function saveGoogleImages(
1146
        array $images,
1147
        callable $destPathCallback,
1148
        bool $overwrite = false
1149
    ): array {
1150
        /** @var array<string, \Nelexa\GPlay\Util\LazyStream> $mapping */
1151 2
        $mapping = [];
1152
1153 2
        foreach ($images as $image) {
1154 2
            if (!$image instanceof Model\GoogleImage) {
1155
                throw new \InvalidArgumentException(
1156
                    'An array of ' . Model\GoogleImage::class . ' objects is expected.'
1157
                );
1158
            }
1159 2
            $destPath = $destPathCallback($image);
1160 2
            $url = $image->getUrl();
1161 2
            $mapping[$url] = new Util\LazyStream($destPath, 'w+b');
1162
        }
1163
1164 2
        $httpClient = $this->getHttpClient();
1165 2
        $promises = (static function () use ($mapping, $overwrite, $httpClient) {
1166 2
            foreach ($mapping as $url => $stream) {
1167 2
                $destPath = $stream->getFilename();
1168 2
                $dynamicPath = strpos($destPath, '{url}') !== false;
1169
1170 2
                if (!$overwrite && !$dynamicPath && is_file($destPath)) {
1171
                    yield $url => new FulfilledPromise($url);
1172
                } else {
1173 2
                    yield $url => $httpClient->getClient()
1174 2
                        ->requestAsync(
1175
                            'GET',
1176
                            $url,
1177
                            [
1178 2
                                RequestOptions::COOKIES => null,
1179
                                RequestOptions::SINK => $stream,
1180
                                RequestOptions::HTTP_ERRORS => true,
1181 2
                                RequestOptions::ON_HEADERS => static function (ResponseInterface $response) use (
1182
                                    $url,
1183
                                    $stream
1184
                                ): void {
1185 2
                                    Model\GoogleImage::onHeaders($response, $url, $stream);
1186
                                },
1187
                            ]
1188
                        )
1189 2
                        ->then(
1190 2
                            static function (
1191
                                /** @noinspection PhpUnusedParameterInspection */
1192
                                ResponseInterface $response
1193
                            ) use ($url) {
1194 1
                                return $url;
1195
                            }
1196
                        )
1197
                    ;
1198
                }
1199
            }
1200
        })();
1201
1202
        /**
1203
         * @var Model\ImageInfo[] $imageInfoList
1204
         */
1205 2
        $imageInfoList = [];
1206 2
        $eachPromise = (new EachPromise(
1207
            $promises,
1208
            [
1209 2
                'concurrency' => $this->getHttpClient()->getConcurrency(),
1210 2
                'fulfilled' => static function (string $url) use (&$imageInfoList, $mapping): void {
1211 1
                    $imageInfoList[] = new Model\ImageInfo($url, $mapping[$url]->getFilename());
1212
                },
1213 2
                'rejected' => static function (\Throwable $reason, string $exceptionUrl) use ($mapping): void {
1214 1
                    foreach ($mapping as $destPath => $url) {
1215 1
                        if (is_file($destPath)) {
1216
                            unlink($destPath);
1217
                        }
1218
                    }
1219
1220 1
                    throw (new Exception\GooglePlayException(
1221 1
                        $reason->getMessage(),
1222 1
                        $reason->getCode(),
1223
                        $reason
1224 1
                    ))->setUrl(
1225
                        $exceptionUrl
1226
                    );
1227
                },
1228
            ]
1229 2
        ))->promise();
1230
1231 2
        if ($eachPromise !== null) {
1232 2
            $eachPromise->wait();
1233
        }
1234
1235 1
        return $imageInfoList;
1236
    }
1237
1238
    /**
1239
     * Returns the locale (language) of the requests.
1240
     *
1241
     * @return string locale (language) for HTTP requests to Google Play
1242
     */
1243 5
    public function getDefaultLocale(): string
1244
    {
1245 5
        return $this->defaultLocale;
1246
    }
1247
1248
    /**
1249
     * Sets the locale (language) of requests.
1250
     *
1251
     * @param string $defaultLocale locale (language) for HTTP requests to Google Play
1252
     *
1253
     * @return GPlayApps returns the current class instance to allow method chaining
1254
     */
1255 63
    public function setDefaultLocale(string $defaultLocale): self
1256
    {
1257 63
        $this->defaultLocale = Util\LocaleHelper::getNormalizeLocale($defaultLocale);
1258
1259 63
        return $this;
1260
    }
1261
1262
    /**
1263
     * Returns the country of the requests.
1264
     *
1265
     * @return string country for HTTP requests to Google Play
1266
     */
1267 5
    public function getDefaultCountry(): string
1268
    {
1269 5
        return $this->defaultCountry;
1270
    }
1271
1272
    /**
1273
     * Sets the country of requests.
1274
     *
1275
     * @param string $defaultCountry country for HTTP requests to Google Play
1276
     *
1277
     * @return GPlayApps returns the current class instance to allow method chaining
1278
     */
1279 63
    public function setDefaultCountry(string $defaultCountry): self
1280
    {
1281 63
        $this->defaultCountry = !empty($defaultCountry)
1282 63
            ? $defaultCountry
1283 2
            : self::DEFAULT_COUNTRY;
1284
1285 63
        return $this;
1286
    }
1287
1288
    /**
1289
     * Sets the number of seconds to wait when trying to connect to the server.
1290
     *
1291
     * @param float $connectTimeout Connection timeout in seconds, for example 3.14. Use 0 to wait indefinitely.
1292
     *
1293
     * @return GPlayApps returns the current class instance to allow method chaining
1294
     */
1295
    public function setConnectTimeout(float $connectTimeout): self
1296
    {
1297
        $this->getHttpClient()->setConnectTimeout($connectTimeout);
1298
1299
        return $this;
1300
    }
1301
1302
    /**
1303
     * Sets the timeout of the request in second.
1304
     *
1305
     * @param float $timeout Waiting timeout in seconds, for example 3.14. Use 0 to wait indefinitely.
1306
     *
1307
     * @return GPlayApps returns the current class instance to allow method chaining
1308
     */
1309
    public function setTimeout(float $timeout): self
1310
    {
1311
        $this->getHttpClient()->setTimeout($timeout);
1312
1313
        return $this;
1314
    }
1315
}
1316