1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace alkemann\h2l; |
4
|
|
|
|
5
|
|
|
use alkemann\h2l\exceptions\CurlFailure; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Class Remote |
9
|
|
|
* |
10
|
|
|
* Makes http requests using cURL. uses Message for both Request and Response description |
11
|
|
|
* |
12
|
|
|
* @TODO SSL verify optional |
13
|
|
|
* @TODO Proxy support? |
14
|
|
|
* @package alkemann\h2l |
15
|
|
|
*/ |
16
|
|
|
class Remote |
17
|
|
|
{ |
18
|
|
|
private $config = []; |
19
|
|
|
|
20
|
|
|
public function __construct(array $config = []) |
21
|
|
|
{ |
22
|
|
|
$this->config = $config + [ |
23
|
|
|
// defaults |
24
|
|
|
]; |
25
|
|
|
} |
26
|
|
|
|
27
|
|
View Code Duplication |
public function get(string $url, array $headers = []): Message |
|
|
|
|
28
|
|
|
{ |
29
|
|
|
$request = (new Message) |
30
|
|
|
->withType(Message::REQUEST) |
31
|
|
|
->withUrl($url) |
32
|
|
|
->withMethod(Request::GET) |
33
|
|
|
->withHeaders($headers); |
34
|
|
|
return $this->http($request); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
View Code Duplication |
public function postJson(string $url, array $data, array $headers = [], string $method = Request::POST): Message |
|
|
|
|
38
|
|
|
{ |
39
|
|
|
$headers['Content-Type'] = 'application/json; charset=utf-8'; |
40
|
|
|
$headers['Accept'] = 'application/json'; |
41
|
|
|
$data_string = json_encode($data); |
42
|
|
|
$headers['Content-Length'] = strlen($data_string); |
43
|
|
|
$request = (new Message) |
44
|
|
|
->withType(Message::REQUEST) |
45
|
|
|
->withUrl($url) |
46
|
|
|
->withMethod($method) |
47
|
|
|
->withBody($data_string) |
48
|
|
|
->withHeaders($headers); |
49
|
|
|
return $this->http($request); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
View Code Duplication |
public function postForm(string $url, array $data, array $headers = [], string $method = Request::POST): Message |
|
|
|
|
53
|
|
|
{ |
54
|
|
|
$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; |
55
|
|
|
$data_string = http_build_query($data); |
56
|
|
|
$headers['Content-Length'] = strlen($data_string); |
57
|
|
|
$request = (new Message) |
58
|
|
|
->withType(Message::REQUEST) |
59
|
|
|
->withUrl($url) |
60
|
|
|
->withMethod($method) |
61
|
|
|
->withBody($data_string) |
62
|
|
|
->withHeaders($headers); |
63
|
|
|
return $this->http($request); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
View Code Duplication |
public function delete(string $url, array $headers = []): Message |
|
|
|
|
67
|
|
|
{ |
68
|
|
|
$request = (new Message) |
69
|
|
|
->withType(Message::REQUEST) |
70
|
|
|
->withUrl($url) |
71
|
|
|
->withMethod(Request::DELETE) |
72
|
|
|
->withHeaders($headers); |
73
|
|
|
return $this->http($request); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
public function http(Message $request): Message |
77
|
|
|
{ |
78
|
|
|
$start = microtime(true); |
79
|
|
|
|
80
|
|
|
$curl_handler = $this->createCurlHandlerFromRequest($request); |
81
|
|
|
|
82
|
|
|
$meta = []; |
83
|
|
|
$headers = []; |
84
|
|
|
|
85
|
|
|
try { |
86
|
|
|
$content = curl_exec($curl_handler); |
87
|
|
|
if ($content === false) { |
88
|
|
|
throw new CurlFailure(curl_error($curl_handler), curl_errno($curl_handler)); |
89
|
|
|
} |
90
|
|
|
} catch (\Exception $e) { |
91
|
|
|
Log::error("CURL exception : " . get_class($e) . " : " . $e->getMessage()); |
92
|
|
|
|
93
|
|
|
$curl_failure = new CurlFailure($e->getMessage(), $e->getCode(), $e); |
94
|
|
|
$latency = microtime(true) - $start; |
95
|
|
|
$info = curl_getinfo($curl_handler); |
96
|
|
|
$curl_failure->setContext(compact('request', 'latency', 'info')); |
97
|
|
|
|
98
|
|
|
curl_close($curl_handler); |
99
|
|
|
unset($curl_handler); |
100
|
|
|
|
101
|
|
|
throw $curl_failure; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
$meta['latency'] = microtime(true) - $start; |
105
|
|
|
$meta['info'] = curl_getinfo($curl_handler); |
106
|
|
|
$code = $meta['info']['http_code']; |
107
|
|
|
|
108
|
|
|
$header_size = curl_getinfo($curl_handler, CURLINFO_HEADER_SIZE); |
109
|
|
|
$header = substr($content, 0, $header_size); |
110
|
|
|
if ($header) { |
111
|
|
|
$headers = $this->extractHeaders($header); |
112
|
|
|
} |
113
|
|
|
$content = substr($content, $header_size); |
114
|
|
|
|
115
|
|
|
// Curl handler no longer needed, let's close it |
116
|
|
|
|
117
|
|
|
curl_close($curl_handler); |
118
|
|
|
unset($curl_handler); |
119
|
|
|
|
120
|
|
|
return (new Message) |
121
|
|
|
->withType(Message::RESPONSE) |
122
|
|
|
->withUrl($request->url()) |
123
|
|
|
->withMethod($request->method()) |
124
|
|
|
->withBody($content) |
125
|
|
|
->withCode($code) |
126
|
|
|
->withHeaders($headers) |
127
|
|
|
->withMeta($meta); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* return resource a hurl handler |
132
|
|
|
*/ |
133
|
|
|
private function createCurlHandlerFromRequest(Message $request) |
134
|
|
|
{ |
135
|
|
|
$curl_handler = curl_init(); |
136
|
|
|
|
137
|
|
|
$options = $this->config['curl'] ?? []; |
138
|
|
|
$options += [ |
139
|
|
|
CURLOPT_FOLLOWLOCATION => true, |
140
|
|
|
CURLOPT_MAXREDIRS => 10, |
141
|
|
|
CURLOPT_CONNECTTIMEOUT => 5, |
142
|
|
|
CURLOPT_TIMEOUT => 5, |
143
|
|
|
CURLOPT_URL => $request->url(), |
144
|
|
|
CURLOPT_USERAGENT => 'alkemann\h2l\Remote', |
145
|
|
|
CURLOPT_RETURNTRANSFER => true, |
146
|
|
|
CURLOPT_HEADER => true, |
147
|
|
|
CURLOPT_CUSTOMREQUEST => $request->method(), |
148
|
|
|
]; |
149
|
|
|
|
150
|
|
|
if (!isset($options[CURLOPT_HTTPHEADER])) { |
151
|
|
|
$options[CURLOPT_HTTPHEADER] = []; |
152
|
|
|
} |
153
|
|
|
$body = $request->body(); |
154
|
|
|
if ($body) { |
|
|
|
|
155
|
|
|
$options[CURLOPT_POSTFIELDS] = $body; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
foreach ($request->headers() as $header_name => $header_value) { |
159
|
|
|
$options[CURLOPT_HTTPHEADER][] = "{$header_name}: {$header_value}"; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
$headers_to_set_to_blank_if_not_set = [ |
163
|
|
|
'Content-Type', |
164
|
|
|
'Expect', |
165
|
|
|
'Accept', |
166
|
|
|
'Accept-Encoding' |
167
|
|
|
]; |
168
|
|
|
foreach ($headers_to_set_to_blank_if_not_set as $name) { |
169
|
|
|
if ($request->header($name) === null) { |
170
|
|
|
$conf[CURLOPT_HTTPHEADER][] = "{$name}:"; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
curl_setopt_array($curl_handler, $options); |
175
|
|
|
|
176
|
|
|
return $curl_handler; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
private function extractHeaders(string $header): array |
180
|
|
|
{ |
181
|
|
|
$parts = explode("\n", $header); |
182
|
|
|
$result = []; |
183
|
|
|
foreach ($parts as $part) { |
184
|
|
|
if (strpos($part, ': ') === false) { |
185
|
|
|
if (substr($part, 0, 4) === 'HTTP') { |
186
|
|
|
if ($result) { |
187
|
|
|
$prev = $result; |
188
|
|
|
$result = []; |
189
|
|
|
$result['redirects'][] = $prev; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$regex = '#^HTTP/(\d\.\d) (\d{3})(.*)#'; |
193
|
|
|
if (preg_match($regex, $part, $matches)) { |
194
|
|
|
$result['Http-Version'] = $matches[1]; |
195
|
|
|
$result['Http-Code'] = $matches[2]; |
196
|
|
|
$result['Http-Message'] = $matches[3] ? trim($matches[3]) : ''; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
} else { |
200
|
|
|
list($key, $value) = explode(": ", $part); |
201
|
|
|
$result[$key] = trim($value); |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
return $result; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.