Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.75% covered (warning)
88.75%
71 / 80
80.00% covered (warning)
80.00%
12 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
VultrClientHandler
88.75% covered (warning)
88.75%
71 / 80
80.00% covered (warning)
80.00%
12 / 15
41.17
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setClient
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRequestFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setResponseFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setStreamFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 post
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 put
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 patch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 generateRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 request
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
5.02
 applyOptions
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
7.12
 formalizeErrorMessage
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 bodySummary
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
7.39
1<?php
2
3declare(strict_types=1);
4
5namespace Vultr\VultrPhp;
6
7use InvalidArgumentException;
8use Psr\Http\Client\ClientExceptionInterface;
9use Psr\Http\Client\ClientInterface;
10use Psr\Http\Message\MessageInterface;
11use Psr\Http\Message\RequestFactoryInterface;
12use Psr\Http\Message\RequestInterface;
13use Psr\Http\Message\ResponseFactoryInterface;
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\StreamFactoryInterface;
16use Throwable;
17use Vultr\VultrPhp\Util\VultrUtil;
18
19class VultrClientHandler
20{
21    private ClientInterface $client;
22    private RequestFactoryInterface $request_fact;
23    private ResponseFactoryInterface $response_fact;
24    private StreamFactoryInterface $stream_fact;
25
26    private VultrAuth $auth;
27
28    private const QUERY = 0;
29    private const JSON = 1;
30
31    public function __construct(
32        VultrAuth $auth,
33        ClientInterface $http,
34        RequestFactoryInterface $request,
35        ResponseFactoryInterface $response,
36        StreamFactoryInterface $stream
37    )
38    {
39        $this->auth = $auth;
40
41        $this->setClient($http);
42        $this->setRequestFactory($request);
43        $this->setResponseFactory($response);
44        $this->setStreamFactory($stream);
45    }
46
47    public function setClient(ClientInterface $http) : void
48    {
49        $this->client = $http;
50    }
51
52    public function setRequestFactory(RequestFactoryInterface $request) : void
53    {
54        $this->request_fact = $request;
55    }
56
57    public function setResponseFactory(ResponseFactoryInterface $response) : void
58    {
59        $this->response_fact = $response;
60    }
61
62    public function setStreamFactory(StreamFactoryInterface $stream) : void
63    {
64        $this->stream_fact = $stream;
65    }
66
67    /**
68     * @param $uri - string - anything after api.vultr.com/v2/
69     * @param $params - array|null - query parameters that will be added to the uri query stirng.
70     * @throws VultrClientException
71     * @return ResponseInterface
72     */
73    public function delete(string $uri, ?array $params = []) : ResponseInterface
74    {
75        $options = [];
76        if ($params !== null)
77        {
78            $options[self::QUERY] = $params;
79        }
80
81        return $this->request($this->generateRequest('DELETE', $uri, $options));
82    }
83
84    /**
85     * @param $uri - string - anything after api.vultr.com/v2/
86     * @param $params - array - form data that will be encoded to a json
87     * @throws VultrClientException
88     * @return ResponseInterface
89     */
90    public function post(string $uri, array $params = []) : ResponseInterface
91    {
92        return $this->request($this->generateRequest('POST', $uri, [self::JSON => $params]));
93    }
94
95    /**
96     * @param $uri - string - anything after api.vultr.com/v2/
97     * @param $params - array - form data that will be encoded to a json
98     * @throws VultrClientException
99     * @return ResponseInterface
100     */
101    public function put(string $uri, array $params = []) : ResponseInterface
102    {
103        return $this->request($this->generateRequest('PUT', $uri, [self::JSON => $params]));
104    }
105
106    /**
107     * @param $uri - string - anything after api.vultr.com/v2/
108     * @param $params - array - form data that will be encoded to a json
109     * @throws VultrClientException
110     * @return ResponseInterface
111     */
112    public function patch(string $uri, array $params = []) : ResponseInterface
113    {
114        return $this->request($this->generateRequest('PATCH', $uri, [self::JSON => $params]));
115    }
116
117    /**
118     * @param $uri - string - anything after api.vultr.com/v2/
119     * @param $params - array|null - query parameters that will be added to the uri query stirng.
120     * @throws VultrClientException
121     * @return ResponseInterface
122     */
123    public function get(string $uri, ?array $params = null) : ResponseInterface
124    {
125        $options = [];
126        if ($params !== null)
127        {
128            $options[self::QUERY] = $params;
129        }
130
131        return $this->request($this->generateRequest('GET', $uri, $options));
132    }
133
134    private function generateRequest(string $method, string $uri, array $options = []) : RequestInterface
135    {
136        $request = $this->request_fact->createRequest($method, VultrConfig::getBaseURI().ltrim($uri, '/'));
137        foreach (VultrConfig::generateHeaders($this->auth) as $header => $value)
138        {
139            $request = $request->withHeader($header, $value);
140        }
141
142        return $this->applyOptions($request, $options);
143    }
144
145    private function request(RequestInterface $request) : ResponseInterface
146    {
147        try
148        {
149            $response = $this->client->sendRequest($request);
150        }
151        catch (ClientExceptionInterface $e)
152        {
153            throw new VultrClientException($this->formalizeErrorMessage($this->response_fact->createResponse(500), $e->getRequest()), null, $e);
154        }
155        catch (Throwable $e)
156        {
157            throw new VultrClientException($request->getMethod().' fatal failed: '.$e->getMessage(), null, $e);
158        }
159
160        $level = VultrUtil::getLevel($response);
161        if ($level >= 4)
162        {
163            $message = $this->formalizeErrorMessage($response, $request);
164            throw new VultrClientException($request->getMethod().' failed: '.$message, $response->getStatusCode());
165        }
166
167        return $response;
168    }
169
170    private function applyOptions(RequestInterface $request, array &$options) : RequestInterface
171    {
172        if (isset($options[self::JSON]) && !empty($options[self::JSON]))
173        {
174            $json = json_encode($options[self::JSON], 0, 512);
175            if (JSON_ERROR_NONE !== json_last_error())
176            {
177                throw new InvalidArgumentException('json_encode error: ' . json_last_error_msg());
178            }
179            $request = $request->withBody($this->stream_fact->createStream($json));
180            unset($options[self::JSON]);
181        }
182
183        if (isset($options[self::QUERY]))
184        {
185            $value = $options[self::QUERY];
186            if (is_array($value))
187            {
188                $value = http_build_query($value, '', '&', PHP_QUERY_RFC3986);
189            }
190
191            if (!is_string($value))
192            {
193                throw new InvalidArgumentException('query must be a string or array');
194            }
195
196            $request = $request->withUri($request->getUri()->withQuery($value), true);
197            unset($options[self::QUERY]);
198        }
199
200        return $request;
201    }
202
203    private function formalizeErrorMessage(ResponseInterface $response, RequestInterface $request) : string
204    {
205        if ($response->getHeaderLine('Content-Type') === 'application/json' && $response->getBody()->getSize() < 512)
206        {
207            $error = json_decode((string)$response->getBody(), true);
208            if (isset($error['error']))
209            {
210                return $error['error'];
211            }
212        }
213
214        $level = VultrUtil::getLevel($response);
215
216        $label = 'Unsuccessful request';
217        if ($level === 4) $label = 'Client error';
218        else if ($level === 5) $label = 'Server error';
219
220        $message = sprintf(
221            '%s: `%s %s` resulted in a `%s %s` response.',
222            $label,
223            $request->getMethod(),
224            $request->getUri(),
225            $response->getStatusCode(),
226            $response->getReasonPhrase()
227        );
228
229        if (($summary = $this->bodySummary($response)) !== null)
230        {
231            $message .= "\n{$summary}\n";
232        }
233
234        return $message;
235    }
236
237    private function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string
238    {
239        $body = $message->getBody();
240
241        if (!$body->isSeekable() || !$body->isReadable())
242        {
243            return null;
244        }
245
246        $size = $body->getSize();
247
248        if ($size === 0)
249        {
250            return null;
251        }
252
253        if ($body->eof())
254        {
255            $body->rewind();
256        }
257
258        $summary = $body->read($truncateAt);
259        $body->rewind();
260
261        if ($size > $truncateAt)
262        {
263            $summary .= ' (truncated...)';
264        }
265
266        // Matches any printable character, including unicode characters:
267        // letters, marks, numbers, punctuation, spacing, and separators.
268        if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary))
269        {
270            return null;
271        }
272
273        return $summary;
274    }
275}