Test Setup Failed
Pull Request — master (#38)
by
unknown
06:25
created

Client::sendRequests()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 29
ccs 16
cts 17
cp 0.9412
rs 9.3888
cc 5
nc 5
nop 1
crap 5.005
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 substr;
44
45
/**
46
 * Import constants
47
 */
48
use const CURLINFO_HEADER_SIZE;
49
use const CURLINFO_RESPONSE_CODE;
50
use const CURLINFO_TOTAL_TIME;
51
use const CURLOPT_CUSTOMREQUEST;
52
use const CURLOPT_HEADER;
53
use const CURLOPT_HTTPHEADER;
54
use const CURLOPT_POSTFIELDS;
55
use const CURLOPT_RETURNTRANSFER;
56
use const CURLOPT_URL;
57
58
/**
59
 * HTTP client based on cURL
60
 *
61
 * @link http://php.net/manual/en/intro.curl.php
62
 * @link https://curl.haxx.se/libcurl/c/libcurl-errors.html
63
 * @link https://www.php-fig.org/psr/psr-2/
64
 * @link https://www.php-fig.org/psr/psr-7/
65
 * @link https://www.php-fig.org/psr/psr-17/
66
 * @link https://www.php-fig.org/psr/psr-18/
67
 */
68
class Client implements ClientInterface
69
{
70
71
    /**
72
     * @var ResponseFactoryInterface
73
     */
74
    protected $responseFactory;
75
76
    /**
77
     * @var array<int, mixed>
78
     */
79
    protected $curlOptions;
80
81
    /**
82
     * Constructor of the class
83
     *
84
     * @param ResponseFactoryInterface $responseFactory
85
     * @param array<int, mixed> $curlOptions
86
     */
87 4
    public function __construct(
88
        ResponseFactoryInterface $responseFactory,
89
        array $curlOptions = []
90
    ) {
91 4
        $this->responseFactory = $responseFactory;
92 4
        $this->curlOptions = $curlOptions;
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 2
    public function sendRequest(RequestInterface $request) : ResponseInterface
99
    {
100 2
        $curlHandle = $this->createCurlHandleFromRequest($request);
101
102 2
        $isSuccess = curl_exec($curlHandle);
103 2
        if ($isSuccess === false) {
104 1
            throw new NetworkException($request, curl_error($curlHandle), curl_errno($curlHandle));
105
        }
106
107 1
        $response = $this->createResponseFromCurlHandle($curlHandle);
108
109 1
        curl_close($curlHandle);
110
111 1
        return $response;
112
    }
113
114
    /**
115
     * Sends the given requests and returns responses in the same order
116
     *
117
     * @param RequestInterface ...$requests
118
     *
119
     * @return array<int, ResponseInterface>
120
     *
121
     * @throws ClientException
122
     * @throws NetworkException
123
     */
124 1
    public function sendRequests(RequestInterface ...$requests) : array
125
    {
126
        /** @var list<RequestInterface> $requests */
127
128 1
        $curlMultiHandle = curl_multi_init();
129 1
        if ($curlMultiHandle === false) {
130
            throw new ClientException('Unable to create CurlMultiHandle');
131
        }
132
133 1
        $curlHandles = [];
134 1
        foreach ($requests as $i => $request) {
135 1
            $curlHandles[$i] = $this->createCurlHandleFromRequest($request);
136 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

136
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $curlMultiHandle, $curlHandles[$i]);
Loading history...
137
        }
138
139
        do {
140 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

140
            curl_multi_exec(/** @scrutinizer ignore-type */ $curlMultiHandle, $isActive);
Loading history...
141 1
        } while ($isActive);
142
143 1
        $responses = [];
144 1
        foreach ($curlHandles as $i => $curlHandle) {
145 1
            $responses[$i] = $this->createResponseFromCurlHandle($curlHandle);
146 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

146
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $curlMultiHandle, $curlHandle);
Loading history...
147 1
            curl_close($curlHandle);
148
        }
149
150 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

150
        curl_multi_close(/** @scrutinizer ignore-type */ $curlMultiHandle);
Loading history...
151
152 1
        return $responses;
153
    }
154
155
    /**
156
     * Creates a CurlHandle from the given request
157
     *
158
     * @param RequestInterface $request
159
     *
160
     * @return resource
161
     *
162
     * @throws ClientException
163
     */
164 3
    private function createCurlHandleFromRequest(RequestInterface $request)
165
    {
166 3
        $curlOptions = $this->curlOptions;
167
168 3
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
169 3
        $curlOptions[CURLOPT_HEADER] = true;
170
171 3
        $curlOptions[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
172 3
        $curlOptions[CURLOPT_URL] = $request->getUri()->__toString();
173
174 3
        $curlOptions[CURLOPT_HTTPHEADER] = [];
175 3
        foreach ($request->getHeaders() as $name => $values) {
176 2
            foreach ($values as $value) {
177 2
                $curlOptions[CURLOPT_HTTPHEADER][] = sprintf('%s: %s', $name, $value);
178
            }
179
        }
180
181 3
        $curlOptions[CURLOPT_POSTFIELDS] = null;
182 3
        if (!in_array($request->getMethod(), ['GET', 'HEAD'], true)) {
183
            $curlOptions[CURLOPT_POSTFIELDS] = $request->getBody()->__toString();
184
        }
185
186 3
        $curlHandle = curl_init();
187 3
        if ($curlHandle === false) {
188
            throw new ClientException('Unable to create CurlHandle');
189
        }
190
191 3
        $isSuccess = curl_setopt_array($curlHandle, $curlOptions);
192 3
        if ($isSuccess === false) {
193
            throw new ClientException('Unable to configure CurlHandle');
194
        }
195
196 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...
197
    }
198
199
    /**
200
     * Creates a response from the given CurlHandle
201
     *
202
     * @param resource $curlHandle
203
     *
204
     * @return ResponseInterface
205
     */
206 2
    private function createResponseFromCurlHandle($curlHandle) : ResponseInterface
207
    {
208
        /** @var int */
209 2
        $statusCode = curl_getinfo($curlHandle, CURLINFO_RESPONSE_CODE);
210 2
        $response = $this->responseFactory->createResponse($statusCode);
211
212
        /** @var float */
213 2
        $requestTime = curl_getinfo($curlHandle, CURLINFO_TOTAL_TIME);
214 2
        $response = $response->withAddedHeader('X-Request-Time', sprintf('%.3f ms', $requestTime * 1000));
215
216
        /** @var ?string */
217 2
        $message = curl_multi_getcontent($curlHandle);
218 2
        if ($message === null) {
219
            return $response;
220
        }
221
222
        /** @var int */
223 2
        $headerSize = curl_getinfo($curlHandle, CURLINFO_HEADER_SIZE);
224 2
        $header = substr($message, 0, $headerSize);
225 2
        $response = $this->populateResponseWithHeaderFields($response, $header);
226
227 2
        $body = substr($message, $headerSize);
228 2
        $response->getBody()->write($body);
229
230 2
        return $response;
231
    }
232
233
    /**
234
     * Populates the given response with the given header's fields
235
     *
236
     * @param ResponseInterface $response
237
     * @param string $header
238
     *
239
     * @return ResponseInterface
240
     *
241
     * @link https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
242
     */
243 2
    private function populateResponseWithHeaderFields(ResponseInterface $response, string $header) : ResponseInterface
244
    {
245 2
        $fields = explode("\r\n", $header);
246
247 2
        foreach ($fields as $i => $field) {
248
            // The first line of a response message is the status-line, consisting
249
            // of the protocol version, a space (SP), the status code, another
250
            // space, a possibly empty textual phrase describing the status code,
251
            // and ending with CRLF.
252
            // https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
253 2
            if ($i === 0) {
254 2
                continue;
255
            }
256
257
            // All HTTP/1.1 messages consist of a start-line followed by a sequence
258
            // of octets in a format similar to the Internet Message Format:
259
            // zero or more header fields (collectively referred to as
260
            // the "headers" or the "header section"), an empty line indicating the
261
            // end of the header section, and an optional message body.
262
            // https://datatracker.ietf.org/doc/html/rfc7230#section-3
263
            // https://datatracker.ietf.org/doc/html/rfc5322
264 2
            if ($field === '') {
265 2
                break;
266
            }
267
268
            // While HTTP/1.x used the message start-line (see [RFC7230],
269
            // Section 3.1) to convey the target URI, the method of the request, and
270
            // the status code for the response, HTTP/2 uses special pseudo-header
271
            // fields beginning with ':' character (ASCII 0x3a) for this purpose.
272
            // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.1
273 2
            if ($field[0] === ':') {
274
                continue;
275
            }
276
277 2
            [$name, $value] = explode(':', $field, 2);
278
279 2
            $response = $response->withAddedHeader($name, ltrim($value));
280
        }
281
282 2
        return $response;
283
    }
284
}
285