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
![]() |
|||||
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
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
![]() |
|||||
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
$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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
$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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
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
![]() |
|||||
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
|
|||||
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 |