Passed
Push — master ( 0c6eee...067d60 )
by Anatoly
15:56 queued 13:58
created

Client   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 199
Duplicated Lines 0 %

Test Coverage

Coverage 91.78%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 21
eloc 71
c 6
b 0
f 0
dl 0
loc 199
ccs 67
cts 73
cp 0.9178
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A sendRequest() 0 14 2
A populateResponseWithHeaderFields() 0 24 5
A sendRequests() 0 29 5
A createCurlHandleFromRequest() 0 33 6
A createResponseFromCurlHandle() 0 25 2
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Fenric <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Fenric
8
 * @license https://github.com/sunrise-php/http-client-curl/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-client-curl
10
 */
11
12
namespace Sunrise\Http\Client\Curl;
13
14
/**
15
 * Import classes
16
 */
17
use Psr\Http\Client\ClientInterface;
18
use Psr\Http\Message\RequestInterface;
19
use Psr\Http\Message\ResponseFactoryInterface;
20
use Psr\Http\Message\ResponseInterface;
21
use Sunrise\Http\Client\Curl\Exception\ClientException;
22
use Sunrise\Http\Client\Curl\Exception\NetworkException;
23
24
/**
25
 * Import functions
26
 */
27
use function curl_close;
28
use function curl_errno;
29
use function curl_error;
30
use function curl_exec;
31
use function curl_getinfo;
32
use function curl_init;
33
use function curl_multi_add_handle;
34
use function curl_multi_close;
35
use function curl_multi_exec;
36
use function curl_multi_init;
37
use function curl_multi_remove_handle;
38
use function curl_setopt_array;
39
use function explode;
40
use function in_array;
41
use function ltrim;
42
use function sprintf;
43
use function strpos;
44
use function substr;
45
46
/**
47
 * Import constants
48
 */
49
use const CURLINFO_HEADER_SIZE;
50
use const CURLINFO_RESPONSE_CODE;
51
use const CURLINFO_TOTAL_TIME;
52
use const CURLOPT_CUSTOMREQUEST;
53
use const CURLOPT_HEADER;
54
use const CURLOPT_HTTPHEADER;
55
use const CURLOPT_POSTFIELDS;
56
use const CURLOPT_RETURNTRANSFER;
57
use const CURLOPT_URL;
58
59
/**
60
 * HTTP client based on cURL
61
 *
62
 * @link http://php.net/manual/en/intro.curl.php
63
 * @link https://curl.haxx.se/libcurl/c/libcurl-errors.html
64
 * @link https://www.php-fig.org/psr/psr-2/
65
 * @link https://www.php-fig.org/psr/psr-7/
66
 * @link https://www.php-fig.org/psr/psr-17/
67
 * @link https://www.php-fig.org/psr/psr-18/
68
 */
69
class Client implements ClientInterface
70
{
71
72
    /**
73
     * @var ResponseFactoryInterface
74
     */
75
    protected $responseFactory;
76
77
    /**
78
     * @var array<int, mixed>
79
     */
80
    protected $curlOptions;
81
82
    /**
83
     * Constructor of the class
84
     *
85
     * @param ResponseFactoryInterface $responseFactory
86
     * @param array<int, mixed> $curlOptions
87
     */
88 4
    public function __construct(
89
        ResponseFactoryInterface $responseFactory,
90
        array $curlOptions = []
91
    ) {
92 4
        $this->responseFactory = $responseFactory;
93 4
        $this->curlOptions = $curlOptions;
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 2
    public function sendRequest(RequestInterface $request) : ResponseInterface
100
    {
101 2
        $curlHandle = $this->createCurlHandleFromRequest($request);
102
103 2
        $isSuccess = curl_exec($curlHandle);
104 2
        if ($isSuccess === false) {
105 1
            throw new NetworkException($request, curl_error($curlHandle), curl_errno($curlHandle));
106
        }
107
108 1
        $response = $this->createResponseFromCurlHandle($curlHandle);
109
110 1
        curl_close($curlHandle);
111
112 1
        return $response;
113
    }
114
115
    /**
116
     * Sends the given requests and returns responses in the same order
117
     *
118
     * @param RequestInterface ...$requests
119
     *
120
     * @return array<int, ResponseInterface>
121
     *
122
     * @throws ClientException
123
     * @throws NetworkException
124
     */
125 1
    public function sendRequests(RequestInterface ...$requests) : array
126
    {
127
        /** @var list<RequestInterface> $requests */
128
129 1
        $curlMultiHandle = curl_multi_init();
130 1
        if ($curlMultiHandle === false) {
131
            throw new ClientException('Unable to create CurlMultiHandle');
132
        }
133
134 1
        $curlHandles = [];
135 1
        foreach ($requests as $i => $request) {
136 1
            $curlHandles[$i] = $this->createCurlHandleFromRequest($request);
137 1
            curl_multi_add_handle($curlMultiHandle, $curlHandles[$i]);
0 ignored issues
show
Bug introduced by
It seems like $curlMultiHandle can also be of type true; however, parameter $multi_handle of curl_multi_add_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

137
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $curlMultiHandle, $curlHandles[$i]);
Loading history...
138
        }
139
140
        do {
141 1
            curl_multi_exec($curlMultiHandle, $isActive);
0 ignored issues
show
Bug introduced by
It seems like $curlMultiHandle can also be of type true; however, parameter $multi_handle of curl_multi_exec() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
            curl_multi_exec(/** @scrutinizer ignore-type */ $curlMultiHandle, $isActive);
Loading history...
142
        } while ($isActive);
143
144 1
        $responses = [];
145 1
        foreach ($curlHandles as $i => $curlHandle) {
146 1
            $responses[$i] = $this->createResponseFromCurlHandle($curlHandle);
147 1
            curl_multi_remove_handle($curlMultiHandle, $curlHandle);
0 ignored issues
show
Bug introduced by
It seems like $curlMultiHandle can also be of type true; however, parameter $multi_handle of curl_multi_remove_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

147
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $curlMultiHandle, $curlHandle);
Loading history...
148 1
            curl_close($curlHandle);
149
        }
150
151 1
        curl_multi_close($curlMultiHandle);
0 ignored issues
show
Bug introduced by
It seems like $curlMultiHandle can also be of type true; however, parameter $multi_handle of curl_multi_close() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

151
        curl_multi_close(/** @scrutinizer ignore-type */ $curlMultiHandle);
Loading history...
152
153 1
        return $responses;
154
    }
