Completed
Pull Request — master (#289)
by
unknown
05:10
created

MailChimp::makeRequest()   C

Complexity

Conditions 10
Paths 96

Size

Total Lines 76

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 76
rs 6.6569
c 0
b 0
f 0
cc 10
nc 96
nop 4

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
namespace DrewM\MailChimp;
4
5
use Composer\CaBundle\CaBundle;
6
7
/**
8
 * Super-simple, minimum abstraction MailChimp API v3 wrapper
9
 * MailChimp API v3: http://developer.mailchimp.com
10
 * This wrapper: https://github.com/drewm/mailchimp-api
11
 *
12
 * @author  Drew McLellan <[email protected]>
13
 * @version 2.5
14
 */
15
class MailChimp
16
{
17
    private $api_key;
18
    private $api_endpoint = 'https://<dc>.api.mailchimp.com/3.0';
19
20
    const TIMEOUT = 10;
21
22
    /*  SSL Verification
23
        Read before disabling:
24
        http://snippets.webaware.com.au/howto/stop-turning-off-curlopt_ssl_verifypeer-and-fix-your-php-config/
25
    */
26
    public $verify_ssl = true;
27
28
    private $request_successful = false;
29
    private $last_error         = '';
30
    private $last_response      = array();
31
    private $last_request       = array();
32
33
    /**
34
     * Create a new instance
35
     *
36
     * @param string $api_key      Your MailChimp API key
37
     * @param string $api_endpoint Optional custom API endpoint
38
     *
39
     * @throws \Exception
40
     */
41
    public function __construct($api_key, $api_endpoint = null)
42
    {
43
        if (!function_exists('curl_init') || !function_exists('curl_setopt')) {
44
            throw new \Exception("cURL support is required, but can't be found.");
45
        }
46
47
        $this->api_key = $api_key;
48
49
        if ($api_endpoint === null) {
50
            if (strpos($this->api_key, '-') === false) {
51
                throw new \Exception("Invalid MailChimp API key supplied.");
52
            }
53
            list(, $data_center) = explode('-', $this->api_key);
54
            $this->api_endpoint = str_replace('<dc>', $data_center, $this->api_endpoint);
55
        } else {
56
            $this->api_endpoint = $api_endpoint;
57
        }
58
59
        $this->last_response = array('headers' => null, 'body' => null);
60
    }
61
62
    /**
63
     * Create a new instance of a Batch request. Optionally with the ID of an existing batch.
64
     *
65
     * @param string $batch_id Optional ID of an existing batch, if you need to check its status for example.
66
     *
67
     * @return Batch            New Batch object.
68
     */
69
    public function new_batch($batch_id = null)
70
    {
71
        return new Batch($this, $batch_id);
72
    }
73
74
    /**
75
     * @return string The url to the API endpoint
76
     */
77
    public function getApiEndpoint()
78
    {
79
        return $this->api_endpoint;
80
    }
81
82
83
    /**
84
     * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL
85
     *
86
     * @param   string $email The subscriber's email address
87
     *
88
     * @return  string          Hashed version of the input
89
     */
90
    public static function subscriberHash($email)
91
    {
92
        return md5(strtolower($email));
93
    }
94
95
    /**
96
     * Was the last request successful?
97
     *
98
     * @return bool  True for success, false for failure
99
     */
100
    public function success()
101
    {
102
        return $this->request_successful;
103
    }
104
105
    /**
106
     * Get the last error returned by either the network transport, or by the API.
107
     * If something didn't work, this should contain the string describing the problem.
108
     *
109
     * @return  string|false  describing the error
110
     */
111
    public function getLastError()
112
    {
113
        return $this->last_error ?: false;
114
    }
115
116
    /**
117
     * Get an array containing the HTTP headers and the body of the API response.
118
     *
119
     * @return array  Assoc array with keys 'headers' and 'body'
120
     */
121
    public function getLastResponse()
122
    {
123
        return $this->last_response;
124
    }
125
126
    /**
127
     * Get an array containing the HTTP headers and the body of the API request.
128
     *
129
     * @return array  Assoc array
130
     */
131
    public function getLastRequest()
132
    {
133
        return $this->last_request;
134
    }
135
136
    /**
137
     * Make an HTTP DELETE request - for deleting data
138
     *
139
     * @param   string $method  URL of the API request method
140
     * @param   array  $args    Assoc array of arguments (if any)
141
     * @param   int    $timeout Timeout limit for request in seconds
142
     *
143
     * @return  array|false   Assoc array of API response, decoded from JSON
144
     */
145
    public function delete($method, $args = array(), $timeout = self::TIMEOUT)
146
    {
147
        return $this->makeRequest('delete', $method, $args, $timeout);
148
    }
149
150
    /**
151
     * Make an HTTP GET request - for retrieving data
152
     *
153
     * @param   string $method  URL of the API request method
154
     * @param   array  $args    Assoc array of arguments (usually your data)
155
     * @param   int    $timeout Timeout limit for request in seconds
156
     *
157
     * @return  array|false   Assoc array of API response, decoded from JSON
158
     */
159
    public function get($method, $args = array(), $timeout = self::TIMEOUT)
160
    {
161
        return $this->makeRequest('get', $method, $args, $timeout);
162
    }
163
164
    /**
165
     * Make an HTTP PATCH request - for performing partial updates
166
     *
167
     * @param   string $method  URL of the API request method
168
     * @param   array  $args    Assoc array of arguments (usually your data)
169
     * @param   int    $timeout Timeout limit for request in seconds
170
     *
171
     * @return  array|false   Assoc array of API response, decoded from JSON
172
     */
173
    public function patch($method, $args = array(), $timeout = self::TIMEOUT)
174
    {
175
        return $this->makeRequest('patch', $method, $args, $timeout);
176
    }
177
178
    /**
179
     * Make an HTTP POST request - for creating and updating items
180
     *
181
     * @param   string $method  URL of the API request method
182
     * @param   array  $args    Assoc array of arguments (usually your data)
183
     * @param   int    $timeout Timeout limit for request in seconds
184
     *
185
     * @return  array|false   Assoc array of API response, decoded from JSON
186
     */
187
    public function post($method, $args = array(), $timeout = self::TIMEOUT)
188
    {
189
        return $this->makeRequest('post', $method, $args, $timeout);
190
    }
191
192
    /**
193
     * Make an HTTP PUT request - for creating new items
194
     *
195
     * @param   string $method  URL of the API request method
196
     * @param   array  $args    Assoc array of arguments (usually your data)
197
     * @param   int    $timeout Timeout limit for request in seconds
198
     *
199
     * @return  array|false   Assoc array of API response, decoded from JSON
200
     */
201
    public function put($method, $args = array(), $timeout = self::TIMEOUT)
202
    {
203
        return $this->makeRequest('put', $method, $args, $timeout);
204
    }
205
206
    /**
207
     * Performs the underlying HTTP request. Not very exciting.
208
     *
209
     * @param  string $http_verb The HTTP verb to use: get, post, put, patch, delete
210
     * @param  string $method    The API method to be called
211
     * @param  array  $args      Assoc array of parameters to be passed
212
     * @param int     $timeout
213
     *
214
     * @return array|false Assoc array of decoded result
215
     */
216
    private function makeRequest($http_verb, $method, $args = array(), $timeout = self::TIMEOUT)
217
    {
218
        $url = $this->api_endpoint . '/' . $method;
219
220
        $response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout);
221
222
        $httpHeader = array(
223
            'Accept: application/vnd.api+json',
224
            'Content-Type: application/vnd.api+json',
225
            'Authorization: apikey ' . $this->api_key
226
        );
227
228
        if (isset($args["language"])) {
229
            $httpHeader[] = "Accept-Language: " . $args["language"];
230
        }
231
232
        if ($http_verb === 'put') {
233
            $httpHeader[] = 'Allow: PUT, PATCH, POST';
234
        }
235
236
        $ch = curl_init();
237
        curl_setopt($ch, CURLOPT_URL, $url);
238
        curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader);
239
        curl_setopt($ch, CURLOPT_USERAGENT, 'DrewM/MailChimp-API/3.0 (github.com/drewm/mailchimp-api)');
240
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
241
        curl_setopt($ch, CURLOPT_VERBOSE, true);
242
        curl_setopt($ch, CURLOPT_HEADER, true);
243
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
244
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl);
245
        curl_setopt($ch, CURLOPT_ENCODING, '');
