Completed
Pull Request — master (#99)
by thomas
42:14 queued 39:21
created

RestClient   C

Complexity

Total Complexity 45

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 20

Test Coverage

Coverage 79.65%

Importance

Changes 0
Metric Value
dl 0
loc 332
ccs 90
cts 113
cp 0.7965
rs 5.0166
c 0
b 0
f 0
wmc 45
lcom 1
cbo 20

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A getGuzzleClient() 0 3 1
A setCurlDebugging() 0 5 1
A setVerboseErrors() 0 3 1
A setCurlDefaultOption() 0 5 1
A setProxy() 0 5 1
A get() 0 3 1
A post() 0 3 1
A put() 0 3 1
A delete() 0 3 1
A hasQueryValue() 0 17 4
F buildRequest() 0 46 11
A request() 0 6 1
A getRFC1123DateString() 0 4 1
D responseHandler() 0 39 17
B createGuzzleClient() 0 30 1

How to fix   Complexity   

Complex Class

Complex classes like RestClient 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 RestClient, and based on these observations, apply Extract Interface, too.

1
<?PHP
2
3
namespace Blocktrail\SDK\Connection;
4
5
use Blocktrail\SDK\Connection\Exceptions\BannedIP;
6
use GuzzleHttp\Client as Guzzle;
7
use GuzzleHttp\Handler\CurlHandler;
8
use GuzzleHttp\HandlerStack;
9
use GuzzleHttp\Psr7\Request;
10
use GuzzleHttp\Psr7\Uri;
11
use HttpSignatures\Context;
12
use Blocktrail\SDK\Blocktrail;
13
use Blocktrail\SDK\Connection\Exceptions\EndpointSpecificError;
14
use Blocktrail\SDK\Connection\Exceptions\GenericServerError;
15
use Blocktrail\SDK\Connection\Exceptions\ObjectNotFound;
16
use Blocktrail\SDK\Connection\Exceptions\UnknownEndpointSpecificError;
17
use Blocktrail\SDK\Connection\Exceptions\EmptyResponse;
18
use Blocktrail\SDK\Connection\Exceptions\InvalidCredentials;
19
use Blocktrail\SDK\Connection\Exceptions\MissingEndpoint;
20
use Blocktrail\SDK\Connection\Exceptions\GenericHTTPError;
21
use HttpSignatures\GuzzleHttpSignatures;
22
use Psr\Http\Message\ResponseInterface;
23
24
/**
25
 * Class RestClient
26
 *
27
 */
28
class RestClient {
29
30
    const AUTH_HTTP_SIG = 'http-signatures';
31
32
    /**
33
     * @var Guzzle
34
     */
35
    protected $guzzle;
36
37
    /**
38
     * @var string
39
     */
40
    protected $apiKey;
41
42
    /**
43
     * @var string
44
     */
45
    protected $apiEndpoint;
46
47
    /**
48
     * @var string
49
     */
50
    protected $apiVersion;
51
52
    /**
53
     * @var string
54
     */
55
    protected $apiSecret;
56
57
    /**
58
     * @var array
59
     */
60
    protected $options = [];
61
62
    /**
63
     * @var array
64
     */
65
    protected $curlOptions = [];
66
67
    /**
68
     * @var bool
69
     */
70
    protected $verboseErrors = false;
71
72 116
    public function __construct($apiEndpoint, $apiVersion, $apiKey, $apiSecret) {
73 116
        $this->apiEndpoint = $apiEndpoint;
74 116
        $this->apiVersion = $apiVersion;
75 116
        $this->apiKey = $apiKey;
76 116
        $this->apiSecret = $apiSecret;
77
78 116
        $this->guzzle = $this->createGuzzleClient();
79 116
    }
80
81
    /**
82
     * @param array $options
83
     * @param array $curlOptions
84
     * @return Guzzle
85
     */
86 116
    protected function createGuzzleClient(array $options = [], array $curlOptions = []) {
87 116
        $options = $options + $this->options;
88 116
        $curlOptions = $curlOptions + $this->curlOptions;
89
90 116
        $context = new Context([
91 116
            'keys' => [$this->apiKey => $this->apiSecret],
92 116
            'algorithm' => 'hmac-sha256',
93
            'headers' => ['(request-target)', 'Content-MD5', 'Date'],
94
        ]);
95
96 116
        $curlHandler = new CurlHandler($curlOptions);
97 116
        $handler = HandlerStack::create($curlHandler);
98 116
        $handler->push(GuzzleHttpSignatures::middlewareFromContext($context));
99
100 116
        return new Guzzle($options + array(
101 116
            'handler' => $handler,
102 116
            'base_uri' => $this->apiEndpoint,
103
            'headers' => array(
104
                'User-Agent' => Blocktrail::SDK_USER_AGENT . '/' . Blocktrail::SDK_VERSION,
105
            ),
106
            'http_errors' => false,
107 116
            'connect_timeout' => 3,
108 116
            'timeout' => 20.0, // tmp until we have a good matrix of all the requests and their expect min/max time
109
            'verify' => true,
110 116
            'proxy' => '',
111
            'debug' => false,
112
            'config' => array(),
113 116
            'auth' => '',
114
        ));
115
    }
116
117
    /**
118
     * @return Guzzle
119
     */
120
    public function getGuzzleClient() {
121
        return $this->guzzle;
122
    }
123
124
    /**
125
     * enable CURL debugging output
126
     *
127
     * @param   bool        $debug
128
     */
129
    public function setCurlDebugging($debug = true) {
130
        $this->options['debug'] = $debug;
131
132
        $this->guzzle = $this->createGuzzleClient();
133
    }
134
135
    /**
136
     * enable verbose errors
137
     *
138
     * @param   bool        $verboseErrors
139
     */
140
    public function setVerboseErrors($verboseErrors = true) {
141
        $this->verboseErrors = $verboseErrors;
142
    }
143
        
144
    /**
145
     * set cURL default option on Guzzle client
146
     * @param string    $key
147
     * @param bool      $value
148
     */
149
    public function setCurlDefaultOption($key, $value) {
150
        $this->curlOptions[$key] = $value;
151
152
        $this->guzzle = $this->createGuzzleClient();
153
    }
154
155
    /**
156
     * set the proxy config for Guzzle
157
     *
158
     * @param   $proxy
159
     */
160
    public function setProxy($proxy) {
161
        $this->options['proxy'] = $proxy;
162
163
        $this->guzzle = $this->createGuzzleClient();
164
    }
165
166
    /**
167
     * @param   string          $endpointUrl
168
     * @param   array           $queryString
169
     * @param   string          $auth           http-signatures to enable http-signature signing
170
     * @param   float           $timeout        timeout in seconds
171
     * @return  Response
172
     */
173 31
    public function get($endpointUrl, $queryString = null, $auth = null, $timeout = null) {
174 31
        return $this->request('GET', $endpointUrl, $queryString, null, $auth, null, $timeout);
175
    }
176
177
    /**
178
     * @param   string          $endpointUrl
179
     * @param   null            $queryString
180
     * @param   array|string    $postData
181
     * @param   string          $auth           http-signatures to enable http-signature signing
182
     * @param   float           $timeout        timeout in seconds
183
     * @return  Response
184
     */
185 23
    public function post($endpointUrl, $queryString = null, $postData = '', $auth = null, $timeout = null) {
186 23
        return $this->request('POST', $endpointUrl, $queryString, $postData, $auth, null, $timeout);
187
    }
188
189
    /**
190
     * @param   string          $endpointUrl
191
     * @param   null            $queryString
192
     * @param   array|string    $putData
193
     * @param   string          $auth           http-signatures to enable http-signature signing
194
     * @param   float           $timeout        timeout in seconds
195
     * @return  Response
196
     */
197 1
    public function put($endpointUrl, $queryString = null, $putData = '', $auth = null, $timeout = null) {
198 1
        return $this->request('PUT', $endpointUrl, $queryString, $putData, $auth, null, $timeout);
199
    }
200
201
    /**
202
     * @param   string          $endpointUrl
203
     * @param   null            $queryString
204
     * @param   array|string    $postData
205
     * @param   string          $auth           http-signatures to enable http-signature signing
206
     * @param   float           $timeout        timeout in seconds
207
     * @return  Response
208
     */
209 11
    public function delete($endpointUrl, $queryString = null, $postData = null, $auth = null, $timeout = null) {
210 11
        return $this->request('DELETE', $endpointUrl, $queryString, $postData, $auth, 'url', $timeout);
211
    }
212
213
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
214 33
    public static function hasQueryValue(Uri $uri, $key) {
215 33
        $current = $uri->getQuery();
216 33
        $key = strtr($key, self::$replaceQuery);
217
218 33
        if (!$current) {
219 33
            $result = [];
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
220
        } else {
221 20
            $result = [];
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
222 20
            foreach (explode('&', $current) as $part) {
223 20
                if (explode('=', $part)[0] === $key) {
224 20
                    return true;
225
                };
226
            }
227
        }
228
229 33
        return false;
230
    }
231
232
    /**
233
     * generic request executor
234
     *
235
     * @param   string          $method         GET, POST, PUT, DELETE
236
     * @param   string          $endpointUrl
237
     * @param   array           $queryString
238
     * @param   array|string    $body
239
     * @param   string          $auth           http-signatures to enable http-signature signing
240
     * @param   string          $contentMD5Mode body or url
241
     * @param   float           $timeout        timeout in seconds
242
     * @return Request
243
     */
244 33
    public function buildRequest($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $auth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $timeout is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
245 33
        if (is_null($contentMD5Mode)) {
246 33
            $contentMD5Mode = !is_null($body) ? 'body' : 'url';
247
        }
248
249 33
        $request = new Request($method, $this->apiEndpoint . $endpointUrl);
250 33
        $uri = $request->getUri();
251
252 33
        if ($queryString) {
253 20
            foreach ($queryString as $k => $v) {
254 20
                $uri = Uri::withQueryValue($uri, $k, $v);
255
            }
256
        }
257
258 33
        if (!self::hasQueryValue($uri, 'api_key')) {
0 ignored issues
show
Compatibility introduced by
$uri of type object<Psr\Http\Message\UriInterface> is not a sub-type of object<GuzzleHttp\Psr7\Uri>. It seems like you assume a concrete implementation of the interface Psr\Http\Message\UriInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
259 33
            $uri = Uri::withQueryValue($uri, 'api_key', $this->apiKey);
260
        }
261
262
        // normalize the query string the same way the server expects it
263
        /** @var Request $request */
264 33
        $request = $request->withUri($uri->withQuery(\Symfony\Component\HttpFoundation\Request::normalizeQueryString($uri->getQuery())));
265
266 33
        if (!$request->hasHeader('Date')) {
267 33
            $request = $request->withHeader('Date', $this->getRFC1123DateString());
268
        }
269
270 33
        if (!is_null($body)) {
271 23
            if (!$request->hasHeader('Content-Type')) {
272 23
                $request = $request->withHeader('Content-Type', 'application/json');
273
            }
274
275 23
            if (!is_string($body)) {
276 23
                $body = json_encode($body);
277
            }
278 23
            $request = $request->withBody(\GuzzleHttp\Psr7\stream_for($body));
279
        }
280
281
        // for GET/DELETE requests, MD5 the request URI (excludes domain, includes query strings)
282 33
        if ($contentMD5Mode == 'body') {
283 23
            $request = $request->withHeader('Content-MD5', md5((string)$body));
284
        } else {
285 31
            $request = $request->withHeader('Content-MD5', md5($request->getRequestTarget()));
286
        }
287
288 33
        return $request;
289
    }
290
291
    /**
292
     * generic request executor
293
     *
294
     * @param   string          $method         GET, POST, PUT, DELETE
295
     * @param   string          $endpointUrl
296
     * @param   array           $queryString
297
     * @param   array|string    $body
298
     * @param   string          $auth           http-signatures to enable http-signature signing
299
     * @param   string          $contentMD5Mode body or url
300
     * @param   float           $timeout        timeout in seconds
301
     * @return Response
302
     */
303 33
    public function request($method, $endpointUrl, $queryString = null, $body = null, $auth = null, $contentMD5Mode = null, $timeout = null) {
304 33
        $request = $this->buildRequest($method, $endpointUrl, $queryString, $body, $auth, $contentMD5Mode);
305 33
        $response = $this->guzzle->send($request, ['auth' => $auth, 'timeout' => $timeout]);
306
307 33
        return $this->responseHandler($response);
308
    }
309
310 33
    public function responseHandler(ResponseInterface $responseObj) {
311 33
        $httpResponseCode = (int)$responseObj->getStatusCode();
312 33
        $httpResponsePhrase = (string)$responseObj->getReasonPhrase();
313 33
        $body = $responseObj->getBody();
314
315 33
        if ($httpResponseCode == 200) {
316 33
            if (!$body) {
317
                throw new EmptyResponse(Blocktrail::EXCEPTION_EMPTY_RESPONSE, $httpResponseCode);
318
            }
319
320 33
            $result = new Response($httpResponseCode, $body);
321
322 33
            return $result;
323 11
        } elseif ($httpResponseCode == 400 || $httpResponseCode == 403) {
324 6
            $data = json_decode($body, true);
325
326 6
            if ($data && isset($data['msg'], $data['code'])) {
327 6
                throw new EndpointSpecificError(!is_string($data['msg']) ? json_encode($data['msg']) : $data['msg'], $data['code']);
328
            } else {
329
                if (preg_match("/^banned( IP)? \[(.+)\]\n?$/", $body, $m)) {
330
                    throw new BannedIP($m[2]);
331
                }
332
333
                throw new UnknownEndpointSpecificError($this->verboseErrors ? $body : Blocktrail::EXCEPTION_UNKNOWN_ENDPOINT_SPECIFIC_ERROR);
334
            }
335 11
        } elseif ($httpResponseCode == 401) {
336 1
            throw new InvalidCredentials($this->verboseErrors ? $body : Blocktrail::EXCEPTION_INVALID_CREDENTIALS, $httpResponseCode);
337 10
        } elseif ($httpResponseCode == 404) {
338 8
            if ($httpResponsePhrase == "Endpoint Not Found") {
339
                throw new MissingEndpoint($this->verboseErrors ? $body : Blocktrail::EXCEPTION_MISSING_ENDPOINT, $httpResponseCode);
340
            } else {
341 8
                throw new ObjectNotFound($this->verboseErrors ? $body : Blocktrail::EXCEPTION_OBJECT_NOT_FOUND, $httpResponseCode);
342
            }
343 2
        } elseif ($httpResponseCode == 500) {
344 2
            throw new GenericServerError(Blocktrail::EXCEPTION_GENERIC_SERVER_ERROR . "\nServer Response: " . $body, $httpResponseCode);
345
        } else {
346
            throw new GenericHTTPError(Blocktrail::EXCEPTION_GENERIC_HTTP_ERROR . "\nServer Response: " . $body, $httpResponseCode);
347
        }
348
    }
349
350
    /**
351
     * Returns curent fate time in RFC1123 format, using UTC time zone
352
     *
353
     * @return  string
354
     */
355 33
    private function getRFC1123DateString() {
356 33
        $date = new \DateTime(null, new \DateTimeZone("UTC"));
357 33
        return str_replace("+0000", "GMT", $date->format(\DateTime::RFC1123));
358
    }
359
}
360