Passed
Push — master ( 81593c...33129c )
by Alexey
10:12 queued 12s
created

HttpClient::__construct()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6.027

Importance

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

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

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