Passed
Push — master ( a1ca1b...704457 )
by Tobias
19:10 queued 17:29
created

AbstractCurl::setOptionsFromRequest()   C

Complexity

Conditions 15
Paths 152

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 15.0062

Importance

Changes 0
Metric Value
cc 15
eloc 34
c 0
b 0
f 0
nc 152
nop 2
dl 0
loc 57
ccs 32
cts 33
cp 0.9697
crap 15.0062
rs 5.4833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Buzz\Client;
6
7
use Buzz\Configuration\ParameterBag;
8
use Buzz\Exception\CallbackException;
9
use Buzz\Exception\ClientException;
10
use Buzz\Exception\NetworkException;
11
use Buzz\Exception\RequestException;
12
use Buzz\Message\HeaderConverter;
13
use Buzz\Message\ResponseBuilder;
14
use Psr\Http\Message\RequestInterface;
15
use Symfony\Component\OptionsResolver\OptionsResolver;
16
17
/**
18
 * Base client class with helpers for working with cURL.
19
 */
20
abstract class AbstractCurl extends AbstractClient
21
{
22
    private $handles = [];
23
24
    private $maxHandles = 5;
25
26 98
    protected function configureOptions(OptionsResolver $resolver): void
27
    {
28 98
        parent::configureOptions($resolver);
29
30 98
        $resolver->setDefault('curl', []);
31 98
        $resolver->setAllowedTypes('curl', ['array']);
32 98
        $resolver->setDefault('expose_curl_info', false);
33 98
        $resolver->setAllowedTypes('expose_curl_info', ['boolean']);
34 98
    }
35
36
    /**
37
     * Creates a new cURL resource.
38
     *
39
     * @return resource A new cURL resource
40
     *
41
     * @throws ClientException If unable to create a cURL resource
42
     */
43 129
    protected function createHandle()
44
    {
45 129
        $curl = $this->handles ? array_pop($this->handles) : curl_init();
46 129
        if (false === $curl) {
47
            throw new ClientException('Unable to create a new cURL handle');
48
        }
49
50 129
        return $curl;
51
    }
52
53
    /**
54
     * Release a cUrl resource. This function is from Guzzle.
55
     *
56
     * @param resource $curl
57
     */
58 129
    protected function releaseHandle($curl): void
59
    {
60 129
        if (\count($this->handles) >= $this->maxHandles) {
61
            curl_close($curl);
62
        } else {
63
            // Remove all callback functions as they can hold onto references
64
            // and are not cleaned up by curl_reset. Using curl_setopt_array
65
            // does not work for some reason, so removing each one
66
            // individually.
67 129
            curl_setopt($curl, CURLOPT_HEADERFUNCTION, null);
68 129
            curl_setopt($curl, CURLOPT_READFUNCTION, null);
69 129
            curl_setopt($curl, CURLOPT_WRITEFUNCTION, null);
70 129
            curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, null);
71 129
            curl_reset($curl);
72
73 129
            if (!\in_array($curl, $this->handles)) {
74 129
                $this->handles[] = $curl;
75
            }
76
        }
77 129
    }
78
79
    /**
80
     * Prepares a cURL resource to send a request.
81
     *
82
     * @param resource $curl
83
     */
84 129
    protected function prepare($curl, RequestInterface $request, ParameterBag $options): ResponseBuilder
85
    {
86 129
        if (\defined('CURLOPT_PROTOCOLS')) {
87 129
            curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
88 129
            curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
89
        }
90
91 129
        curl_setopt($curl, CURLOPT_HEADER, false);
92 129
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
93 129
        curl_setopt($curl, CURLOPT_FAILONERROR, false);
94
95 129
        $this->setOptionsFromParameterBag($curl, $options);
96 129
        $this->setOptionsFromRequest($curl, $request);
97
98 129
        $responseBuilder = new ResponseBuilder($this->responseFactory);
99
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, function ($ch, $data) use ($responseBuilder) {
100 117
            $str = trim($data);
101 117
            if ('' !== $str) {
102 117
                if (0 === strpos(strtolower($str), 'http/')) {
103 117
                    $responseBuilder->setStatus($str);
104
                } else {
105 117
                    $responseBuilder->addHeader($str);
106
                }
107
            }
108
109 117
            return \strlen($data);
110 129
        });
111
112
        curl_setopt($curl, CURLOPT_WRITEFUNCTION, function ($ch, $data) use ($responseBuilder) {
113 113
            return $responseBuilder->writeBody($data);
114 129
        });
115
116
        // apply additional options
117 129
        curl_setopt_array($curl, $options->get('curl'));
118
119 129
        return $responseBuilder;
120
    }
121
122
    /**
123
     * Sets options on a cURL resource based on a request.
124
     *
125
     * @param resource         $curl    A cURL resource
126
     * @param RequestInterface $request A request object
127
     */
