Passed
Push — master ( 80ad96...45165d )
by Alexey
03:00
created

HttpClient::setProxy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
/** @noinspection PhpDocRedundantThrowsInspection */
3
declare(strict_types=1);
4
5
namespace Nelexa\GPlay\Http;
6
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Cookie\CookieJar;
9
use GuzzleHttp\Exception\ConnectException;
10
use GuzzleHttp\Exception\GuzzleException;
11
use GuzzleHttp\Exception\RequestException;
12
use GuzzleHttp\HandlerStack;
13
use GuzzleHttp\Middleware;
14
use GuzzleHttp\RequestOptions;
15
use Psr\Http\Message\RequestInterface;
16
use Psr\Http\Message\ResponseInterface;
17
use Psr\SimpleCache\CacheInterface;
18
use function GuzzleHttp\Promise\each_limit_all;
19
20
class HttpClient extends Client
21
{
22
    public const OPTION_HANDLER_RESPONSE = 'handler_response';
23
    public const OPTION_CACHE_TTL = 'cache_ttl';
24
    public const OPTION_NO_CACHE = 'no_cache';
25
    public const OPTION_CACHE_KEY = 'cache_key';
26
27
    /**
28
     * @internal
29
     */
30
    private const CACHE_KEY = 'gplay.v1.%s.%s';
31
32
    /**
33
     * Number of attempts with HTTP error (except 404)
34
     *
35
     * @var int
36
     */
37
    private $retryLimit;
38
    /**
39
     * @var CacheInterface|null
40
     */
41
    private $cache;
42
43
    /**
44
     * HttpClient constructor.
45
     *
46
     * @param array $config
47
     * @param int $retryLimit
48
     * @param CacheInterface|null $cache
49
     */
50
    public function __construct(array $config = [], int $retryLimit = 4, ?CacheInterface $cache = null)
51
    {
52
        $this->setRetryLimit($retryLimit);
53
        $this->setCache($cache);
54
55
        $handlerStack = HandlerStack::create();
56
        $handlerStack->unshift(
57
            function (callable $handler) {
58
                return function (RequestInterface $request, array $options) use ($handler) {
59
                    if (!isset($options[self::OPTION_HANDLER_RESPONSE])) {
60
                        return $handler($request, $options);
61
                    }
62
63
                    if (!$options[self::OPTION_HANDLER_RESPONSE] instanceof ResponseHandlerInterface) {
64
                        throw new \RuntimeException("'" . self::OPTION_HANDLER_RESPONSE . "' option is not implements " . ResponseHandlerInterface::class);
65
                    }
66
67
                    if ($this->cache !== null) {
68
                        if (!isset($options[self::OPTION_CACHE_KEY])) {
69
                            $func = $options[self::OPTION_HANDLER_RESPONSE];
70
                            $ref = new \ReflectionClass($func);
71
                            if ($ref->isAnonymous()) {
72
                                static $hashes;
73
74
                                if ($hashes === null) {
75
                                    $hashes = new \SplObjectStorage();
76
                                }
77
                                if (!isset($hashes[$func])) {
78
                                    try {
79
                                        $file = new \SplFileObject($ref->getFileName());
80
                                        $file->seek($ref->getStartLine() - 1);
81
                                        $content = '';
82
                                        while ($file->key() < $ref->getEndLine()) {
83
                                            $content .= $file->current();
84
                                            $file->next();
85
                                        }
86
                                        $hashes[$func] = $content;
87
                                    } catch (\ReflectionException $e) {
88
                                        throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
89
                                    }
90
                                }
91
                                $handlerHash = (string)$hashes[$func];
92
                            } else {
93
                                $handlerHash = $ref->getName();
94
                            }
95
96
                            $options[self::OPTION_CACHE_KEY] = sprintf(
97
                                self::CACHE_KEY,
98
                                hash('crc32b', $handlerHash),
99
                                self::getRequestHash($request)
100
                            );
101
                        }
102
103
                        $value = $this->cache->get($options[self::OPTION_CACHE_KEY]);
104
                        if ($value !== null) {
105
                            return $value;
106
                        }
107
                    }
108
109
                    return $handler($request, $options)
110
                        ->then(
111
                            function (ResponseInterface $response) use ($options, $request) {
112
                                $result = call_user_func(
113
                                    $options[self::OPTION_HANDLER_RESPONSE],
114
                                    $request,
115
                                    $response
116
                                );
117
                                if ($this->cache !== null && $result !== null) {
118
                                    $ttl = $options[self::OPTION_CACHE_TTL] ?? \DateInterval::createFromDateString('1 hour');
119
                                    $noCache = $options[self::OPTION_NO_CACHE] ?? false;
120
                                    if (!$noCache) {
121
                                        $this->cache->set(
122
                                            $options[self::OPTION_CACHE_KEY],
123
                                            $result,
124
                                            $ttl
125
                                        );
126
                                    }
127
                                }
128
                                return $result;
129
                            }
130
                        );
131
                };
132
            }
133
        );
134
        $handlerStack->push(
135
            Middleware::retry(
136
                function (
137
                    $retries,
138
                    /** @noinspection PhpUnusedParameterInspection */
139
                    RequestInterface $request,
140
                    ResponseInterface $response = null,
141
                    RequestException $exception = null
142
                ) {
143
                    // retry decider
144
                    if ($retries >= $this->retryLimit) {
145
                        return false;
146
                    }
147
148
                    // Retry connection exceptions
149
                    if ($exception instanceof ConnectException) {
150
                        return true;
151
                    }
152
153
                    if (
154
                        $response !== null && (
155
                            $response->getStatusCode() !== 404 &&
156
                            $response->getStatusCode() >= 400
157
                        )
158
                    ) {
159
                        return true;
160
                    }
161
                    return false;
162
                },
163
                static function (int $numberOfRetries) {
164
                    // retry delay
165
                    return 1000 * $numberOfRetries;
166
                }
167
            )
168
        );
169
170
        $config = array_replace_recursive($config, [
171
            'handler' => $handlerStack,
172
            RequestOptions::TIMEOUT => 10.0,
173
            RequestOptions::COOKIES => new CookieJar(),
174
            RequestOptions::HEADERS => [
175
                'Accept-Encoding' => 'gzip',
176
                'Accept-Language' => 'en',
177
                'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0',
178
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
179
                'Connection' => 'keep-alive',
180
            ],
181
        ]);
182
        parent::__construct($config);
183
    }
184
185
    /**
186
     * @param CacheInterface|null $cache
187
     * @return self
188
     */
189
    public function setCache(?CacheInterface $cache): self
190
    {
191
        $this->cache = $cache;
192
        return $this;
193
    }
194
195
    /**
196
     * @param string|null $proxy
197
     * @return self
198
     */
199
    public function setProxy(?string $proxy): self
200
    {
201
        $config = $this->getConfig();
202
        $config[RequestOptions::PROXY] = $proxy;
203
        $this->setConfig($config);
204
        return $this;
205
    }
206
207
    /**
208
     * @param int $retryLimit
209
     * @return self
210
     */
211
    public function setRetryLimit(int $retryLimit): self
212
    {
213
        $this->retryLimit = max(0, $retryLimit);
214
        return $this;
215
    }
216
217
    /**
218
     * @param string $key
219
     * @param string $value
220
     * @return HttpClient
221
     */
222
    public function setHttpHeader(string $key, ?string $value): self
223
    {
224
        $config = $this->getConfig();
225
        if ($value === null) {
226
            if (isset($config[RequestOptions::HEADERS][$key])) {
227
                unset($config[RequestOptions::HEADERS][$key]);
228
                $this->setConfig($config);
229
            }
230
        } else {
231
            $config[RequestOptions::HEADERS][$key] = $value;
232
            $this->setConfig($config);
233
        }
234
        return $this;
235
    }
236
237
    /**
238
     * @param \DateInterval|int|null $ttl
239
     * @return HttpClient
240
     */
241
    public function setCacheTtl($ttl): self
242
    {
243
        if ($ttl !== null && !is_int($ttl) && !$ttl instanceof \DateInterval) {
0 ignored issues
show
introduced by
$ttl is always a sub-type of DateInterval.
Loading history...
244
            throw new \InvalidArgumentException('Invalid cache ttl value. Supported \DateInterval, int and null.');
245
        }
246
        $config = $this->getConfig();
247
        $config[self::OPTION_CACHE_TTL] = $ttl;
248
        $this->setConfig($config);
249
        return $this;
250
    }
251
252
    /**
253
     * @param array $config
254
     */
255
    protected function mergeConfig(array $config): void
256
    {
257
        if (!empty($config)) {
258
            $this->setConfig(
259
                array_replace_recursive(
260
                    $this->getConfig(),
261
                    $config
262
                )
263
            );
264
        }
265
    }
266
267
    /**
268
     * @param array $config
269
     */
270
    protected function setConfig(array $config): void
271
    {
272
        static $property;
273
        try {
274
            if ($property === null) {
275
                $property = new \ReflectionProperty(parent::class, 'config');
276
                $property->setAccessible(true);
277
            }
278
            $property->setValue($this, $config);
279
        } catch (\ReflectionException $e) {
280
            throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
281
        }
282
    }
283
284
    /**
285
     * @param RequestInterface $request
286
     * @return string
287
     */
288
    private static function getRequestHash(RequestInterface $request): string
289
    {
290
        $data = [
291
            $request->getMethod(),
292
            (string)$request->getUri(),
293
            $request->getBody()->getContents(),
294
        ];
295
        foreach ($request->getHeaders() as $name => $header) {
296
            $data[] = $name . ': ' . implode(', ', $header);
297
        }
298
        $data[] = $request->getBody()->getContents();
299
        return hash('crc32b', implode("\n", $data));
300
    }
301
302
    /**
303
     * @param string $method
304
     * @param iterable $urls
305
     * @param array $options
306
     * @param int $concurrency
307
     * @return array
308
     * @throws GuzzleException
309
     */
310
    public function requestAsyncPool(string $method, iterable $urls, array $options = [], int $concurrency = 4): array
311
    {
312
        $results = [];
313
        if (!$urls instanceof \Generator) {
314
            $urls = $this->requestGenerator($method, $urls, $options);
315
        }
316
        each_limit_all($urls, $concurrency, static function ($response, $index) use (&$results) {
317
            $results[$index] = $response;
318
        })->wait();
319
        return $results;
320
    }
321
322
    /**
323
     * @param string $method
324
     * @param iterable $urls
325
     * @param array $options
326
     * @return \Generator
327
     */
328
    private function requestGenerator(string $method, iterable $urls, array $options): \Generator
329
    {
330
        foreach ($urls as $key => $url) {
331
            yield $key => $this->requestAsync($method, $url, $options);
332
        }
333
    }
334
}
335