Passed
Push — master ( 641929...562012 )
by Alexey
06:04 queued 12s
created

HttpClient::setConcurrency()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

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

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