Test Failed
Branch feature/fix-old-client (2ee070)
by Alexey
11:31
created

HttpClient::getRequestPromise()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 35
rs 8.4444
cc 8
nc 4
nop 1
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
    public function __construct(?GuzzleClient $client = null, ?CacheInterface $cache = null)
45
    {
46
        if ($client === null) {
47
            $proxy = getenv('HTTP_PROXY');
48
49
            $defaultOptions = [
50
                RequestOptions::HEADERS => [
51
                    'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0',
52
                ],
53
            ];
54
55
            if ($proxy !== false) {
56
                $defaultOptions[RequestOptions::PROXY] = $proxy;
57
            }
58
59
            $stack = HandlerStack::create();
60
            $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...
61
                ?? '🌎 [{ts}] "{method} {url} HTTP/{version}" {code} "{phrase}" - "{req_headers}"'
62
                . ' - {res_header_Content-Length}';
63
            $stack->push(Middleware::log(new ConsoleLog(), new MessageFormatter($logTemplate)), 'logger');
64
            $stack->push(
65
                Middleware::retry(
66
                    static function (
67
                        int $retries,
68
                        RequestInterface $request,
69
                        ?ResponseInterface $response = null,
70
                        ?TransferException $exception = null
71
                    ) {
72
                        return $retries < 3 && (
73
                            $exception instanceof ConnectException
74
                                || ($response !== null && ($response->getStatusCode() === 429 || $response->getStatusCode(
75
                                        ) >= 500))
76
                        );
77
                    },
78
                    static function (int $retries) {
79
                        return 2 ** $retries * 1000;
80
                    }
81
                ),
82
                'retry'
83
            );
84
            $defaultOptions['handler'] = $stack;
85
86
            $client = new GuzzleClient($defaultOptions);
87
        }
88
89
        $this->client = $client;
90
        $this->cache = $cache;
91
    }
92
93
    /**
94
     * @return \Psr\SimpleCache\CacheInterface|null
95
     */
96
    public function getCache(): ?CacheInterface
97
    {
98
        return $this->cache;
99
    }
100
101
    /**
102
     * @return \GuzzleHttp\Client
103
     */
104
    public function getClient(): GuzzleClient
105
    {
106
        return $this->client;
107
    }
108
109
    /**
110
     * @param \Nelexa\GPlay\HttpClient\Request $request
111
     * @param \Closure|null                    $onRejected
112
     *
113
     * @return mixed
114
     */
115
    public function request(Request $request, ?\Closure $onRejected = null)
116
    {
117
        $promise = $this->getRequestPromise($request);
118
        $promise->otherwise(
119
            $onRejected ?? static function (\Throwable $throwable) {
120
                return $throwable;
121
            }
122
        );
123
124
        return $promise->wait();
125
    }
126
127
    /**
128
     * @param \Nelexa\GPlay\HttpClient\Request $request
129
     *
130
     * @return \GuzzleHttp\Promise\PromiseInterface
131
     *
132
     * @internal
133
     */
134
    public function getRequestPromise(Request $request): PromiseInterface
135
    {
136
        $options = array_merge($this->options, $request->getOptions());
137
        $cacheKey = null;
138
139
        if (
140
            $this->cache !== null
141
            && !\array_key_exists('no_cache', $options)
142
            && \array_key_exists('cache_ttl', $options)
143
        ) {
144
            $cacheKey = $options['cache_key'] ?? sprintf(
145
                'http_client_gplay.v1.%s.%s',
146
                HashUtil::hashCallable($request->getParseHandler()),
147
                HashUtil::getRequestHash($request->getPsrRequest())
148
            );
149
            try {
150
                $cachedValue = $this->cache->get($cacheKey);
151
            } catch (InvalidArgumentException $e) {
152
                throw new \RuntimeException('Error fetch cache');
153
            }
154
155
            if ($cachedValue !== null) {
156
                return new FulfilledPromise($cachedValue);
157
            }
158
        }
159
160
        return $this->client
161
            ->sendAsync($request->getPsrRequest(), $request->getOptions())
162
            ->then(function (ResponseInterface $response) use ($request, $cacheKey, $options) {
163
                $parseResult = $request->getParseHandler()($request->getPsrRequest(), $response, $options);
164
                if ($cacheKey !== null && $parseResult !== null) {
165
                    $this->cache->set($cacheKey, $parseResult, $options['cache_ttl']);
0 ignored issues
show
Bug introduced by
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

165
                    $this->cache->/** @scrutinizer ignore-call */ 
166
                                  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...
166
                }
167
168
                return $parseResult;
169
            })
170
        ;
171
    }
172
173
    /**
174
     * @param array<Request> $requests
175
     * @param \Closure|null  $onRejected
176
     *
177
     * @return array
178
     */
179
    public function requestPool(array $requests, ?\Closure $onRejected = null): array
180
    {
181
        $makeRequests = function () use ($requests): \Generator {
182
            foreach ($requests as $key => $request) {
183
                yield $key => function () use ($request): PromiseInterface {
184
                    return $this->getRequestPromise($request);
185
                };
186
            }
187
        };
188
189
        $results = [];
190
        $pool = new Pool($this->client, $makeRequests(), [
191
            '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...
192
            'fulfilled' => static function ($result, $key) use (&$results): void {
193
                $results[$key] = $result;
194
            },
195
            'rejected' => $onRejected ?? static function (\Throwable $throwable, $key): void {
0 ignored issues
show
Unused Code introduced by
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

195
            '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...
196
                throw $throwable;
197
            },
198
        ]);
199
200
        $pool->promise()->wait();
201
202
        return $results;
203
    }
204
205
    /**
206
     * @param \Psr\SimpleCache\CacheInterface|null $cache
207
     *
208
     * @return HttpClient
209
     */
210
    public function setCache(?CacheInterface $cache): self
211
    {
212
        $this->cache = $cache;
213
214
        return $this;
215
    }
216
217
    /**
218
     * @param \GuzzleHttp\Client $client
219
     *
220
     * @return HttpClient
221
     */
222
    public function setClient(GuzzleClient $client): self
223
    {
224
        $this->client = $client;
225
226
        return $this;
227
    }
228
229
    public function setOption(string $key, $value): self
230
    {
231
        $this->options[$key] = $value;
232
233
        return $this;
234
    }
235
236
    public function setConcurrency(int $concurrency): self
237
    {
238
        $this->options['concurrency'] = max(1, $concurrency);
239
240
        return $this;
241
    }
242
243
    public function getConcurrency(): int
244
    {
245
        return $this->options['concurrency'] ?? self::DEFAULT_CONCURRENCY;
246
    }
247
248
    public function setConnectTimeout(float $connectTimeout): self
249
    {
250
        $this->options[RequestOptions::CONNECT_TIMEOUT] = max(0, $connectTimeout);
251
252
        return $this;
253
    }
254
255
    public function setTimeout(float $timeout): self
256
    {
257
        $this->options[RequestOptions::TIMEOUT] = max(0, $timeout);
258
259
        return $this;
260
    }
261
}
262