246
        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
247
248
	    $caPathOrFile = CaBundle::getSystemCaRootBundlePath();
249
	    if (is_dir($caPathOrFile)) {
250
		    curl_setopt($ch, CURLOPT_CAPATH, $caPathOrFile);
251
	    } else {
252
		    curl_setopt($ch, CURLOPT_CAINFO, $caPathOrFile);
253
	    }
254
255
        switch ($http_verb) {
256
            case 'post':
257
                curl_setopt($ch, CURLOPT_POST, true);
258
                $this->attachRequestPayload($ch, $args);
259
                break;
260
261
            case 'get':
262
                $query = http_build_query($args, '', '&');
263
                curl_setopt($ch, CURLOPT_URL, $url . '?' . $query);
264
                break;
265
266
            case 'delete':
267
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
268
                break;
269
270
            case 'patch':
271
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
272
                $this->attachRequestPayload($ch, $args);
273
                break;
274
275
            case 'put':
276
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
277
                $this->attachRequestPayload($ch, $args);
278
                break;
279
        }
280
281
        $responseContent     = curl_exec($ch);
282
        $response['headers'] = curl_getinfo($ch);
283
        $response            = $this->setResponseState($response, $responseContent, $ch);
284
        $formattedResponse   = $this->formatResponse($response);
