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\HttpClient; |
||||||
15 | |||||||
16 | use GuzzleHttp\Client as GuzzleClient; |
||||||
17 | use GuzzleHttp\Exception\ConnectException; |
||||||
18 | use GuzzleHttp\Exception\TransferException; |
||||||
19 | use GuzzleHttp\HandlerStack; |
||||||
20 | use GuzzleHttp\MessageFormatter; |
||||||
21 | use GuzzleHttp\Middleware; |
||||||
22 | use GuzzleHttp\Pool; |
||||||
23 | use GuzzleHttp\Promise\FulfilledPromise; |
||||||
24 | use GuzzleHttp\Promise\PromiseInterface; |
||||||
25 | use GuzzleHttp\RequestOptions; |
||||||
26 | use Psr\Http\Message\RequestInterface; |
||||||
27 | use Psr\Http\Message\ResponseInterface; |
||||||
28 | use Psr\SimpleCache\CacheInterface; |
||||||
29 | use Psr\SimpleCache\InvalidArgumentException; |
||||||
30 | |||||||
31 | class HttpClient |
||||||
32 | { |
||||||
33 | public const DEFAULT_CONCURRENCY = 4; |
||||||
34 | |||||||
35 | /** @var \Psr\SimpleCache\CacheInterface|null */ |
||||||
36 | private $cache; |
||||||
37 | |||||||
38 | /** @var \GuzzleHttp\Client */ |
||||||
39 | private $client; |
||||||
40 | |||||||
41 | /** @var array */ |
||||||
42 | private $options = []; |
||||||
43 | |||||||
44 | 51 | public function __construct(?GuzzleClient $client = null, ?CacheInterface $cache = null) |
|||||
45 | { |
||||||
46 | 2 | if ($client === null) { |
|||||
47 | 2 | $proxy = getenv('HTTP_PROXY'); |
|||||
48 | |||||||
49 | $defaultOptions = [ |
||||||
50 | RequestOptions::HEADERS => [ |
||||||
51 | 2 | 'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0', |
|||||
52 | ], |
||||||
53 | ]; |
||||||
54 | |||||||
55 | 2 | if ($proxy !== false) { |
|||||
56 | $defaultOptions[RequestOptions::PROXY] = $proxy; |
||||||
57 | } |
||||||
58 | |||||||
59 | 2 | $stack = HandlerStack::create(); |
|||||
60 | 2 | if (\PHP_SAPI === 'cli') { |
|||||
61 | 2 | $logTemplate = $config['logTemplate'] |
|||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||||||
62 | ?? '🌎 [{ts}] "{method} {url} HTTP/{version}" {code} "{phrase}" - {res_header_Content-Length}'; |
||||||
63 | 2 | $stack->push(Middleware::log(new ConsoleLog(), new MessageFormatter($logTemplate)), 'logger'); |
|||||
64 | } |
||||||
65 | 2 | $stack->push( |
|||||
66 | 2 | Middleware::retry( |
|||||
67 | 2 | static function ( |
|||||
68 | int $retries, |
||||||
69 | RequestInterface $request, |
||||||
70 | ?ResponseInterface $response = null, |
||||||
71 | ?TransferException $exception = null |
||||||
72 | ) { |
||||||
73 | 51 | return $retries < 3 && ( |
|||||
74 | 51 | $exception instanceof ConnectException |
|||||
75 | || ( |
||||||
76 | 51 | $response !== null |
|||||
77 | 51 | && \in_array($response->getStatusCode(), [408, 429, 500, 502, 503, 522], true) |
|||||
78 | ) |
||||||
79 | ); |
||||||
80 | }, |
||||||
81 | 2 | static function (int $retries) { |
|||||
82 | return 2 ** $retries * 1000; |
||||||
83 | } |
||||||
84 | ), |
||||||
85 | 'retry' |
||||||
86 | ); |
||||||
87 | 2 | $defaultOptions['handler'] = $stack; |
|||||
88 | |||||||
89 | 2 | $client = new GuzzleClient($defaultOptions); |
|||||
90 | } |
||||||
91 | |||||||
92 | 2 | $this->client = $client; |
|||||
93 | 2 | $this->cache = $cache; |
|||||
94 | } |
||||||
95 | |||||||
96 | /** |
||||||
97 | * @return \Psr\SimpleCache\CacheInterface|null |
||||||
98 | */ |
||||||
99 | public function getCache(): ?CacheInterface |
||||||
100 | { |
||||||
101 | return $this->cache; |
||||||
102 | } |
||||||
103 | |||||||
104 | /** |
||||||
105 | * @return \GuzzleHttp\Client |
||||||
106 | */ |
||||||
107 | 3 | public function getClient(): GuzzleClient |
|||||
108 | { |
||||||
109 | 3 | return $this->client; |
|||||
110 | } |
||||||
111 | |||||||
112 | /** |
||||||
113 | * @param \Nelexa\GPlay\HttpClient\Request $request |
||||||
114 | * @param \Closure|null $onRejected |
||||||
115 | * |
||||||
116 | * @return mixed |
||||||
117 | */ |
||||||
118 | 36 | public function request(Request $request, ?\Closure $onRejected = null) |
|||||
119 | { |
||||||
120 | 36 | $promise = $this->getRequestPromise($request); |
|||||
121 | 36 | $promise->otherwise( |
|||||
122 | 36 | $onRejected ?? static function (\Throwable $throwable) { |
|||||
123 | 1 | return $throwable; |
|||||
124 | } |
||||||
125 | ); |
||||||
126 | |||||||
127 | 36 | return $promise->wait(); |
|||||
128 | } |
||||||
129 | |||||||
130 | /** |
||||||
131 | * @param \Nelexa\GPlay\HttpClient\Request $request |
||||||
132 | * |
||||||
133 | * @return \GuzzleHttp\Promise\PromiseInterface |
||||||
134 | * |
||||||
135 | * @internal |
||||||
136 | */ |
||||||
137 | 49 | public function getRequestPromise(Request $request): PromiseInterface |
|||||
138 | { |
||||||
139 | 49 | $options = array_merge($this->options, $request->getOptions()); |
|||||
140 | 49 | $cacheKey = null; |
|||||
141 | |||||||
142 | if ( |
||||||
143 | 49 | $this->cache !== null |
|||||
144 | 49 | && !\array_key_exists('no_cache', $options) |
|||||
145 | 49 | && \array_key_exists('cache_ttl', $options) |
|||||
146 | ) { |
||||||
147 | $cacheKey = $options['cache_key'] ?? sprintf( |
||||||
148 | 'http_client_gplay.v1.%s.%s', |
||||||
149 | HashUtil::hashCallable($request->getParseHandler()), |
||||||
150 | HashUtil::getRequestHash($request->getPsrRequest()) |
||||||
151 | ); |
||||||
152 | try { |
||||||
153 | $cachedValue = $this->cache->get($cacheKey); |
||||||
154 | } catch (InvalidArgumentException $e) { |
||||||
155 | throw new \RuntimeException('Error fetch cache'); |
||||||
156 | } |
||||||
157 | |||||||
158 | if ($cachedValue !== null) { |
||||||
159 | return new FulfilledPromise($cachedValue); |
||||||
160 | } |
||||||
161 | } |
||||||
162 | |||||||
163 | 49 | return $this->client |
|||||
164 | 49 | ->sendAsync($request->getPsrRequest(), $request->getOptions()) |
|||||
165 | 49 | ->then(function (ResponseInterface $response) use ($request, $cacheKey, $options) { |
|||||
166 | 47 | $parseResult = $request->getParseHandler()($request->getPsrRequest(), $response, $options); |
|||||
167 | 47 | if ($cacheKey !== null && $parseResult !== null) { |
|||||
168 | $this->cache->set($cacheKey, $parseResult, $options['cache_ttl']); |
||||||
0 ignored issues
–
show
The method
set() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
169 | } |
||||||
170 | |||||||
171 | 47 | return $parseResult; |
|||||
172 | }) |
||||||
173 | ; |
||||||
174 | } |
||||||
175 | |||||||
176 | /** |
||||||
177 | * @param array<Request> $requests |
||||||
178 | * @param \Closure|null $onRejected |
||||||
179 | * |
||||||
180 | * @return array |
||||||
181 | */ |
||||||
182 | 13 | public function requestPool(array $requests, ?\Closure $onRejected = null): array |
|||||
183 | { |
||||||
184 | 13 | $makeRequests = function () use ($requests): \Generator { |
|||||
185 | 13 | foreach ($requests as $key => $request) { |
|||||
186 | 13 | yield $key => function () use ($request): PromiseInterface { |
|||||
187 | 13 | return $this->getRequestPromise($request); |
|||||
188 | }; |
||||||
189 | } |
||||||
190 | }; |
||||||
191 | |||||||
192 | 13 | $results = []; |
|||||
193 | 13 | $pool = new Pool($this->client, $makeRequests(), [ |
|||||
194 | 13 | 'concurrency' => $options['concurrency'] ?? self::DEFAULT_CONCURRENCY, |
|||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||||
195 | 13 | 'fulfilled' => static function ($result, $key) use (&$results): void { |
|||||
196 | 12 | $results[$key] = $result; |
|||||
197 | }, |
||||||
198 | 13 | 'rejected' => $onRejected ?? static function (\Throwable $throwable, $key): void { |
|||||
0 ignored issues
–
show
The parameter
$key is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||||
199 | 2 | throw $throwable; |
|||||
200 | }, |
||||||
201 | ]); |
||||||
202 | |||||||
203 | 13 | $pool->promise()->wait(); |
|||||
204 | |||||||
205 | 11 | return $results; |
|||||
206 | } |
||||||
207 | |||||||
208 | /** |
||||||
209 | * @param \Psr\SimpleCache\CacheInterface|null $cache |
||||||
210 | * |
||||||
211 | * @return HttpClient |
||||||
212 | */ |
||||||
213 | public function setCache(?CacheInterface $cache): self |
||||||
214 | { |
||||||
215 | $this->cache = $cache; |
||||||
216 | |||||||
217 | return $this; |
||||||
218 | } |
||||||
219 | |||||||
220 | /** |
||||||
221 | * @param \GuzzleHttp\Client $client |
||||||
222 | * |
||||||
223 | * @return HttpClient |
||||||
224 | */ |
||||||
225 | public function setClient(GuzzleClient $client): self |
||||||
226 | { |
||||||
227 | $this->client = $client; |
||||||
228 | |||||||
229 | return $this; |
||||||
230 | } |
||||||
231 | |||||||
232 | public function setOption(string $key, $value): self |
||||||
233 | { |
||||||
234 | $this->options[$key] = $value; |
||||||
235 | |||||||
236 | return $this; |
||||||
237 | } |
||||||
238 | |||||||
239 | 3 | public function setConcurrency(int $concurrency): self |
|||||
240 | { |
||||||
241 | 3 | $this->options['concurrency'] = max(1, $concurrency); |
|||||
242 | |||||||
243 | 3 | return $this; |
|||||
244 | } |
||||||
245 | |||||||
246 | 2 | public function getConcurrency(): int |
|||||
247 | { |
||||||
248 | 2 | return $this->options['concurrency'] ?? self::DEFAULT_CONCURRENCY; |
|||||
249 | } |
||||||
250 | |||||||
251 | public function setConnectTimeout(float $connectTimeout): self |
||||||
252 | { |
||||||
253 | $this->options[RequestOptions::CONNECT_TIMEOUT] = max(0, $connectTimeout); |
||||||
254 | |||||||
255 | return $this; |
||||||
256 | } |
||||||
257 | |||||||
258 | public function setTimeout(float $timeout): self |
||||||
259 | { |
||||||
260 | $this->options[RequestOptions::TIMEOUT] = max(0, $timeout); |
||||||
261 | |||||||
262 | return $this; |
||||||
263 | } |
||||||
264 | } |
||||||
265 |