Completed
Pull Request — master (#236)
by
unknown
01:30
created

MailChimp   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 493
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 1
dl 0
loc 493
rs 6.8
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 20 5
A new_batch() 0 4 1
A getApiEndpoint() 0 4 1
A subscriberHash() 0 4 1
A success() 0 4 1
A getLastError() 0 4 2
A getErrorsArray() 0 4 1
A getLastResponse() 0 4 1
A getLastRequest() 0 4 1
A delete() 0 4 1
A get() 0 4 1
A patch() 0 4 1
A post() 0 4 1
A put() 0 4 1
C makeRequest() 0 66 8
A prepareStateForRequest() 0 22 1
B getHeadersAsArray() 0 28 5
A getLinkHeaderAsArray() 0 12 3
A attachRequestPayload() 0 6 1
A formatResponse() 0 10 2
A setResponseState() 0 18 3
C determineSuccess() 0 26 8
B findHTTPStatus() 0 12 5

How to fix   Complexity   

Complex Class

Complex classes like MailChimp 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 MailChimp, and based on these observations, apply Extract Interface, too.

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