Issues (45)

src/HttpClient/HttpClient.php (4 issues)

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
The variable $config seems to never exist and therefore isset should always be false.
Loading history...
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 ignore-call  annotation

168
                    $this->cache->/** @scrutinizer ignore-call */ 
169
                                  set($cacheKey, $parseResult, $options['cache_ttl']);

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.

Loading history...
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
The variable $options seems to never exist and therefore isset should always be false.
Loading history...
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 ignore-unused  annotation

198
            'rejected' => $onRejected ?? static function (\Throwable $throwable, /** @scrutinizer ignore-unused */ $key): void {

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

Loading history...
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