Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
79 / 79 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
Client | |
100.00% |
79 / 79 |
|
100.00% |
3 / 3 |
18 | |
100.00% |
1 / 1 |
run | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
4 | |||
runMulti | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
8 | |||
parseHeaderLine | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
6 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework HTTP Client Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\HTTP\Client; |
11 | |
12 | use CurlHandle; |
13 | use Framework\HTTP\Status; |
14 | use Generator; |
15 | use InvalidArgumentException; |
16 | use RuntimeException; |
17 | |
18 | /** |
19 | * Class Client. |
20 | * |
21 | * @see https://www.php.net/manual/en/function.curl-setopt.php |
22 | * @see https://curl.se/libcurl/c/curl_easy_setopt.html |
23 | * @see https://php.watch/articles/php-curl-security-hardening |
24 | * |
25 | * @package http-client |
26 | */ |
27 | class Client |
28 | { |
29 | /** |
30 | * @var array<mixed> |
31 | */ |
32 | protected array $parsed = []; |
33 | |
34 | /** |
35 | * Run the Request. |
36 | * |
37 | * @param Request $request |
38 | * |
39 | * @throws InvalidArgumentException for invalid Request Protocol |
40 | * @throws RuntimeException for curl error |
41 | * |
42 | * @return Response |
43 | */ |
44 | public function run(Request $request) : Response |
45 | { |
46 | $handle = \curl_init(); |
47 | $options = $request->getOptions(); |
48 | $options[\CURLOPT_HEADERFUNCTION] = [$this, 'parseHeaderLine']; |
49 | \curl_setopt_array($handle, $options); |
50 | $body = \curl_exec($handle); |
51 | $info = []; |
52 | if ($request->isGettingResponseInfo()) { |
53 | $info = (array) \curl_getinfo($handle); |
54 | } |
55 | if ($body === false) { |
56 | throw new RuntimeException(\curl_error($handle), \curl_errno($handle)); |
57 | } |
58 | \curl_close($handle); |
59 | if ($body === true) { |
60 | $body = ''; |
61 | } |
62 | $objectId = \spl_object_id($handle); |
63 | $response = new Response( |
64 | $request, |
65 | $this->parsed[$objectId]['protocol'], |
66 | $this->parsed[$objectId]['code'], |
67 | $this->parsed[$objectId]['reason'], |
68 | $this->parsed[$objectId]['headers'], |
69 | $body, |
70 | $info |
71 | ); |
72 | unset($this->parsed[$objectId]); |
73 | return $response; |
74 | } |
75 | |
76 | /** |
77 | * Run multiple HTTP Requests. |
78 | * |
79 | * @param Request[] $requests An associative array of Request instances |
80 | * with ids as keys |
81 | * |
82 | * @return Generator<Response> The Requests ids as keys and its respective |
83 | * Responses as values |
84 | */ |
85 | public function runMulti(array $requests) : Generator |
86 | { |
87 | $multiHandle = \curl_multi_init(); |
88 | $handles = []; |
89 | foreach ($requests as $id => $request) { |
90 | $handle = \curl_init(); |
91 | $options = $request->getOptions(); |
92 | $options[\CURLOPT_HEADERFUNCTION] = [$this, 'parseHeaderLine']; |
93 | \curl_setopt_array($handle, $options); |
94 | $handles[$id] = $handle; |
95 | \curl_multi_add_handle($multiHandle, $handle); |
96 | } |
97 | do { |
98 | $status = \curl_multi_exec($multiHandle, $stillRunning); |
99 | $message = \curl_multi_info_read($multiHandle); |
100 | if ($message) { |
101 | foreach ($handles as $id => $handle) { |
102 | if ($message['handle'] === $handle) { |
103 | $info = []; |
104 | if ($requests[$id]->isGettingResponseInfo()) { |
105 | $info = (array) \curl_getinfo($handle); |
106 | } |
107 | $objectId = \spl_object_id($handle); |
108 | if ( ! isset($this->parsed[$objectId])) { |
109 | unset($handles[$id]); |
110 | break; |
111 | } |
112 | yield $id => new Response( |
113 | $requests[$id], |
114 | $this->parsed[$objectId]['protocol'], |
115 | $this->parsed[$objectId]['code'], |
116 | $this->parsed[$objectId]['reason'], |
117 | $this->parsed[$objectId]['headers'], |
118 | (string) \curl_multi_getcontent($message['handle']), |
119 | $info |
120 | ); |
121 | unset($this->parsed[$objectId], $handles[$id]); |
122 | break; |
123 | } |
124 | } |
125 | \curl_multi_remove_handle($multiHandle, $message['handle']); |
126 | } |
127 | } while ($stillRunning && $status === \CURLM_OK); |
128 | \curl_multi_close($multiHandle); |
129 | } |
130 | |
131 | /** |
132 | * Parses Header line. |
133 | * |
134 | * @param CurlHandle $curlHandle |
135 | * @param string $line |
136 | * |
137 | * @return int |
138 | */ |
139 | protected function parseHeaderLine(CurlHandle $curlHandle, string $line) : int |
140 | { |
141 | $trimmedLine = \trim($line); |
142 | $lineLength = \strlen($line); |
143 | if ($trimmedLine === '') { |
144 | return $lineLength; |
145 | } |
146 | $id = \spl_object_id($curlHandle); |
147 | if ( ! \str_contains($trimmedLine, ':')) { |
148 | if (\str_starts_with($trimmedLine, 'HTTP/')) { |
149 | $parts = \explode(' ', $trimmedLine, 3); |
150 | $this->parsed[$id]['protocol'] = $parts[0]; |
151 | $this->parsed[$id]['code'] = (int) ($parts[1] ?? 200); |
152 | $this->parsed[$id]['reason'] = $parts[2] |
153 | ?? Status::getReason($this->parsed[$id]['code']); |
154 | } |
155 | return $lineLength; |
156 | } |
157 | [$name, $value] = \explode(':', $trimmedLine, 2); |
158 | $name = \trim($name); |
159 | $value = \trim($value); |
160 | if ($name !== '' && $value !== '') { |
161 | $this->parsed[$id]['headers'][\strtolower($name)][] = $value; |
162 | } |
163 | return $lineLength; |
164 | } |
165 | } |