Passed
Branch master (507aab)
by Alexander
01:48
created

Remote::execute_curl()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 1
nop 1
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
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
    private $curl_options = [];
20
    private $curl_handler;
21
    // @var float
22
    private $start;
23
24
    public function __construct(array $curl_options = [], array $config = [])
25
    {
26
        $this->config = $config;
27
        $this->curl_options = [
28
                CURLOPT_FOLLOWLOCATION => true,
29
                CURLOPT_MAXREDIRS => 10,
30
                CURLOPT_CONNECTTIMEOUT => 10,
31
                CURLOPT_TIMEOUT => 10,
32
                CURLOPT_USERAGENT => 'alkemann-h2l-Remote-0.29',
33
                CURLOPT_RETURNTRANSFER => true,
34
                CURLOPT_HEADER => true,
35
            ] + $curl_options;
36
    }
37
38 View Code Duplication
    public function get(string $url, array $headers = []): Message
39
    {
40
        $request = (new Message)
41
            ->withType(Message::REQUEST)
42
            ->withUrl($url)
43
            ->withMethod(Request::GET)
44
            ->withHeaders($headers);
45
        return $this->http($request);
46
    }
47
48 View Code Duplication
    public function postJson(string $url, array $data, array $headers = [], string $method = Request::POST): Message
49
    {
50
        $headers['Content-Type'] = 'application/json; charset=utf-8';
51
        $headers['Accept'] = 'application/json';
52
        $data_string = json_encode($data);
53
        $headers['Content-Length'] = strlen($data_string);
54
        $request = (new Message)
55
            ->withType(Message::REQUEST)
56
            ->withUrl($url)
57
            ->withMethod($method)
58
            ->withBody($data_string)
59
            ->withHeaders($headers);
60
        return $this->http($request);
61
    }
62
63 View Code Duplication
    public function postForm(string $url, array $data, array $headers = [], string $method = Request::POST): Message
64
    {
65
        $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
66
        $data_string = http_build_query($data);
67
        $headers['Content-Length'] = strlen($data_string);
68
        $request = (new Message)
69
            ->withType(Message::REQUEST)
70
            ->withUrl($url)
71
            ->withMethod($method)
72
            ->withBody($data_string)
73
            ->withHeaders($headers);
74
        return $this->http($request);
75
    }
76
77 View Code Duplication
    public function delete(string $url, array $headers = []): Message
78
    {
79
        $request = (new Message)
80
            ->withType(Message::REQUEST)
81
            ->withUrl($url)
82
            ->withMethod(Request::DELETE)
83
            ->withHeaders($headers);
84
        return $this->http($request);
85
    }
86
87
    public function http(Message $request): Message
88
    {
89
        $this->createCurlHandlerFromRequest($request);
90
        $content = $this->execute_curl($request);
91
        $meta = curl_getinfo($this->curl_handler);
92
        $header_size = curl_getinfo($this->curl_handler, CURLINFO_HEADER_SIZE);
93
        $header = substr($content, 0, $header_size);
94
        $headers = $this->extractHeaders($header);
95
        $content = substr($content, $header_size);
96
        curl_close($this->curl_handler);
97
        unset($this->curl_handler);
98
        $meta['latency'] = microtime(true) - $this->start;
99
        return (new Message)
100
            ->withType(Message::RESPONSE)
101
            ->withUrl($request->url())
102
            ->withMethod($request->method())
103
            ->withBody($content)
104
            ->withCode($meta['http_code'])
105
            ->withHeaders($headers)
106
            ->withMeta($meta)
107
        ;
108
    }
109
110
    /**
111
     * @param Message $request
112
     */
113
    private function createCurlHandlerFromRequest(Message $request)
114
    {
115
        $this->start = microtime(true);
116
117
        $this->curl_handler = curl_init();
118
119
        $options = $this->curl_options;
120
        $options += [
121
            CURLOPT_URL => $request->url(),
122
            CURLOPT_CUSTOMREQUEST => $request->method(),
123
        ];
124
125
        if (!isset($options[CURLOPT_HTTPHEADER])) {
126
            $options[CURLOPT_HTTPHEADER] = [];
127
        }
128
        $body = $request->body();
129
        if ($body !== null) {
130
            $options[CURLOPT_POSTFIELDS] = $body;
131
        }
132
133
        foreach ($request->headers() as $header_name => $header_value) {
134
            $options[CURLOPT_HTTPHEADER][] = "{$header_name}: {$header_value}";
135
        }
136
137
        $headers_to_set_to_blank_if_not_set = [
138
            'Content-Type',
139
            'Expect',
140
            'Accept',
141
            'Accept-Encoding'
142
        ];
143
        foreach ($headers_to_set_to_blank_if_not_set as $name) {
144
            if ($request->header($name) === null) {
145
                $conf[CURLOPT_HTTPHEADER][] = "{$name}:";
146
            }
147
        }
148
149
        curl_setopt_array($this->curl_handler, $options);
150
    }
151
152
    /**
153
     * @param Message $request
154
     * @return string
155
     * @throws CurlFailure if curl_exec returns false or throws an Exception
156
     */
157
    private function execute_curl(Message $request): string
158
    {
159
        try {
160
            $content = curl_exec($this->curl_handler);
161
            if ($content === false) {
162
                throw new CurlFailure(curl_error($this->curl_handler), curl_errno($this->curl_handler));
163
            }
164
            return $content;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $content returns the type mixed which includes types incompatible with the type-hinted return string.
Loading history...
165
        } catch (\Exception $e) {
166
            Log::error("CURL exception : " . get_class($e) . " : " . $e->getMessage());
167
168
            $curl_failure = new CurlFailure($e->getMessage(), $e->getCode(), $e);
169
            $latency = microtime(true) - $this->start;
170
            $info = curl_getinfo($this->curl_handler);
171
            $curl_failure->setContext(compact('request', 'latency', 'info'));
172
173
            curl_close($this->curl_handler);
174
            unset($this->curl_handler);
175
176
            throw $curl_failure;
177
        }
178
    }
179
180
    private function extractHeaders(string $header): array
181
    {
182
        $parts = explode("\n", $header);
183
        $result = [];
184
        foreach ($parts as $part) {
185
            $part = trim($part);
186
            if (empty($part)) {
187
                continue;
188
            }
189
190
            if (strpos($part, ': ') !== false) {
191
                list($key, $value) = explode(": ", $part);
192
                $result[$key] = trim($value);
193
                continue;
194
            }
195
196
            $regex = '#^HTTP/(\d\.\d) (\d{3})(.*)#';
197
            if (preg_match($regex, $part, $matches)) {
198
                if ($result) {
199
                    $result = ['redirected' => $result];
200
                }
201
202
                $result['Http-Version'] = $matches[1];
203
                $result['Http-Code'] = $matches[2];
204
                $result['Http-Message'] = $matches[3] ? trim($matches[3]) : '';
205
            }
206
        }
207
        return $result;
208
    }
209
}
210