285
286
        curl_close($ch);
287
288
        $isSuccess = $this->determineSuccess($response, $formattedResponse, $timeout);
289
290
        return is_array($formattedResponse) ? $formattedResponse : $isSuccess;
291
    }
292
293
    /**
294
     * @param string  $http_verb
295
     * @param string  $method
296
     * @param string  $url
297
     * @param integer $timeout
298
     *
299
     * @return array
300
     */
301
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
302
    {
303
        $this->last_error = '';
304
305
        $this->request_successful = false;
306
307
        $this->last_response = array(
308
            'headers'     => null, // array of details from curl_getinfo()
309
            'httpHeaders' => null, // array of HTTP headers
310
            'body'        => null // content of the response
311
        );
312
313
        $this->last_request = array(
314
            'method'  => $http_verb,
315
            'path'    => $method,
316
            'url'     => $url,
317
            'body'    => '',
318
            'timeout' => $timeout,
319
        );
320
321
        return $this->last_response;
322
    }
323
324
    /**
325
     * Get the HTTP headers as an array of header-name => header-value pairs.
326
     *
327
     * The "Link" header is parsed into an associative array based on the
328
     * rel names it contains. The original value is available under
329
     * the "_raw" key.
330
     *
331
     * @param string $headersAsString
332
     *
333
     * @return array
334
     */
335
    private function getHeadersAsArray($headersAsString)
336
    {
337
        $headers = array();
338
339
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
340
            if (preg_match('/HTTP\/[1-2]/', substr($line, 0, 7)) === 1) { // http code
341
                continue;
342
            }
343
344
            $line = trim($line);
345
            if (empty($line)) {
346
                continue;
347
            }
348
349
            list($key, $value) = explode(': ', $line);
350
351
            if ($key == 'Link') {
352
                $value = array_merge(
353
                    array('_raw' => $value),
354
                    $this->getLinkHeaderAsArray($value)
355
                );
356
            }
357
358
            $headers[$key] = $value;
359
        }
