Test Failed
Branch develop (c8bc8f)
by Alexey
08:48
created

GPlayApps::getSuggest()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
c 0
b 0
f 0
rs 9.2
cc 2
nc 3
nop 3

1 Method

Rating   Name   Duplication   Size   Complexity  
A GPlayApps.php$0 ➔ __invoke() 0 7 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Nelexa\GPlay;
5
6
use GuzzleHttp\Exception\GuzzleException;
7
use GuzzleHttp\Promise\EachPromise;
8
use GuzzleHttp\Promise\FulfilledPromise;
9
use GuzzleHttp\RequestOptions;
10
use Nelexa\GPlay\Enum\AgeEnum;
11
use Nelexa\GPlay\Enum\CategoryEnum;
12
use Nelexa\GPlay\Enum\CollectionEnum;
13
use Nelexa\GPlay\Enum\PriceEnum;
14
use Nelexa\GPlay\Enum\SortEnum;
15
use Nelexa\GPlay\Exception\GooglePlayException;
16
use Nelexa\GPlay\Http\HttpClient;
17
use Nelexa\GPlay\Http\ResponseHandlerInterface;
18
use Nelexa\GPlay\Model\App;
19
use Nelexa\GPlay\Model\AppDetail;
20
use Nelexa\GPlay\Model\Category;
21
use Nelexa\GPlay\Model\Developer;
22
use Nelexa\GPlay\Model\GoogleImage;
23
use Nelexa\GPlay\Model\ImageInfo;
24
use Nelexa\GPlay\Model\Permission;
25
use Nelexa\GPlay\Model\Review;
26
use Nelexa\GPlay\Request\PlayStoreUiRequest;
27
use Nelexa\GPlay\Request\RequestApp;
28
use Nelexa\GPlay\Scraper\AppDetailScraper;
29
use Nelexa\GPlay\Scraper\CategoriesScraper;
30
use Nelexa\GPlay\Scraper\CategoryAppsScraper;
31
use Nelexa\GPlay\Scraper\ClusterAppsScraper;
32
use Nelexa\GPlay\Scraper\DeveloperInfoScraper;
33
use Nelexa\GPlay\Scraper\ExistsAppScraper;
34
use Nelexa\GPlay\Scraper\FindDevAppsUrlScraper;
35
use Nelexa\GPlay\Scraper\FindSimilarAppsUrlScraper;
36
use Nelexa\GPlay\Scraper\PermissionScraper;
37
use Nelexa\GPlay\Scraper\PlayStoreUiAppsScraper;
38
use Nelexa\GPlay\Scraper\ReviewsScraper;
39
use Nelexa\GPlay\Util\LocaleHelper;
40
use Psr\Http\Message\RequestInterface;
41
use Psr\Http\Message\ResponseInterface;
42
use Psr\SimpleCache\CacheInterface;
43
44
/**
45
 * PHP Scraper to extract application data from the Google Play store.
46
 *
47
 * @package Nelexa\GPlay
48
 * @author Ne-Lexa
49
 */
