CurlHttpClient::prepare()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 16
rs 9.9332
cc 3
nc 4
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of slick/http
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Slick\Http\Client;
13
14
use CurlHandle;
15
use Psr\Http\Client\ClientInterface;
16
use Psr\Http\Message\RequestInterface;
17
use Psr\Http\Message\ResponseInterface;
18
use Slick\Http\Client\Exception\NetworkException;
19
use Slick\Http\Client\Exception\RequestException;
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 ClientInterface
29
{
30
    /**
31
     * @var null|Uri
32
     */
33
    private ?Uri $url = null;
34
35
    /**
36
     * @var null|HttpClientAuthentication
37
     */
38
    private ?HttpClientAuthentication $auth = null;
39
40
    /**
41
     * @var array<int, int|string|list<non-falsy-string>|non-falsy-string|true>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, int|string|li...|non-falsy-string|true> at position 8 could not be parsed: Expected '>' at position 8, but found 'list'.
Loading history...
42
     */
43
    private array $options = [
44
        CURLOPT_RETURNTRANSFER => true,
45
        CURLOPT_HEADER => true
46
    ];
47
48
    private mixed $handler = null;
49
50
    /**
51
     * Creates a CURL HTTP Client
52
     *
53
     * @param Uri|null                      $url
54
     * @param HttpClientAuthentication|null $auth
55
     * @param array<int, int|list<non-falsy-string>|string|true> $options
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, int|list<non-...sy-string>|string|true> at position 6 could not be parsed: Expected '>' at position 6, but found 'list'.
Loading history...
56
     */
57
    public function __construct(
58
        ?Uri $url = null,
59
        ?HttpClientAuthentication $auth = null,
60
        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
     * @inheritDoc
73
     */
74
    public function sendRequest(RequestInterface $request): ResponseInterface
75
    {
76
        $this->prepare($request);
77
        $result = curl_exec($this->handler);
78
        $errno = curl_errno($this->handler);
79
80
        switch ($errno) {
81
            case CURLE_OK:
82
                // All OK, no actions needed.
83
                break;
84
            case CURLE_COULDNT_RESOLVE_PROXY:
85
            case CURLE_COULDNT_RESOLVE_HOST:
86
            case CURLE_COULDNT_CONNECT:
87
            case CURLE_OPERATION_TIMEOUTED:
88
            case CURLE_SSL_CONNECT_ERROR:
89
                throw new NetworkException($request, curl_error($this->handler));
90
            default:
91
                throw new RequestException($request, curl_error($this->handler));
92
        }
93
94
        return $this->createResponse(\is_string($result) ? $result : '');
95
    }
96
97
    /**
98
     * Prepares the cURL handler options
99
     *
100
     * @param RequestInterface $request
101
     */
102
    private function prepare(RequestInterface $request): void
103
    {
104
        if ($this->handler) {
105
            $this->reset($this->handler);
106
        }
107
        $this->setUrl($request);
108
        $this->options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
109
        $this->setHeaders($request);
110
        $this->options[CURLOPT_POSTFIELDS] = (string) $request->getBody();
111
112
        if ($this->auth instanceof HttpClientAuthentication) {
113
            $this->options[CURLOPT_USERPWD] = "{$this->auth->username()}:{$this->auth->password()}";
114
            $this->options[CURLOPT_HTTPAUTH] = $this->auth->type();
115
        }
116
117
        curl_setopt_array($this->handler, $this->options);
118
    }
119
120
    /**
121
     * Sets the URL for cURL to use
122
     *
123
     * @param RequestInterface $request
124
     */
125
    private function setUrl(RequestInterface $request): void
126
    {
127
        $target = $request->getRequestTarget();
128
        /** @var array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string,
129
         *     path?: string, query?: string, fragment?: string}|false $parts */
130
        $parts = parse_url($target);
131
132
        $uri = $this->url instanceof Uri
133
            ? $this->url
134
            : $request->getUri();
135
136
        if (\is_array($parts)) {
0 ignored issues
show
introduced by
The condition is_array($parts) is always true.
Loading history...
137
            $uri = \array_key_exists('path', $parts) ? $uri->withPath($parts['path']) : $uri;
138
            $uri =  \array_key_exists('query', $parts)
139
                ? $uri->withQuery($parts['query'])
140
                : $uri;
141
        }
142
143
144
        $this->options[CURLOPT_URL] = (string) $uri;
145
    }
146
147
    /**
148
     * Sets the headers from the request
149
     *
150
     * @param RequestInterface $request
151
     */
152
    private function setHeaders(RequestInterface $request): void
153
    {
154
        $headers = [];
155
        foreach ($request->getHeaders() as $header => $values) {
156
            $headers[] = "$header: ".implode('; ', $values);
157
        }
158
        $this->options[CURLOPT_HTTPHEADER] = $headers;
159
    }
160
161
    /**
162
     * Resets the cURL handler
163
     *
164
     * @param resource $handler
165
     * @param-out CurlHandle|false $handler
166
     */
167
    private function reset(&$handler): void
168
    {
169
        $handler = curl_init();
170
    }
171
172
    /**
173
     * Creates a response from cURL execution result
174
     *
175
     * @param string $result
176
     *
177
     * @return Response
178
     */
179
    private function createResponse(string $result): Response
180
    {
181
        $status = curl_getinfo($this->handler, CURLINFO_HTTP_CODE);
182
        list($header, $body) = $this->splitHeaderFromBody($result);
183
        return new Response($status, trim($body), $this->parseHeaders($header));
184
    }
185
186
    /**
187
     * Splits the cURL execution result into header and body
188
     *
189
     * @param string $result
190
     *
191
     * @return array{0: string, 1: string}
192
     */
193
    private function splitHeaderFromBody(string $result): array
194
    {
195
        $headerSize = curl_getinfo($this->handler, CURLINFO_HEADER_SIZE);
196
197
        $header = substr($result, 0, $headerSize);
198
        $body = substr($result, $headerSize);
199
200
        return [trim($header), trim($body)];
201
    }
202
203
    /**
204
     * Parses the HTTP message headers from header part
205
     *
206
     * @param string $header
207
     *
208
     * @return array<string>
209
     */
210
    private function parseHeaders(string $header): array
211
    {
212
        $lines = explode("\n", $header);
213
        $headers = [];
214
        foreach ($lines as $line) {
215
            if (!str_contains($line, ':')) {
216
                continue;
217
            }
218
219
            $middle = explode(":", $line);
220
            $headers[trim($middle[0])] = trim($middle[1]);
221
        }
222
        return $headers;
223
    }
224
225
    /**
226
     * Close the cURL handler on destruct
227
     */
228
    public function __destruct()
229
    {
230
        if ($this->handler) {
231
            curl_close($this->handler);
232
        }
233
    }
234
}
235