Completed
Push — master ( 6b8416...c520c0 )
by Filipe
02:12
created

CurlHttpClient   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 25
lcom 1
cbo 8
dl 0
loc 243
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A send() 0 12 2
A call() 0 19 2
A prepare() 0 15 2
A setUrl() 0 7 2
A setHeaders() 0 8 2
A reset() 0 3 1
A createResponse() 0 6 1
A splitHeaderFromBody() 0 9 1
A parseHeaders() 0 14 3
A checkClientError() 0 6 3
A checkServerError() 0 6 2
A __destruct() 0 5 2
1
<?php
2
3
/**
4
 * This file is part of slick/http
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Http\Client;
11
12
use Psr\Http\Message\RequestInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use React\Promise\Deferred;
15
use React\Promise\PromiseInterface;
16
use Slick\Http\Client\Exception\ClientErrorException;
17
use Slick\Http\Client\Exception\RuntimeException;
18
use Slick\Http\Client\Exception\ServerErrorException;
19
use Slick\Http\HttpClientInterface;
20
use Slick\Http\Message\Response;
21
use Slick\Http\Message\Uri;
22
23
/**
24
 * CurlHttpClient
25
 *
26
 * @package Slick\Http\Client
27
*/
28
final class CurlHttpClient implements HttpClientInterface
29
{
30
    /**
31
     * @var null|Uri
32
     */
33
    private $url;
34
35
    /**
36
     * @var null|HttpClientAuthentication
37
     */
38
    private $auth;
39
40
    /**
41
     * @var array
42
     */
43
    private $options = [
44
        CURLOPT_RETURNTRANSFER => true,
45
        CURLOPT_HEADER => true
46
    ];
47
48
    /**
49
     * @var resource
50
     */
51
    private $handler;
52
53
    /**
54
     * Creates a CURL HTTP Client
55
     *
56
     * @param Uri|null                      $url
57
     * @param HttpClientAuthentication|null $auth
58
     * @param array                         $options
59
     */
60
    public function __construct(Uri $url = null, HttpClientAuthentication $auth = null, array $options = [])
61
    {
62
        $this->handler = curl_init();
63
        $this->url = $url;
64
        $this->auth = $auth;
65
66
        foreach ($options as $name => $option) {
67
            $this->options[$name] = $option;
68
        }
69
    }
70
71
    /**
72
     * Send out an HTTP requests returning a promise
73
     *
74
     * @param RequestInterface $request
75
     *
76
     * @return PromiseInterface
77
     */
78
    public function send(RequestInterface $request)
79
    {
80
        $deferred = new Deferred();
81
        try {
82
            $response = $this->call($request);
83
            $deferred->resolve($response);
84
        } catch (\Exception $caught) {
85
            $deferred->reject($caught);
86
        }
87
88
        return $deferred->promise();
89
    }
90
91
    /**
92
     * Call the HTTP server returning the response
93
     *
94
     * @param RequestInterface $request
95
     *
96
     * @return Response
97
     *
98
     * @throws RuntimeException If any error occur while preparing and connecting to the server
99
     * @throws ClientErrorException for responses with status codes between 400 and 499
100
     * @throws ServerErrorException for responses with status codes grater or equal to 500
101
     */
102
    private function call(RequestInterface $request)
103
    {
104
        $this->prepare($request);
105
        $result = curl_exec($this->handler);
106
107
        if (curl_errno($this->handler) !== 0) {
108
            throw new RuntimeException(
109
                "Error connecting to server: ".curl_error($this->handler)
110
            );
111
        }
112
113
        $response = $this->createResponse($result);
114
115
        $this->checkClientError($response, $request);
116
117
        $this->checkServerError($response, $request);
118
119
        return $response;
120
    }
121
122
    /**
123
     * Prepares the cURL handler options
124
     *
125
     * @param RequestInterface $request
126
     */
127
    private function prepare(RequestInterface $request)
128
    {
129
        $this->reset($this->handler);
130
        $this->setUrl($request);
131
        $this->options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
132
        $this->setHeaders($request);
133
        $this->options[CURLOPT_POSTFIELDS] = (string) $request->getBody();
134
135
        if ($this->auth instanceof HttpClientAuthentication) {
136
            $this->options[CURLOPT_USERPWD] = "{$this->auth->username()}:{$this->auth->password()}";
137
            $this->options[CURLOPT_HTTPAUTH] = $this->auth->type();
138
        }
139
140
        curl_setopt_array($this->handler, $this->options);
141
    }
142
143
    /**
144
     * Sets the URL for cURL to use
145
     *
146
     * @param RequestInterface $request
147
     */
148
    private function setUrl(RequestInterface $request)
149
    {
150
        $uri = $this->url instanceof Uri
151
            ? $this->url
152
            : $request->getUri();
153
        $this->options[CURLOPT_URL] = (string) $uri;
154
    }
155
156
    /**
157
     * Sets the headers from the request
158
     *
159
     * @param RequestInterface $request
160
     */
161
    private function setHeaders(RequestInterface $request)
162
    {
163
        $headers = [];
164
        foreach ($request->getHeaders() as $header => $values) {
165
            $headers[] = "{$header}: ".implode('; ', $values);
166
        }
167
        $this->options[CURLOPT_HTTPHEADER] = $headers;
168
    }
169
170
    /**
171
     * Resets the cURL handler
172
     *
173
     * @param resource $ch
174
     */
175
    private function reset(&$ch){
176
        $ch = curl_init();
177
    }
178
179
    /**
180
     * Creates a response from cURL execution result
181
     *
182
     * @param string $result
183
     *
184
     * @return Response
185
     */
186
    private function createResponse($result)
187
    {
188
        $status = curl_getinfo($this->handler, CURLINFO_HTTP_CODE);
189
        list($header, $body) = $this->splitHeaderFromBody($result);
190
        return new Response($status, trim($body), $this->parseHeaders($header));
191
    }
192
193
    /**
194
     * Splits the cURL execution result into header and body
195
     *
196
     * @param $result
197
     *
198
     * @return array
199
     */
200
    private function splitHeaderFromBody($result)
201
    {
202
        $header_size = curl_getinfo($this->handler, CURLINFO_HEADER_SIZE);
203
204
        $header = substr($result, 0, $header_size);
205
        $body = substr($result, $header_size);
206
207
        return [trim($header), trim($body)];
208
    }
209
210
    /**
211
     * Parses the HTTP message headers from header part
212
     *
213
     * @param string $header
214
     *
215
     * @return array
216
     */
217
    private function parseHeaders($header)
218
    {
219
        $lines = explode("\n", $header);
220
        $headers = [];
221
        foreach ($lines as $line) {
222
            if (strpos($line, ':') === false ) {
223
                continue;
224
            }
225
226
            $middle=explode(":",$line);
227
            $headers[trim($middle[0])] = trim($middle[1]);
228
        }
229
        return $headers;
230
    }
231
232
    /**
233
     * Checks provided response is an HTTP client error (4xx)
234
     *
235
     * @param ResponseInterface $response
236
     * @param RequestInterface $request
237
     *
238
     * @throws ClientErrorException for responses with status codes between 400 and 499
239
     */
240
    private function checkClientError(ResponseInterface $response, RequestInterface $request)
241
    {
242
        if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 500) {
243
            throw new ClientErrorException($request, $response);
244
        }
245
    }
246
247
    /**
248
     * Checks provided response is an HTTP server error (5xx)
249
     *
250
     * @param ResponseInterface $response
251
     * @param RequestInterface $request
252
     *
253
     * @throws ServerErrorException for responses with status codes grater or equal to 500
254
     */
255
    private function checkServerError(ResponseInterface $response, RequestInterface $request)
256
    {
257
        if ($response->getStatusCode() >= 500) {
258
            throw new ServerErrorException($request, $response);
259
        }
260
    }
261
262
    /**
263
     * Close the cURL handler on destruct
264
     */
265
    public function __destruct()
266
    {
267
        if (is_resource($this->handler))
268
            curl_close($this->handler);
269
    }
270
}