50
class GPlayApps
51
{
52
    /**
53
     * @const Default locale
54
     */
55
    public const DEFAULT_LOCALE = 'en_US';
56
    /**
57
     * @const Default country
58
     */
59
    public const DEFAULT_COUNTRY = 'us'; // Affected price
60
61
    public const GOOGLE_PLAY_URL = 'https://play.google.com';
62
    public const GOOGLE_PLAY_APPS_URL = self::GOOGLE_PLAY_URL . '/store/apps';
63
    public const MAX_SEARCH_RESULTS = 250;
64
65
    public const REQ_PARAM_LOCALE = 'hl';
66
    public const REQ_PARAM_COUNTRY = 'gl';
67
    public const REQ_PARAM_ID = 'id';
68
69
    /**
70
     * @var int
71
     */
72
    private $concurrency = 4;
73
    /**
74
     * @var string
75
     */
76
    private $defaultLocale;
77
    /**
78
     * @var string
79
     */
80
    private $defaultCountry;
81
82
    /**
83
     * GPlayApps constructor.
84
     *
85
     * @param string $defaultLocale
0 ignored issues
show
Documentation introduced by
Should the type for parameter $defaultLocale not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
86
     * @param string $defaultCountry
0 ignored issues
show
Documentation introduced by
Should the type for parameter $defaultCountry not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
87
     */
88
    public function __construct(?string $defaultLocale = null, ?string $defaultCountry = null)
89
    {
90
        $this
91
            ->setDefaultLocale($defaultLocale ?? self::DEFAULT_LOCALE)
92
            ->setDefaultCountry($defaultCountry ?? self::DEFAULT_COUNTRY);
93
    }
94
95
    /**
96
     * @return string
97
     */
98
    public function getDefaultLocale(): string
99
    {
100
        return $this->defaultLocale;
101
    }
102
103
    /**
104
     * @param string $defaultLocale
105
     * @return GPlayApps
106
     */
107
    public function setDefaultLocale(string $defaultLocale): GPlayApps
108
    {
109
        $this->defaultLocale = LocaleHelper::getNormalizeLocale($defaultLocale);
110
        return $this;
111
    }
112
113
    /**
114
     * @return string
115
     */
116
    public function getDefaultCountry(): string
117
    {
118
        return $this->defaultCountry;
119
    }
120
121
    /**
122
     * @param string $defaultCountry
123
     * @return GPlayApps
124
     */
125
    public function setDefaultCountry(string $defaultCountry): GPlayApps
126
    {
127
        $this->defaultCountry = $defaultCountry;
128
        return $this;
129
    }
130
131
132
    /**
133
     * Sets caching for HTTP requests.
134
     *
135
     * @param CacheInterface|null $cache PSR-16 Simple Cache instance
136
     * @param \DateInterval|int|null $cacheTtl Optional. The TTL of cached data.
137
     * @return GPlayApps
138
     */
139
    public function setCache(?CacheInterface $cache, $cacheTtl = null): self
140
    {
141
        $this->getHttpClient()
142
            ->setCache($cache)
143
            ->setCacheTtl($cacheTtl);
144
        return $this;
145
    }
146
147
    /**
148
     * Returns an instance of HTTP client.
149
     *
150
     * @return HttpClient
151
     */
152
    protected function getHttpClient(): HttpClient
153
    {
154
        static $httpClient;
155
        if ($httpClient === null) {
156
            $httpClient = new HttpClient();
157
        }
158
        return $httpClient;
159
    }
160
161
    /**
162
     * Sets the limit of concurrent HTTP requests.
163
     *
164
     * @param int $concurrency maximum number of concurrent HTTP requests
165
     * @return GPlayApps
166
     */
167
    public function setConcurrency(int $concurrency): GPlayApps
168
    {
169
        $this->concurrency = max(1, $concurrency);
170
        return $this;
171
    }
172
173
    /**
174
     * Sets proxy for outgoing HTTP requests.
175
     *
176
     * @param string|null $proxy Proxy url, ex. socks5://127.0.0.1:9050 or https://116.90.233.2:47348
177
     * @return GPlayApps
178
     * @see https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html
179
     */
180
    public function setProxy(?string $proxy): GPlayApps
181
    {
182
        $this->getHttpClient()->setProxy($proxy);
183
        return $this;
184
    }
185
186
    /**
187
     * @param float $connectTimeout
188
     * @return GPlayApps
189
     */
190
    public function setConnectTimeout(float $connectTimeout): GPlayApps
191
    {
192
        $this->getHttpClient()->setConnectTimeout($connectTimeout);
193
        return $this;
194
    }
195
196
    /**
197
     * @param float $timeout
198
     * @return GPlayApps
199
     */
200
    public function setTimeout(float $timeout): GPlayApps
201
    {
202
        $this->getHttpClient()->setTimeout($timeout);
203
        return $this;
204
    }
205
206
    /**
207
     * Returns detailed information about an Android application from
208
     * Google Play by its id (package name).
209
     *
210
     * @param string|RequestApp|App $requestApp Application id (package name)
211
     *     or object {@see RequestApp} or object {@see App}.
212
     * @return AppDetail Detailed information about the Android
213
     *     application or exception.
214
     * @throws GooglePlayException if the application is not exists or other HTTP error
215
     */
216
    public function getApp($requestApp): AppDetail
217
    {
218
        return $this->getApps([$requestApp])[0];
219
    }
220
221
    /**
222
     * Returns detailed information about many android packages.
223
     * HTTP requests are executed in parallel.
224
     *
225
     * @param string[]|RequestApp[]|App[] $requestApps array of application ids or array of {@see RequestApp} or array
226
     *     of {@see App}.
227
     * @return AppDetail[] An array of detailed information for each application.
228
     *     The keys of the returned array matches to the passed array.
229
     * @throws GooglePlayException if the application is not exists or other HTTP error
230
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
231
     */
232
    public function getApps(array $requestApps): array
233
    {
234
        if (empty($requestApps)) {
235
            return [];
236
        }
237
        $urls = $this->getRequestAppsUrlList($requestApps);
238
        try {
239
            return $this->getHttpClient()->requestAsyncPool(
240
                'GET',
241
                $urls,
0 ignored issues
show
Documentation introduced by
$urls is of type array, but the function expects a object<Nelexa\GPlay\Http\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
242
                [
243
                    HttpClient::OPTION_HANDLER_RESPONSE => new AppDetailScraper(),
244
                ],
245
                $this->concurrency
246
            );
247
        } catch (GuzzleException $e) {
248
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
249
        }
250
    }
251
252
    /**
253
     * @param string[]|RequestApp[]|App[] $requestApps
254
     * @return array
255
     * @throws \InvalidArgumentException
256
     */
257
    private function getRequestAppsUrlList(array $requestApps): array
258
    {
259
        $urls = [];
260
        foreach ($requestApps as $key => $requestApp) {
261
            $requestApp = $this->castToRequestApp($requestApp);
262
            $urls[$key] = $requestApp->getFullUrl();
263
        }
264
        return $urls;
265
    }
266
267
    /**
268
     * @param string|RequestApp|App $requestApp
269
     * @return RequestApp
270
     */
271
    private function castToRequestApp($requestApp): RequestApp
272
    {
273
        if ($requestApp === null) {
274
            throw new \InvalidArgumentException('$requestApp is null');
275
        }
276
        if (is_string($requestApp)) {
277
            return new RequestApp($requestApp, $this->defaultLocale, $this->defaultCountry);
278
        }
279
        if ($requestApp instanceof App) {
280
            return new RequestApp($requestApp->getId(), $requestApp->getLocale(), $this->defaultCountry);
281
        }
282
        return $requestApp;
283
    }
284
285
    /**
286
     * Returns detailed information about an application from the Google Play store
287
     * for an array of locales. HTTP requests are executed in parallel.
288
     *
289
     * @param string|RequestApp|App $requestApp Application id (package name)
290
     *     or object {@see RequestApp} or object {@see App}.
291
     * @param string[] $locales array of locales
292
     * @return AppDetail[] array of application detailed application by locale.
293
     *     The array key is the locale.
294
     * @throws GooglePlayException if the application is not exists or other HTTP error
295
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
296
     */
297
    public function getAppInLocales($requestApp, array $locales): array
298
    {
299
        $requestApp = $this->castToRequestApp($requestApp);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $requestApp. This often makes code more readable.
Loading history...
300
        $requests = [];
301
        foreach ($locales as $locale) {
302
            $requests[$locale] = new RequestApp($requestApp->getId(), $locale, $requestApp->getCountry());
303
        }
304
        return $this->getApps($requests);
305
    }
306
307
    /**
308
     * Returns detailed information about the application in all
309
     * available locales. HTTP requests are executed in parallel.
310
     *
311
     * @param string|RequestApp|App $requestApp Application id (package name)
312
     *     or object {@see RequestApp} or object {@see App}.
313
     * @return AppDetail[] An array with detailed information about the application on all available locales. The array
314
     *     key is the locale.
315
     * @throws GooglePlayException if the application is not exists or other HTTP error
316
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
317
     */
318
    public function getAppInAvailableLocales($requestApp): array
319
    {
320
        $list = $this->getAppInLocales($requestApp, LocaleHelper::SUPPORTED_LOCALES);
321
322
        $preferredLocale = $list[self::DEFAULT_LOCALE];
323
        foreach ($list as $app) {
324
            if ($app->getTranslatedFromLanguage() !== null) {
325
                $preferredLocale = $app->getTranslatedFromLanguage();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $preferredLocale is correct as $app->getTranslatedFromLanguage() (which targets Nelexa\GPlay\Model\AppDe...ranslatedFromLanguage()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
326
                break;
327
            }
328
        }
329
330
        $preferredApp = $list[$preferredLocale];
331
        $list = array_filter($list, static function (AppDetail $app, string $locale) use ($preferredApp, $list) {
332
            // deletes locales in which there is no translation added, but automatic translation by Google Translate is used.
333
            if ($preferredApp->getLocale() === $locale || !$preferredApp->equals($app)) {
334
                if (($pos = strpos($locale, '_')) !== false) {
335
                    $rootLang = substr($locale, 0, $pos);
336
                    $rootLangLocale = LocaleHelper::getNormalizeLocale($rootLang);
337
                    if (
338
                        $rootLangLocale !== $locale &&
339
                        isset($list[$rootLangLocale]) &&
340
                        $list[$rootLangLocale]->equals($app)
341
                    ) {
342
                        // delete duplicate data,
343
                        // for example, delete en_CA, en_IN, en_GB, en_ZA, if there is en_US and they are equals.
344
                        return false;
345
                    }
346
                }
347
                return true;
348
            }
349
            return false;
350
        }, ARRAY_FILTER_USE_BOTH);
351
352
        // sorting array keys; the first key is the preferred locale
353
        uksort(
354
            $list,
355
            static function (
356
                /** @noinspection PhpUnusedParameterInspection */
357
                string $a,
358
                string $b
359
            ) use ($preferredLocale) {
360
                return $b === $preferredLocale ? 1 : 0;
361
            }
362
        );
363
364
        return $list;
365
    }
366
367
    /**
368
     * Checks if the specified application exists in the Google Play Store.
369
     *
370
     * @param string|RequestApp|App $requestApp Application id (package name)
371
     *     or object {@see RequestApp} or object {@see App}.
372
     * @return bool true if the application exists on the Google Play store, and false if not.
373
     */
374
    public function existsApp($requestApp): bool
0 ignored issues
show
Coding Style introduced by
function existsApp() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
375
    {
376
        $requestApp = $this->castToRequestApp($requestApp);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $requestApp. This often makes code more readable.
Loading history...
377
378
        try {
379
            return (bool)$this->getHttpClient()->request(
380
                'HEAD',
381
                $requestApp->getFullUrl(),
382
                [
383
                    RequestOptions::HTTP_ERRORS => false,
384
                    HttpClient::OPTION_HANDLER_RESPONSE => new ExistsAppScraper(),
385
                ]
386
            );
387
        } catch (GuzzleException $e) {
388
            return false;
389
        }
390
    }
391
392
    /**
393
     * Checks if the specified applications exist in the Google Play store.
394
     * HTTP requests are executed in parallel.
395
     *
396
     * @param string[]|RequestApp[]|App[] $requestApps array of application ids or array of {@see RequestApp} or array
397
     *     of {@see App}.
398
     * @return AppDetail[] Массив подробной информации для каждого приложения.
399
     * Ключи возвращаемого массива соответствуют переданному массиву.
400
     * @return bool[] An array of information about the existence of each application in the store Google Play. The
401
     *     keys of the returned array matches to the passed array.
402
     * @throws GooglePlayException
403
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
404
     */
405
    public function existsApps(array $requestApps): array
406
    {
407
        if (empty($requestApps)) {
408
            return [];
409
        }
410
        $urls = $this->getRequestAppsUrlList($requestApps);
411
        try {
412
            return $this->getHttpClient()->requestAsyncPool(
413
                'HEAD',
414
                $urls,
0 ignored issues
show
Documentation introduced by
$urls is of type array, but the function expects a object<Nelexa\GPlay\Http\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
415
                [
416
                    RequestOptions::HTTP_ERRORS => false,
417
                    HttpClient::OPTION_HANDLER_RESPONSE => new ExistsAppScraper(),
418
                ],
419
                $this->concurrency
420
            );
421
        } catch (GuzzleException $e) {
422
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
423
        }
424
    }
425
426
    /**
427
     * Returns an array with application reviews.
428
     *
429
     * @param string|RequestApp|App $requestApp Application id (package name)
430
     *     or object {@see RequestApp} or object {@see App}.
431
     * @param int $limit Maximum count of reviews. To extract all reviews,
432
     *     specify -1. This can take a lot of time.
433
     * @param SortEnum|null $sort Sort reviews of the application.
434
     *     If null, then sort by the newest reviews.
435
     * @return Review[] App reviews
436
     * @throws GooglePlayException if the application is not exists or other HTTP error
437
     * @see SortEnum::NEWEST()       Sort by latest reviews.
438
     * @see SortEnum::HELPFULNESS()  Sort by helpful reviews.
439
     * @see SortEnum::RATING()       Sort by rating reviews.
440
     */
441
    public function getAppReviews($requestApp, int $limit, ?SortEnum $sort = null): array
442
    {
443
        $requestApp = $this->castToRequestApp($requestApp);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $requestApp. This often makes code more readable.
Loading history...
444
        $sort = $sort ?? SortEnum::NEWEST();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $sort. This often makes code more readable.
Loading history...
445
446
        $allCount = 0;
447
        $token = null;
448
        $allReviews = [];
449
450
        $cacheTtl = $sort === SortEnum::NEWEST() ? \DateInterval::createFromDateString('5 min') : \DateInterval::createFromDateString('1 hour');
451
452
        try {
453
            do {
454
                $count = $limit === -1 ?
455
                    PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE :
456
                    min(PlayStoreUiRequest::LIMIT_REVIEW_ON_PAGE, max($limit - $allCount, 1));
457
458
                $request = PlayStoreUiRequest::getReviewsRequest($requestApp, $count, $sort, $token);
459
460
                [$reviews, $token] = $this->getHttpClient()->send(
0 ignored issues
show
Bug introduced by
The variable $reviews does not exist. Did you mean $allReviews?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
461
                    $request,
462
                    [
463
                        HttpClient::OPTION_CACHE_TTL => $cacheTtl,
464
                        HttpClient::OPTION_HANDLER_RESPONSE => new ReviewsScraper($requestApp),
465
                    ]
466
                );
467
                $allCount += count($reviews);
0 ignored issues
show
Bug introduced by
The variable $reviews does not exist. Did you mean $allReviews?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
468
                $allReviews[] = $reviews;
0 ignored issues
show
Bug introduced by
The variable $reviews does not exist. Did you mean $allReviews?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
469
            } while ($token !== null && ($limit === -1 || $allCount < $limit));
470
        } catch (GuzzleException $e) {
471
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
472
        }
473
474
        return empty($allReviews) ? $allReviews : array_merge(...$allReviews);
475
    }
476
477
    /**
478
     * Returns a list of similar applications in the Google Play store.
479
     *
480
     * @param string|RequestApp|App $requestApp Application id (package name)
481
     *     or object {@see RequestApp} or object {@see App}.
482
     * @param int $limit limit of similar applications
483
     * @return App[] array of applications with basic information
484
     * @throws GooglePlayException if the application is not exists or other HTTP error
485
     */
486
    public function getSimilarApps($requestApp, int $limit = 50): array
487
    {
488
        $requestApp = $this->castToRequestApp($requestApp);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $requestApp. This often makes code more readable.
Loading history...
489
        $limit = max(1, min($limit, self::MAX_SEARCH_RESULTS));
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $limit. This often makes code more readable.
Loading history...
490
        try {
491
            /**
492
             * @var string|null $similarAppsUrl
493
             */
494
            $similarAppsUrl = $this->getHttpClient()->request(
495
                'GET',
496
                $requestApp->getFullUrl(),
497
                [
498
                    HttpClient::OPTION_HANDLER_RESPONSE => new FindSimilarAppsUrlScraper($requestApp),
499
                ]
500
            );
501
            if ($similarAppsUrl === null) {
502
                return [];
503
            }
504
            return $this->getAppsFromClusterPage(
505
                $similarAppsUrl,
506
                $requestApp->getLocale(),
507
                $requestApp->getCountry(),
508
                $limit
509
            );
510
        } catch (GuzzleException $e) {
511
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
512
        }
513
    }
514
515
    /**
516
     * @param string $clusterPageUrl
517
     * @param string $locale
518
     * @param string $country
519
     * @param int $limit
520
     * @return App[]
521
     * @throws GooglePlayException
522
     */
523
    private function getAppsFromClusterPage(
524
        string $clusterPageUrl,
525
        string $locale,
526
        string $country,
527
        int $limit
528
    ): array {
529
        try {
530
            [$apps, $token] = $this->getHttpClient()->request(
0 ignored issues
show
Bug introduced by
The variable $apps does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $token does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
531
                'GET',
532
                $clusterPageUrl,
533
                [
534
                    HttpClient::OPTION_HANDLER_RESPONSE => new ClusterAppsScraper(),
535
                ]
536
            );
537
538
            $allCount = count($apps);
539
            $allApps = [$apps];
540
541
            while ($token !== null && ($limit === -1 || $allCount < $limit)) {
542
                $count = $limit === -1 ?
543
                    PlayStoreUiRequest::LIMIT_APPS_ON_PAGE :
544
                    min(PlayStoreUiRequest::LIMIT_APPS_ON_PAGE, max($limit - $allCount, 1));
545
546
                $request = PlayStoreUiRequest::getAppsRequest($locale, $country, $count, $token);
547
548
                [$apps, $token] = $this->getHttpClient()->send(
549
                    $request,
550
                    [
551
                        HttpClient::OPTION_HANDLER_RESPONSE => new PlayStoreUiAppsScraper(),
552
                    ]
553
                );
554
                $allCount += count($apps);
555
                $allApps[] = $apps;
556
            }
557
        } catch (GuzzleException $e) {
558
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
559
        }
560
        return empty($allApps) ? $allApps : array_slice(array_merge(...$allApps), 0, $limit);
561
    }
562
563
    /**
564
     * Returns a list of permissions for the application.
565
     *
566
     * @param string|RequestApp|App $requestApp Application id (package name)
567
     *     or object {@see RequestApp} or object {@see App}.
568
     * @return Permission[] list of permissions for the application
569
     * @throws GooglePlayException
570
     */
571
    public function getPermissions($requestApp): array
572
    {
573
        $requestApp = $this->castToRequestApp($requestApp);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $requestApp. This often makes code more readable.
Loading history...
574
575
        $url = self::GOOGLE_PLAY_URL . '/store/xhr/getdoc?authuser=0';
576
        try {
577
            return $this->getHttpClient()->request(
578
                'POST',
579
                $url,
580
                [
581
                    RequestOptions::FORM_PARAMS => [
582
                        'ids' => $requestApp->getId(),
583
                        self::REQ_PARAM_LOCALE => $requestApp->getLocale(),
584
                        'xhr' => 1,
585
                    ],
586
                    HttpClient::OPTION_HANDLER_RESPONSE => new PermissionScraper(),
587
                ]
588
            );
589
        } catch (GuzzleException $e) {
590
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
591
        }
592
    }
593
594
    /**
595
     * Returns a list of application categories from the Google Play store.
596
     *
597
     * @param string|null $locale site locale or default locale used
598
     * @return Category[] list of application categories
599
     * @throws GooglePlayException caused by HTTP error
600
     */
601
    public function getCategories(?string $locale = null): array
602
    {
603
        $locale = LocaleHelper::getNormalizeLocale($locale ?? $this->defaultLocale);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locale. This often makes code more readable.
Loading history...
604
605
        $url = self::GOOGLE_PLAY_APPS_URL;
606
        try {
607
            return $this->getHttpClient()->request(
608
                'GET',
609
                $url,
610
                [
611
                    RequestOptions::QUERY => [
612
                        self::REQ_PARAM_LOCALE => $locale,
613
                    ],
614
                    HttpClient::OPTION_HANDLER_RESPONSE => new CategoriesScraper(),
615
                ]
616
            );
617
        } catch (GuzzleException $e) {
618
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
619
        }
620
    }
621
622
    /**
623
     * Returns a list of application categories from the Google Play store for the locale array.
624
     * HTTP requests are executed in parallel.
625
     *
626
     * @param string[] $locales array of locales
627
     * @return Category[][] list of application categories by locale
628
     * @throws GooglePlayException caused by HTTP error
629
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
630
     */
631
    public function getCategoriesInLocales(array $locales): array
632
    {
633
        if (empty($locales)) {
634
            return [];
635
        }
636
        $locales = LocaleHelper::getNormalizeLocales($locales);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locales. This often makes code more readable.
Loading history...
637
638
        $urls = [];
639
        $url = self::GOOGLE_PLAY_APPS_URL;
640
        foreach ($locales as $locale) {
641
            $urls[$locale] = $url . '?' . http_build_query([
642
                    self::REQ_PARAM_LOCALE => $locale,
643
                ]);
644
        }
645
646
        try {
647
            return $this->getHttpClient()->requestAsyncPool(
648
                'GET',
649
                $urls,
0 ignored issues
show
Documentation introduced by
$urls is of type array, but the function expects a object<Nelexa\GPlay\Http\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
650
                [
651
                    HttpClient::OPTION_HANDLER_RESPONSE => new CategoriesScraper(),
652
                ],
653
                $this->concurrency
654
            );
655
        } catch (GuzzleException $e) {
656
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
657
        }
658
    }
659
660
    /**
661
     * Returns a list of categories from the Google Play store for all available locales.
662
     *
663
     * @return Category[][] list of application categories by locale
664
     * @throws GooglePlayException caused by HTTP error
665
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
666
     */
667
    public function getCategoriesInAvailableLocales(): array
668
    {
669
        return $this->getCategoriesInLocales(LocaleHelper::SUPPORTED_LOCALES);
670
    }
671
672
    /**
673
     * Returns information about the developer: name, icon, cover, description and website address.
674
     *
675
     * @param string|int|Developer|App|AppDetail $developerId developer identifier or object containing it
676
     * @param string|null $locale site locale or default locale used
677
     * @return Developer information about the developer
678
     * @throws GooglePlayException caused by HTTP error
679
     */
680
    public function getDeveloperInfo($developerId, ?string $locale = null): Developer
681
    {
682
        $developerId = $this->castToDeveloperId($developerId);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $developerId. This often makes code more readable.
Loading history...
683
        if (!is_numeric($developerId)) {
684
            throw new GooglePlayException(sprintf('Developer "%s" does not have a personalized page on Google Play.', $developerId));
685
        }
686
687
        $locale = LocaleHelper::getNormalizeLocale($locale ?? $this->defaultLocale);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locale. This often makes code more readable.
Loading history...
688
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
689
        try {
690
            return $this->getHttpClient()->request(
691
                'GET',
692
                $url,
693
                [
694
                    RequestOptions::QUERY => [
695
                        self::REQ_PARAM_ID => $developerId,
696
                        self::REQ_PARAM_LOCALE => $locale,
697
                    ],
698
                    HttpClient::OPTION_HANDLER_RESPONSE => new DeveloperInfoScraper(),
699
                ]
700
            );
701
        } catch (GuzzleException $e) {
702
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
703
        }
704
    }
705
706
    /**
707
     * @param string|int|Developer|App|AppDetail $developerId
708
     * @return string
709
     */
710
    private function castToDeveloperId($developerId): string
711
    {
712
        if ($developerId instanceof App) {
713
            return $developerId->getDeveloper()->getId();
714
        }
715
        if ($developerId instanceof Developer) {
716
            return $developerId->getId();
717
        }
718
        if (is_int($developerId)) {
719
            return (string)$developerId;
720
        }
721
        return $developerId;
722
    }
723
724
    /**
725
     * Returns information about the developer for the locale array.
726
     *
727
     * @param string|int|Developer|App|AppDetail $developerId developer identifier or object containing it
728
     * @param string[] $locales array of locales
729
     * @return Developer[] list of developer by locale
730
     * @throws GooglePlayException caused by HTTP error
731
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
732
     */
733
    public function getDeveloperInfoInLocales($developerId, array $locales = []): array
734
    {
735
        if (empty($locales)) {
736
            return [];
737
        }
738
        $locales = LocaleHelper::getNormalizeLocales($locales);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locales. This often makes code more readable.
Loading history...
739
740
        $id = $this->castToDeveloperId($developerId);
741
        if (!is_numeric($id)) {
742
            throw new GooglePlayException(sprintf('Developer "%s" does not have a personalized page on Google Play.', $id));
743
        }
744
745
        $urls = [];
746
        $url = self::GOOGLE_PLAY_APPS_URL . '/dev';
747
        foreach ($locales as $locale) {
748
            $urls[$locale] = $url . '?' . http_build_query([
749
                    self::REQ_PARAM_ID => $id,
750
                    self::REQ_PARAM_LOCALE => $locale,
751
                ]);
752
        }
753
754
        try {
755
            return $this->getHttpClient()->requestAsyncPool(
756
                'GET',
757
                $urls,
0 ignored issues
show
Documentation introduced by
$urls is of type array, but the function expects a object<Nelexa\GPlay\Http\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
758
                [
759
                    HttpClient::OPTION_HANDLER_RESPONSE => new DeveloperInfoScraper(),
760
                ],
761
                $this->concurrency
762
            );
763
        } catch (GuzzleException $e) {
764
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
765
        }
766
    }
767
768
    /**
769
     * Returns information about the developer for all available locales.
770
     *
771
     * @param string|int|Developer|App|AppDetail $developerId developer identifier or object containing it
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $developerId a bit more specific; maybe use integer.
Loading history...
772
     * @return Developer[] list of developer by locale
773
     * @throws GooglePlayException caused by HTTP error
774
     * @see GPlayApps::setConcurrency() To set the limit of parallel requests
775
     */
776
    public function getDeveloperInfoInAvailableLocales(int $developerId): array
777
    {
778
        $list = $this->getDeveloperInfoInLocales($developerId, LocaleHelper::SUPPORTED_LOCALES);
779
780
        $preferredLocale = self::DEFAULT_LOCALE;
781
782
        $preferredInfo = $list[$preferredLocale];
783
        $list = array_filter($list, static function (Developer $info, string $locale) use ($preferredInfo, $preferredLocale) {
784
            return $locale === $preferredLocale || $preferredInfo->equals($info);
785
        }, ARRAY_FILTER_USE_BOTH);
786
787
        foreach ($list as $locale => $info) {
788
            if (($pos = strpos($locale, '_')) !== false) {
789
                $rootLang = substr($locale, 0, $pos);
790
                $rootLangLocale = LocaleHelper::getNormalizeLocale($rootLang);
791
                if (
792
                    $rootLangLocale !== $locale &&
793
                    isset($list[$rootLangLocale]) &&
794
                    $list[$rootLangLocale]->equals($info)
795
                ) {
796
                    // delete duplicate data,
797
                    // for example, delete en_CA, en_IN, en_GB, en_ZA, if there is en_US and they are equals.
798
                    unset($list[$locale]);
799
                }
800
            }
801
        }
802
803
        return $list;
804
    }
805
806
    /**
807
     * Returns a list of developer applications in the Google Play store.
808
     *
809
     * @param string|int|Developer|App|AppDetail $developerId developer identifier or object containing it
810
     * @param string|null $locale locale or default locale used
811
     * @param string|null $country country or default country used
812
     * @return App[] array of applications with basic information
813
     * @throws GooglePlayException caused by HTTP error
814
     */
815
    public function getDeveloperApps(
816
        $developerId,
817
        ?string $locale = null,
818
        ?string $country = null
819
    ): array {
820
        $locale = LocaleHelper::getNormalizeLocale($locale ?? $this->defaultLocale);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locale. This often makes code more readable.
Loading history...
821
        $country = $country ?? $this->defaultCountry;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $country. This often makes code more readable.
Loading history...
822
        $developerId = $this->castToDeveloperId($developerId);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $developerId. This often makes code more readable.
Loading history...
823
824
        $query = [
825
            self::REQ_PARAM_ID => $developerId,
826
            self::REQ_PARAM_LOCALE => $locale,
827
            self::REQ_PARAM_COUNTRY => $country,
828
        ];
829
830
        if (is_numeric($developerId)) {
831
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/dev?' . http_build_query($query);
832
            try {
833
                /**
834
                 * @var string|null $developerUrl
835
                 */
836
                $developerUrl = $this->getHttpClient()->request(
837
                    'GET',
838
                    $developerUrl,
839
                    [
840
                        HttpClient::OPTION_HANDLER_RESPONSE => new FindDevAppsUrlScraper(),
841
                    ]
842
                );
843
                if ($developerUrl === null) {
844
                    return [];
845
                }
846
                $developerUrl .= '&' . self::REQ_PARAM_LOCALE . '=' . urlencode($locale) .
847
                    '&' . self::REQ_PARAM_COUNTRY . '=' . urlencode($country);
848
            } catch (GuzzleException $e) {
849
                throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
850
            }
851
        } else {
852
            $developerUrl = self::GOOGLE_PLAY_APPS_URL . '/developer?' . http_build_query($query);
853
        }
854
855
        return $this->getAppsFromClusterPage(
856
            $developerUrl,
857
            $locale,
858
            $country,
859
            1000
860
        );
861
    }
862
863
    /**
864
     * Returns the Google Play search suggests.
865
     *
866
     * @param string $query search query
867
     * @param string|null $locale locale or default locale used
868
     * @param string|null $country country or default country used
869
     * @return string[] array with search suggest
870
     * @throws GooglePlayException caused by HTTP error
871
     */
872
    public function getSuggest(
873
        string $query,
874
        ?string $locale = null,
875
        ?string $country = null
876
    ): array {
877
        $query = trim($query);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $query. This often makes code more readable.
Loading history...
878
        if ($query === '') {
879
            return [];
880
        }
881
        $locale = LocaleHelper::getNormalizeLocale($locale ?? $this->defaultLocale);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locale. This often makes code more readable.
Loading history...
882
        $country = $country ?? $this->defaultCountry;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $country. This often makes code more readable.
Loading history...
883
884
        $url = 'https://market.android.com/suggest/SuggRequest';
885
        try {
886
            return $this->getHttpClient()->request(
887
                'GET',
888
                $url,
889
                [
890
                    RequestOptions::QUERY => [
891
                        'json' => 1,
892
                        'c' => 3,
893
                        'query' => $query,
894
                        self::REQ_PARAM_LOCALE => $locale,
895
                        self::REQ_PARAM_COUNTRY => $country,
896
                    ],
897
                    HttpClient:: OPTION_HANDLER_RESPONSE => new class implements ResponseHandlerInterface {
0 ignored issues
show
Coding Style introduced by
anonymous//src/GPlayApps.php$0 does not seem to conform to the naming convention (^[A-Z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
898
                        /**
899
                         * @param RequestInterface $request
900
                         * @param ResponseInterface $response
901
                         * @return mixed
902
                         */
903
                        public function __invoke(RequestInterface $request, ResponseInterface $response)
904
                        {
905
                            $json = \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
906
                            return array_map(static function (array $v) {
907
                                return $v['s'];
908
                            }, $json);
909
                        }
910
                    },
911
                ]
912
            );
913
        } catch (GuzzleException $e) {
914
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
915
        }
916
    }
917
918
    /**
919
     * Returns a list of applications from the Google Play store for a search query.
920
     *
921
     * @param string $query search query
922
     * @param int $limit limit
923
     * @param PriceEnum|null $price free, paid or both applications
924
     * @param string|null $locale locale or default locale used
925
     * @param string|null $country country or default country used
926
     * @return App[] array of applications with basic information
927
     * @throws GooglePlayException caused by HTTP error
928
     *
929
     * @see PriceEnum::ALL()
930
     * @see PriceEnum::FREE()
931
     * @see PriceEnum::PAID()
932
     */
933
    public function search(
934
        string $query,
935
        int $limit = 50,
936
        ?PriceEnum $price = null,
937
        ?string $locale = null,
938
        ?string $country = null
939
    ): array {
940
        $query = trim($query);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $query. This often makes code more readable.
Loading history...
941
        if (empty($query)) {
942
            throw new \InvalidArgumentException('Search query missing');
943
        }
944
        $limit = min(max($limit, 1), self::MAX_SEARCH_RESULTS);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $limit. This often makes code more readable.
Loading history...
945
        $locale = LocaleHelper::getNormalizeLocale($locale ?? $this->defaultLocale);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locale. This often makes code more readable.
Loading history...
946
        $country = $country ?? $this->defaultCountry;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $country. This often makes code more readable.
Loading history...
947
        $price = $price ?? PriceEnum::ALL();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $price. This often makes code more readable.
Loading history...
948
949
        $params = [
950
            'c' => 'apps',
951
            'q' => $query,
952
            'hl' => $locale,
953
            'gl' => $country,
954
            'price' => $price->value(),
955
        ];
956
        $clusterPageUrl = self::GOOGLE_PLAY_URL . '/store/search?' . http_build_query($params);
957
958
        return $this->getAppsFromClusterPage($clusterPageUrl, $locale, $country, $limit);
959
    }
960
961
    /**
962
     * Gets a list of apps from the Google Play store for category and collection.
963
     *
964
     * @param CategoryEnum|null $category application category or null
965
     * @param CollectionEnum $collection коллекция приложения
966
     * @param int $limit application limit
967
     * @param AgeEnum|null $age
968
     * @param string|null $locale locale or default locale used
969
     * @param string|null $country country or default country used
970
     * @return App[] array of applications with basic information
971
     * @throws GooglePlayException caused by HTTP error
972
     *
973
     * @see CategoryEnum
974
     *
975
     * @see CollectionEnum::TOP_FREE()  Top Free
976
     * @see CollectionEnum::TOP_PAID()  Top Paid
977
     * @see CollectionEnum::NEW_FREE()  Top New Free
978
     * @see CollectionEnum::NEW_PAID()  Top New Paid
979
     * @see CollectionEnum::GROSSING()  Top Grossing
980
     * @see CollectionEnum::TRENDING()  Trending Apps
981
     *
982
     * @see AgeEnum::FIVE_UNDER()       Ages 5 and under
983
     * @see AgeEnum::SIX_EIGHT()        Ages 6-8
984
     * @see AgeEnum::NINE_UP()          Ages 9 & Up
985
     */
986
    public function getAppsByCategory(
987
        ?CategoryEnum $category,
988
        CollectionEnum $collection,
989
        int $limit = 60,
990
        ?AgeEnum $age = null,
991
        ?string $locale = null,
992
        ?string $country = null
993
    ): array {
994
        $limit = min(560, max(1, $limit));
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $limit. This often makes code more readable.
Loading history...
995
        $locale = LocaleHelper::getNormalizeLocale($locale ?? $this->defaultLocale);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $locale. This often makes code more readable.
Loading history...
996
        $country = $country ?? $this->defaultCountry;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $country. This often makes code more readable.
Loading history...
997
998
        $url = self::GOOGLE_PLAY_APPS_URL . '';
999
        if ($category !== null) {
1000
            $url .= '/category/' . $category->value();
1001
        }
1002
        $url .= '/collection/' . $collection->value();
1003
1004
        $offset = 0;
1005
1006
        $queryParams = [
1007
            self::REQ_PARAM_LOCALE => $locale,
1008
            self::REQ_PARAM_COUNTRY => $country,
1009
        ];
1010
        if ($age !== null) {
1011
            $queryParams['age'] = $age->value();
1012
        }
1013
1014
        $results = [];
1015
        $countResults = 0;
1016
        $slice = 0;
1017
        try {
1018
            do {
1019
                if ($offset > 500) {
1020
                    $slice = $offset - 500;
1021
                    $offset = 500;
1022
                }
1023
                $queryParams['num'] = min($limit - $offset + $slice, 60);
1024
1025
                $result = $this->getHttpClient()->request(
1026
                    'POST',
1027
                    $url,
1028
                    [
1029
                        RequestOptions::QUERY => $queryParams,
1030
                        RequestOptions::FORM_PARAMS => [
1031
                            'start' => $offset,
1032
                        ],
1033
                        HttpClient::OPTION_HANDLER_RESPONSE => new CategoryAppsScraper(),
1034
                    ]
1035
                );
1036
                if ($slice > 0) {
1037
                    $result = array_slice($result, $slice);
1038
                }
1039
                $countResult = count($result);
1040
                $countResults += $countResult;
1041
                $results[] = $result;
1042
                $offset += $countResult;
1043
            } while ($countResult === 60 && $countResults < $limit);
1044
        } catch (GuzzleException $e) {
1045
            throw new GooglePlayException($e->getMessage(), $e->getCode(), $e);
0 ignored issues
show
Documentation introduced by
$e is of type object<GuzzleHttp\Exception\GuzzleException>, but the function expects a null|object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1046
        }
1047
        $results = array_merge(...$results);
1048
        $results = array_slice($results, 0, $limit);
1049
        return $results;
1050
    }
1051
1052
    /**
1053
     * Asynchronously saves images from googleusercontent.com and similar URLs to disk.
1054
     * Before use, you can set the parameters of the width-height of images.
1055
     *
1056
     * @param GoogleImage[] $images
1057
     * @param callable $destPathFn The function to which the GoogleImage object is passed and you must return
1058
     *     the full output. path to save this file. File extension can be omitted.
1059
     *     It will be automatically installed.
1060
     * @param bool $overwrite Overwrite files
1061
     * @return ImageInfo[]
1062
     */
1063
    public function saveGoogleImages(
1064
        array $images,
1065
        callable $destPathFn,
1066
        bool $overwrite = false
1067
    ): array {
1068
        $mapping = [];
1069
        foreach ($images as $image) {
1070
            if (!$image instanceof GoogleImage) {
1071
                throw new \InvalidArgumentException('An array of ' . GoogleImage::class . ' objects is expected.');
1072
            }
1073
            $destPath = $destPathFn($image);
1074
            $mapping[$destPath] = $image->getUrl();
1075
        }
1076
1077
        $httpClient = $this->getHttpClient();
1078
        $promises = (static function () use ($mapping, $overwrite, $httpClient) {
1079
            foreach ($mapping as $destPath => $url) {
1080
                if (!$overwrite && is_file($destPath)) {
1081
                    yield $destPath => new FulfilledPromise($url);
1082
                } else {
1083
                    $dir = dirname($destPath);
1084
                    if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
1085
                        throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir));
1086
                    }
1087
                    yield $destPath => $httpClient
1088
                        ->requestAsync('GET', $url, [
1089
                            RequestOptions::COOKIES => null,
1090
                            RequestOptions::SINK => $destPath,
1091
                            RequestOptions::HTTP_ERRORS => true,
1092
                        ])
1093
                        ->then(static function (
1094
                            /** @noinspection PhpUnusedParameterInspection */
1095
                            ResponseInterface $response
0 ignored issues
show
Unused Code introduced by
The parameter $response is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1096
                        ) use ($url) {
1097
                            return $url;
1098
                        });
1099
                }
1100
            }
1101
        })();
1102
1103
        /**
1104
         * @var ImageInfo[] $imageInfoList
1105
         */
1106
        $imageInfoList = [];
1107
        (new EachPromise($promises, [
1108
            'concurrency' => $this->concurrency,
1109
            'fulfilled' => static function (string $url, string $destPath) use (&$imageInfoList) {
1110
                $imageInfoList[] = new ImageInfo($url, $destPath);
1111
            },
1112
            'rejected' => static function (\Throwable $reason, string $key) use ($mapping) {
1113
                $exceptionUrl = $mapping[$key];
1114
                foreach ($mapping as $destPath => $url) {
1115
                    if (is_file($destPath)) {
1116
                        unlink($destPath);
1117
                    }
1118
                }
1119
                throw (new GooglePlayException($reason->getMessage(), $reason->getCode(), $reason))->setUrl($exceptionUrl);
1120
            },
1121
        ]))->promise()->wait();
1122
1123
        return $imageInfoList;
1124
    }
1125
}
1126