Passed
Push — master ( d53d6f...24ad68 )
by Tomáš
09:09
created

Requester::getContents()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 2
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Inspirum\Balikobot\Services;
6
7
use GuzzleHttp\Psr7\InflateStream;
8
use GuzzleHttp\Psr7\Response;
9
use Inspirum\Balikobot\Contracts\RequesterInterface;
10
use Inspirum\Balikobot\Definitions\API;
11
use Inspirum\Balikobot\Exceptions\BadRequestException;
12
use JsonException;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\StreamInterface;
15
use RuntimeException;
16
use Throwable;
17
use function base64_encode;
18
use function count;
19
use function curl_close;
20
use function curl_errno;
21
use function curl_error;
22
use function curl_exec;
23
use function curl_getinfo;
24
use function curl_init;
25
use function curl_setopt;
26
use function json_decode;
27
use function json_encode;
28
use function str_replace;
29
use function trim;
30
use const CURLINFO_HTTP_CODE;
31
use const CURLOPT_HEADER;
32
use const CURLOPT_HTTPHEADER;
33
use const CURLOPT_POST;
34
use const CURLOPT_POSTFIELDS;
35
use const CURLOPT_RETURNTRANSFER;
36
use const CURLOPT_SSL_VERIFYHOST;
37
use const CURLOPT_SSL_VERIFYPEER;
38
use const CURLOPT_URL;
39
use const JSON_THROW_ON_ERROR;
40
41
class Requester implements RequesterInterface
42
{
43
    /**
44
     * API User
45
     *
46
     * @var string
47
     */
48
    private string $apiUser;
49
50
    /**
51
     * API key
52
     *
53
     * @var string
54
     */
55
    private string $apiKey;
56
57
    /**
58
     * SSL verification enabled
59
     *
60
     * @var bool
61
     */
62
    private bool $sslVerify;
63
64
    /**
65
     * Response validator
66
     *
67
     * @var \Inspirum\Balikobot\Services\Validator
68
     */
69
    private Validator $validator;
70
71
    /**
72
     * Balikobot API client
73
     *
74
     * @param string $apiUser
75
     * @param string $apiKey
76
     * @param bool   $sslVerify
77
     */
78 369
    public function __construct(string $apiUser, string $apiKey, bool $sslVerify = true)
79
    {
80 369
        $this->apiUser   = $apiUser;
81 369
        $this->apiKey    = $apiKey;
82 369
        $this->sslVerify = $sslVerify;
83
84 369
        $this->validator = new Validator();
85 369
    }
86
87
    /**
88
     * Call API
89
     *
90
     * @param string             $version
91
     * @param string             $request
92
     * @param string             $shipper
93
     * @param array<mixed,mixed> $data
94
     * @param bool               $shouldHaveStatus
95
     *
96
     * @return array<mixed,mixed>
97
     *
98
     * @throws \Inspirum\Balikobot\Contracts\ExceptionInterface
99
     */
100 357
    public function call(
101
        string $version,
102
        string $shipper,
103
        string $request,
104
        array $data = [],
105
        bool $shouldHaveStatus = true,
106
        bool $gzip = false,
107
    ): array {
108
        // resolve url
109 357
        $path = trim($shipper . '/' . $request, '/');
110 357
        $path = str_replace('//', '/', $path);
111 357
        $host = $this->resolveHostName($version);
112
113
        // add query to compress response as gzip
114 357
        if ($gzip) {
115 14
            $path .= '?gzip=1';
116
        }
117
118
        // call API server and get response
119 357
        $response = $this->request($host . $path, $data);
120
121
        // get status code and content
122 357
        $statusCode = $response->getStatusCode();
123 357
        $content    = $this->getContents($response->getBody(), $gzip);
124
125
        // parse response content to assoc array
126 357
        $content = $this->parseContents($content, $statusCode < 300);
127
128
        // validate response status code
129 356
        $this->validateResponse($statusCode, $content, $shouldHaveStatus);
130
131
        // return response
132 244
        return $content;
133
    }
134
135
    /**
136
     * Get API response
137
     *
138
     * @param string             $url
139
     * @param array<mixed,mixed> $data
140
     *
141
     * @return \Psr\Http\Message\ResponseInterface
142
     */
143 57
    public function request(string $url, array $data = []): ResponseInterface
144
    {
145
        // init curl
146 57
        $ch = curl_init();
147
148
        // set headers
149 57
        curl_setopt($ch, CURLOPT_URL, $url);
150 57
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
151 57
        curl_setopt($ch, CURLOPT_HEADER, false);
152
153
        // disable SSL verification
154 57
        if ($this->sslVerify === false) {
155 1
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
156 1
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
157
        }
158
159
        // set data
160 57
        if (count($data) > 0) {
161 23
            curl_setopt($ch, CURLOPT_POST, true);
162 23
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
163
        }
164
165
        // set auth
166 57
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
167 57
            'Authorization: Basic ' . base64_encode($this->apiUser . ':' . $this->apiKey),
168 57
            'Content-Type: application/json',
169
        ]);
