Completed
Pull Request — master (#17)
by Alexander
06:45
created

Remote::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 8
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 8
loc 8
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
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
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $body of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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