128 129
    private function setOptionsFromRequest($curl, RequestInterface $request): void
129
    {
130
        $options = [
131 129
            CURLOPT_CUSTOMREQUEST => $request->getMethod(),
132 129
            CURLOPT_URL => $request->getUri()->__toString(),
133 129
            CURLOPT_HTTPHEADER => HeaderConverter::toBuzzHeaders($request->getHeaders()),
134
        ];
135
136 129
        if (0 !== $version = $this->getProtocolVersion($request)) {
137 129
            $options[CURLOPT_HTTP_VERSION] = $version;
138
        }
139
140 129
        if ($request->getUri()->getUserInfo()) {
141
            $options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
142
        }
143
144 129
        switch (strtoupper($request->getMethod())) {
145 129
            case 'HEAD':
146 4
                $options[CURLOPT_NOBODY] = true;
147
148 4
                break;
149
150 125
            case 'GET':
151 67
                $options[CURLOPT_HTTPGET] = true;
152
153 67
                break;
154
155 58
            case 'POST':
156 40
            case 'PUT':
157 29
            case 'DELETE':
158 18
            case 'PATCH':
159 15
            case 'OPTIONS':
160 54
                $body = $request->getBody();
161 54
                $bodySize = $body->getSize();
162 54
                if (0 !== $bodySize) {
163 38
                    if ($body->isSeekable()) {
164 38
                        $body->rewind();
165
                    }
166
167
                    // Message has non empty body.
168 38
                    if (null === $bodySize || $bodySize > 1024 * 1024) {
169
                        // Avoid full loading large or unknown size body into memory
170 2
                        $options[CURLOPT_UPLOAD] = true;
171 2
                        if (null !== $bodySize) {
172 2
                            $options[CURLOPT_INFILESIZE] = $bodySize;
173
                        }
174
                        $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
175 2
                            return $body->read($length);
176 2
                        };
177
                    } else {
178
                        // Small body can be loaded into memory
179 36
                        $options[CURLOPT_POSTFIELDS] = (string) $body;
180
                    }
181
                }
182
        }
183
184 129
        curl_setopt_array($curl, $options);
185 129
    }
186
187
    /**
188
     * @param resource $curl
189
     */
190 129
    private function setOptionsFromParameterBag($curl, ParameterBag $options): void
191
    {
192 129
        if (null !== $proxy = $options->get('proxy')) {
193
            curl_setopt($curl, CURLOPT_PROXY, $proxy);
194
        }
195
196 129
        $canFollow = !ini_get('safe_mode') && !ini_get('open_basedir') && $options->get('allow_redirects');
197 129
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $canFollow);
198 129
        curl_setopt($curl, CURLOPT_MAXREDIRS, $canFollow ? $options->get('max_redirects') : 0);
199 129
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $options->get('verify') ? 1 : 0);
200 129
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $options->get('verify') ? 2 : 0);
201 129
        if (0 < $options->get('timeout')) {
202 11
            curl_setopt($curl, CURLOPT_TIMEOUT, $options->get('timeout'));
203
        }
204 129
    }
205
206
    /**
207
     * @param resource $curl
208
     *
209
     * @throws NetworkException
210
     * @throws RequestException
211
     * @throws CallbackException
212
     */
213 129
    protected function parseError(RequestInterface $request, int $errno, $curl): void
214
    {
215
        switch ($errno) {
216 129
            case CURLE_OK:
217
                // All OK, create a response object
218 117
                break;
219 12
            case CURLE_COULDNT_RESOLVE_PROXY:
220 12
            case CURLE_COULDNT_RESOLVE_HOST:
221 6
            case CURLE_COULDNT_CONNECT:
222 6
            case CURLE_OPERATION_TIMEOUTED:
223 3
            case CURLE_SSL_CONNECT_ERROR:
224 9
                throw new NetworkException($request, curl_error($curl), $errno);
225 3
            case CURLE_ABORTED_BY_CALLBACK:
226 3
                throw new CallbackException($request, curl_error($curl), $errno);
227
            default:
228
                throw new RequestException($request, curl_error($curl), $errno);
229
        }
230 117
    }
231
232 129
    private function getProtocolVersion(RequestInterface $request): int
233
    {
234 129
        switch ($request->getProtocolVersion()) {
235 129
            case '1.0':
236 24
                return CURL_HTTP_VERSION_1_0;
237 105
            case '1.1':
238 105
                return CURL_HTTP_VERSION_1_1;
239
            case '2.0':
240
                if (\defined('CURL_HTTP_VERSION_2_0')) {
241
                    return CURL_HTTP_VERSION_2_0;
242
                }
243
244
                throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support');
245
            default:
246
                return 0;
247
        }
248
    }
249
}
250