170
171
        // execute curl
172 57
        $response   = curl_exec($ch);
173 57
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
174
175
        // check for errors.
176 57
        if ($response === false) {
177 1
            throw new RuntimeException(curl_error($ch), curl_errno($ch));
178
        }
179
180
        // close curl
181 56
        curl_close($ch);
182
183 56
        return new Response((int) $statusCode, [], (string) $response);
184
    }
185
186
    /**
187
     * Decode API response JSON to array
188
     *
189
     * @param string $content
190
     *
191
     * @return array<mixed,mixed>
192
     *
193
     * @throws \Inspirum\Balikobot\Contracts\ExceptionInterface
194
     */
195 357
    private function parseContents(string $content, bool $throwOnError): array
196
    {
197
        try {
198 357
            return $this->decode($content);
199 2
        } catch (JsonException $exception) {
200 2
            if ($throwOnError) {
201 1
                throw new BadRequestException([], 400, $exception, 'Cannot parse response data');
202
            }
203
204 1
            return [];
205
        }
206
    }
207
208
    /**
209
     * Decode API response JSON to array
210
     *
211
     * @param string $content
212
     *
213
     * @return array<mixed,mixed>
214
     *
215
     * @throws \JsonException
216
     */
217 357
    protected function decode(string $content): array
218
    {
219 357
        return json_decode($content, true, flags: JSON_THROW_ON_ERROR);
220
    }
221
222
    /**
223
     * Get API url for given version
224
     *
225
     * @param string $version
226
     *
227
     * @return string
228
     */
229 357
    private function resolveHostName(string $version): string
230
    {
231 357
        return API::URL[$version] ?? API::URL[API::V2V1];
232
    }
233
234
    /**
235
     * Get response content (even gzipped)
236
     *
237
     * @param \Psr\Http\Message\StreamInterface $stream
238
     * @param bool                              $gzip
239
     *
240
     * @return string
241
     */
242 357
    private function getContents(StreamInterface $stream, bool $gzip): string
243
    {
244 357
        if ($gzip === false) {
245 344
            return $stream->getContents();
246
        }
247
248
        try {
249 14
            $inflateStream = new InflateStream($stream);
250
251 14
            return $inflateStream->getContents();
252 10
        } catch (Throwable) {
253 10
            $stream->rewind();
254
255 10
            return $stream->getContents();
256
        }
257
    }
258
259
    /**
260
     * Validate response
261
     *
262
     * @param int                $statusCode
263
     * @param array<mixed,mixed> $response
264
     * @param bool               $shouldHaveStatus
265
     *
266
     * @throws \Inspirum\Balikobot\Contracts\ExceptionInterface
267
     */
268 356
    private function validateResponse(int $statusCode, array $response, bool $shouldHaveStatus): void
269
    {
270 356
        $this->validator->validateStatus($statusCode, $response);
271
272 312
        $this->validator->validateResponseStatus($response, null, $shouldHaveStatus);
273 244
    }
274
}
275