155
156
    /**
157
     * Creates a CurlHandle from the given request
158
     *
159
     * @param RequestInterface $request
160
     *
161
     * @return resource
162
     *
163
     * @throws ClientException
164
     */
165 3
    private function createCurlHandleFromRequest(RequestInterface $request)
166
    {
167 3
        $curlOptions = $this->curlOptions;
168
169 3
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
170 3
        $curlOptions[CURLOPT_HEADER] = true;
171
172 3
        $curlOptions[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
173 3
        $curlOptions[CURLOPT_URL] = $request->getUri()->__toString();
174
175 3
        $curlOptions[CURLOPT_HTTPHEADER] = [];
176 3
        foreach ($request->getHeaders() as $name => $values) {
177 2
            foreach ($values as $value) {
178 2
                $curlOptions[CURLOPT_HTTPHEADER][] = sprintf('%s: %s', $name, $value);
179
            }
180
        }
181
182 3
        $curlOptions[CURLOPT_POSTFIELDS] = null;
183 3
        if (!in_array($request->getMethod(), ['GET', 'HEAD'], true)) {
184
            $curlOptions[CURLOPT_POSTFIELDS] = $request->getBody()->__toString();
185
        }
186
187 3
        $curlHandle = curl_init();
188 3
        if ($curlHandle === false) {
189
            throw new ClientException('Unable to create CurlHandle');
190
        }
191
192 3
        $isSuccess = curl_setopt_array($curlHandle, $curlOptions);
193 3
        if ($isSuccess === false) {
194
            throw new ClientException('Unable to configure CurlHandle');
195
        }
196
197 3
        return $curlHandle;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $curlHandle also could return the type CurlHandle which is incompatible with the documented return type resource.
Loading history...
198
    }
199
200
    /**
201
     * Creates a response from the given CurlHandle
202
     *
203
     * @param resource $curlHandle
204
     *
205
     * @return ResponseInterface
206
     */
207 2
    private function createResponseFromCurlHandle($curlHandle) : ResponseInterface
208
    {
209
        /** @var int */
210 2
        $statusCode = curl_getinfo($curlHandle, CURLINFO_RESPONSE_CODE);
211 2
        $response = $this->responseFactory->createResponse($statusCode);
212
213
        /** @var float */
214 2
        $requestTime = curl_getinfo($curlHandle, CURLINFO_TOTAL_TIME);
215 2
        $response = $response->withAddedHeader('X-Request-Time', sprintf('%.3f ms', $requestTime * 1000));
216
217
        /** @var ?string */
218 2
        $message = curl_multi_getcontent($curlHandle);
219 2
        if ($message === null) {
220
            return $response;
221
        }
222
223
        /** @var int */
224 2
        $headerSize = curl_getinfo($curlHandle, CURLINFO_HEADER_SIZE);
225 2
        $header = substr($message, 0, $headerSize);
226 2
        $response = $this->populateResponseWithHeaderFields($response, $header);
227
228 2
        $body = substr($message, $headerSize);
229 2
        $response->getBody()->write($body);
230
231 2
        return $response;
232
    }
233
234
    /**
235
     * Populates the given response with the given header's fields
236
     *
237
     * @param ResponseInterface $response
238
     * @param string $header
239
     *
240
     * @return ResponseInterface
241
     *
242
     * @link https://datatracker.ietf.org/doc/html/rfc2616#section-4.2
243
     */
244 2
    private function populateResponseWithHeaderFields(ResponseInterface $response, string $header) : ResponseInterface
245
    {
246 2
        $fields = explode("\r\n", $header);
247
248 2
        foreach ($fields as $field) {
249
            // end...
250 2
            if ($field === '') {
251 2
                break;
252
            }
253
            // status line
254 2
            if (strpos($field, 'HTTP/') === 0) {
255 2
                continue;
256
            }
257
            // HTTP/2 field
258 2
            if (strpos($field, ':') === 0) {
259
                continue;
260
            }
261
262 2
            list($name, $value) = explode(':', $field, 2);
263
264 2
            $response = $response->withAddedHeader($name, ltrim($value));
265
        }
266
267 2
        return $response;
268
    }
269
}
270