360
361
        return $headers;
362
    }
363
364
    /**
365
     * Extract all rel => URL pairs from the provided Link header value
366
     *
367
     * Mailchimp only implements the URI reference and relation type from
368
     * RFC 5988, so the value of the header is something like this:
369
     *
370
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy",
371
     * <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
372
     *
373
     * @param string $linkHeaderAsString
374
     *
375
     * @return array
376
     */
377
    private function getLinkHeaderAsArray($linkHeaderAsString)
378
    {
379
        $urls = array();
380
381
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
382
            foreach ($matches[2] as $i => $relName) {
383
                $urls[$relName] = $matches[1][$i];
384
            }
385
        }
386
387
        return $urls;
388
    }
389
390
    /**
391
     * Encode the data and attach it to the request
392
     *
393
     * @param   resource $ch   cURL session handle, used by reference
394
     * @param   array    $data Assoc array of data to attach
395
     */
396
    private function attachRequestPayload(&$ch, $data)
397
    {
398
        $encoded                    = json_encode($data);
399
        $this->last_request['body'] = $encoded;
400
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
401
    }
402
403
    /**
404
     * Decode the response and format any error messages for debugging
405
     *
406
     * @param array $response The response from the curl request
407
     *
408
     * @return array|false    The JSON decoded into an array
409
     */
410
    private function formatResponse($response)
411
    {
412
        $this->last_response = $response;
413
414
        if (!empty($response['body'])) {
415
            return json_decode($response['body'], true);
416
        }
417
418
        return false;
419
    }
420
421
    /**
422
     * Do post-request formatting and setting state from the response
423
     *
424
     * @param array    $response        The response from the curl request
425
     * @param string   $responseContent The body of the response from the curl request
426
     * @param resource $ch              The curl resource
427
     *
428
     * @return array    The modified response
429
     */
430
    private function setResponseState($response, $responseContent, $ch)
431
    {
432
        if ($responseContent === false) {
433
            $this->last_error = curl_error($ch);
434
        } else {
435
436
            $headerSize = $response['headers']['header_size'];
437
438
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
439
            $response['body']        = substr($responseContent, $headerSize);
440
441
            if (isset($response['headers']['request_header'])) {
442
                $this->last_request['headers'] = $response['headers']['request_header'];
443
            }
444
        }
445
446
        return $response;
447
    }
448
449
    /**
450
     * Check if the response was successful or a failure. If it failed, store the error.
451
     *
452
     * @param array       $response          The response from the curl request
453
     * @param array|false $formattedResponse The response body payload from the curl request
454
     * @param int         $timeout           The timeout supplied to the curl request.
455
     *
456
     * @return bool     If the request was successful
457
     */
458
    private function determineSuccess($response, $formattedResponse, $timeout)
459
    {
460
        $status = $this->findHTTPStatus($response, $formattedResponse);
461
462
        if ($status >= 200 && $status <= 299) {
463
            $this->request_successful = true;
464
            return true;
465
        }
466
467
        if (isset($formattedResponse['detail'])) {
468
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
469
            return false;
470
        }
471
472
        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
473
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
474
            return false;
475
        }
476
477
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
478
        return false;
479
    }
480
481
    /**
482
     * Find the HTTP status code from the headers or API response body
483
     *
484
     * @param array       $response          The response from the curl request
485
     * @param array|false $formattedResponse The response body payload from the curl request
486
     *
487
     * @return int  HTTP status code
488
     */
489
    private function findHTTPStatus($response, $formattedResponse)
490
    {
491
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
492
            return (int)$response['headers']['http_code'];
493
        }
494
495
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
496
            return (int)$formattedResponse['status'];
497
        }
498
499
        return 418;
500
    }
501
}
502