AbstractCurl   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Test Coverage

Coverage 91.38%

Importance

Changes 0
Metric Value
eloc 110
dl 0
loc 227
ccs 106
cts 116
cp 0.9138
rs 8.64
c 0
b 0
f 0
wmc 47

8 Methods

Rating   Name   Duplication   Size   Complexity  
C setOptionsFromRequest() 0 57 15
A releaseHandle() 0 17 3
A createHandle() 0 8 3
A configureOptions() 0 8 1
A prepare() 0 36 4
A getProtocolVersion() 0 15 5
B parseError() 0 16 8
B setOptionsFromParameterBag() 0 13 8

How to fix   Complexity   

Complex Class

Complex classes like AbstractCurl often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractCurl, and based on these observations, apply Extract Interface, too.

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 129
        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 129
        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 2
                        $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