Completed
Pull Request — master (#276)
by David
01:09
created

MailChimp::__construct()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9137
c 0
b 0
f 0
cc 6
nc 6
nop 3
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
 * @version 2.5
12
 */
13
class MailChimp
14
{
15
    private $api_key;
16
    private $api_endpoint = 'https://<dc>.api.mailchimp.com/3.0';
17
18
    private $timeout = 10;
19
20
    /*  SSL Verification
21
        Read before disabling:
22
        http://snippets.webaware.com.au/howto/stop-turning-off-curlopt_ssl_verifypeer-and-fix-your-php-config/
23
    */
24
    public $verify_ssl = true;
25
26
    private $request_successful = false;
27
    private $last_error         = '';
28
    private $last_response      = array();
29
    private $last_request       = array();
30
31
    /**
32
     * Create a new instance
33
     *
34
     * @param string $api_key      Your MailChimp API key
35
     * @param string $api_endpoint Optional custom API endpoint
36
     *
37
     * @throws \Exception
38
     */
39
    public function __construct($api_key, $api_endpoint = null, $parameters = array())
40
    {
41
        if (!function_exists('curl_init') || !function_exists('curl_setopt')) {
42
            throw new \Exception("cURL support is required, but can't be found.");
43
        }
44
45
        $this->api_key = $api_key;
46
47
        if ($api_endpoint === null) {
48
            if (strpos($this->api_key, '-') === false) {
49
                throw new \Exception("Invalid MailChimp API key supplied.");
50
            }
51
            list(, $data_center) = explode('-', $this->api_key);
52
            $this->api_endpoint = str_replace('<dc>', $data_center, $this->api_endpoint);
53
        } else {
54
            $this->api_endpoint = $api_endpoint;
55
        }
56
57
        if (isset($parameters['timeout'])) {
58
            $this->timeout = $parameters['timeout'];
59
        }
60
61
        $this->last_response = array('headers' => null, 'body' => null);
62
    }
63
64
    /**
65
     * Create a new instance of a Batch request. Optionally with the ID of an existing batch.
66
     *
67
     * @param string $batch_id Optional ID of an existing batch, if you need to check its status for example.
68
     *
69
     * @return Batch            New Batch object.
70
     */
71
    public function new_batch($batch_id = null)
72
    {
73
        return new Batch($this, $batch_id);
74
    }
75
76
    /**
77
     * @return string The url to the API endpoint
78
     */
79
    public function getApiEndpoint()
80
    {
81
        return $this->api_endpoint;
82
    }
83
84
85
    /**
86
     * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL
87
     *
88
     * @param   string $email The subscriber's email address
89
     *
90
     * @return  string          Hashed version of the input
91
     */
92
    public function subscriberHash($email)
93
    {
94
        return md5(strtolower($email));
95
    }
96
97
    /**
98
     * Was the last request successful?
99
     *
100
     * @return bool  True for success, false for failure
101
     */
102
    public function success()
103
    {
104
        return $this->request_successful;
105
    }
106
107
    /**
108
     * Get the last error returned by either the network transport, or by the API.
109
     * If something didn't work, this should contain the string describing the problem.
110
     *
111
     * @return  string|false  describing the error
112
     */
113
    public function getLastError()
114
    {
115
        return $this->last_error ?: false;
116
    }
117
118
    /**
119
     * Get an array containing the HTTP headers and the body of the API response.
120
     *
121
     * @return array  Assoc array with keys 'headers' and 'body'
122
     */
123
    public function getLastResponse()
124
    {
125
        return $this->last_response;
126
    }
127
128
    /**
129
     * Get an array containing the HTTP headers and the body of the API request.
130
     *
131
     * @return array  Assoc array
132
     */
133
    public function getLastRequest()
134
    {
135
        return $this->last_request;
136
    }
137
138
    /**
139
     * Make an HTTP DELETE request - for deleting data
140
     *
141
     * @param   string $method  URL of the API request method
142
     * @param   array  $args    Assoc array of arguments (if any)
143
     * @param   int    $timeout Timeout limit for request in seconds
144
     *
145
     * @return  array|false   Assoc array of API response, decoded from JSON
146
     */
147 View Code Duplication
    public function delete($method, $args = array(), $timeout = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
148
    {
149
        if ($timeout === 0) {
150
            $timeout = $this->timeout;
151
        }
152
153
        return $this->makeRequest('delete', $method, $args, $timeout);
154
    }
155
156
    /**
157
     * Make an HTTP GET request - for retrieving data
158
     *
159
     * @param   string $method  URL of the API request method
160
     * @param   array  $args    Assoc array of arguments (usually your data)
161
     * @param   int    $timeout Timeout limit for request in seconds
162
     *
163
     * @return  array|false   Assoc array of API response, decoded from JSON
164
     */
165 View Code Duplication
    public function get($method, $args = array(), $timeout = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
166
    {
167
        if ($timeout === 0) {
168
            $timeout = $this->timeout;
169
        }
170
171
        return $this->makeRequest('get', $method, $args, $timeout);
172
    }
173
174
    /**
175
     * Make an HTTP PATCH request - for performing partial updates
176
     *
177
     * @param   string $method  URL of the API request method
178
     * @param   array  $args    Assoc array of arguments (usually your data)
179
     * @param   int    $timeout Timeout limit for request in seconds
180
     *
181
     * @return  array|false   Assoc array of API response, decoded from JSON
182
     */
183 View Code Duplication
    public function patch($method, $args = array(), $timeout = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
    {
185
        if ($timeout === 0) {
186
            $timeout = $this->timeout;
187
        }
188
189
        return $this->makeRequest('patch', $method, $args, $timeout);
190
    }
191
192
    /**
193
     * Make an HTTP POST request - for creating and updating 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 View Code Duplication
    public function post($method, $args = array(), $timeout = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
    {
203
        if ($timeout === 0) {
204
            $timeout = $this->timeout;
205
        }
206
207
        return $this->makeRequest('post', $method, $args, $timeout);
208
    }
209
210
    /**
211
     * Make an HTTP PUT request - for creating new items
212
     *
213
     * @param   string $method  URL of the API request method
214
     * @param   array  $args    Assoc array of arguments (usually your data)
215
     * @param   int    $timeout Timeout limit for request in seconds
216
     *
217
     * @return  array|false   Assoc array of API response, decoded from JSON
218
     */
219 View Code Duplication
    public function put($method, $args = array(), $timeout = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
    {
221
        if ($timeout === 0) {
222
            $timeout = $this->timeout;
223
        }
224
        return $this->makeRequest('put', $method, $args, $timeout);
225
    }
226
227
    /**
228
     * Performs the underlying HTTP request. Not very exciting.
229
     *
230
     * @param  string $http_verb The HTTP verb to use: get, post, put, patch, delete
231
     * @param  string $method    The API method to be called
232
     * @param  array  $args      Assoc array of parameters to be passed
233
     * @param int     $timeout
234
     *
235
     * @return array|false Assoc array of decoded result
236
     */
237
    private function makeRequest($http_verb, $method, $args = array(), $timeout = 0)
238
    {
239
        if ($timeout === 0) {
240
            $timeout = $this->timeout;
241
        }
242
243
        $url = $this->api_endpoint . '/' . $method;
244
245
        $response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout);
246
247
        $httpHeader = array(
248
            'Accept: application/vnd.api+json',
249
            'Content-Type: application/vnd.api+json',
250
            'Authorization: apikey ' . $this->api_key
251
        );
252
253
        if (isset($args["language"])) {
254
            $httpHeader[] = "Accept-Language: " . $args["language"];
255
        }
256
257
        if ($http_verb === 'put') {
258
            $httpHeader[] = 'Allow: PUT, PATCH, POST';
259
        }
260
261
        $ch = curl_init();
262
        curl_setopt($ch, CURLOPT_URL, $url);
263
        curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader);
264
        curl_setopt($ch, CURLOPT_USERAGENT, 'DrewM/MailChimp-API/3.0 (github.com/drewm/mailchimp-api)');
265
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
266
        curl_setopt($ch, CURLOPT_VERBOSE, true);
267
        curl_setopt($ch, CURLOPT_HEADER, true);
268
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
269
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl);
270
        curl_setopt($ch, CURLOPT_ENCODING, '');
271
        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
272
273
        switch ($http_verb) {
274
            case 'post':
275
                curl_setopt($ch, CURLOPT_POST, true);
276
                $this->attachRequestPayload($ch, $args);
277
                break;
278
279
            case 'get':
280
                $query = http_build_query($args, '', '&');
281
                curl_setopt($ch, CURLOPT_URL, $url . '?' . $query);
282
                break;
283
284
            case 'delete':
285
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
286
                break;
287
288
            case 'patch':
289
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
290
                $this->attachRequestPayload($ch, $args);
291
                break;
292
293
            case 'put':
294
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
295
                $this->attachRequestPayload($ch, $args);
296
                break;
297
        }
298
299
        $responseContent     = curl_exec($ch);
300
        $response['headers'] = curl_getinfo($ch);
301
        $response            = $this->setResponseState($response, $responseContent, $ch);
302
        $formattedResponse   = $this->formatResponse($response);
303
304
        curl_close($ch);
305
306
        $isSuccess = $this->determineSuccess($response, $formattedResponse, $timeout);
307
308
        return is_array($formattedResponse) ? $formattedResponse : $isSuccess;
309
    }
310
311
    /**
312
     * @param string  $http_verb
313
     * @param string  $method
314
     * @param string  $url
315
     * @param integer $timeout
316
     *
317
     * @return array
318
     */
319
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
320
    {
321
        $this->last_error = '';
322
323
        $this->request_successful = false;
324
325
        $this->last_response = array(
326
            'headers'     => null, // array of details from curl_getinfo()
327
            'httpHeaders' => null, // array of HTTP headers
328
            'body'        => null // content of the response
329
        );
330
331
        $this->last_request = array(
332
            'method'  => $http_verb,
333
            'path'    => $method,
334
            'url'     => $url,
335
            'body'    => '',
336
            'timeout' => $timeout,
337
        );
338
339
        return $this->last_response;
340
    }
341
342
    /**
343
     * Get the HTTP headers as an array of header-name => header-value pairs.
344
     *
345
     * The "Link" header is parsed into an associative array based on the
346
     * rel names it contains. The original value is available under
347
     * the "_raw" key.
348
     *
349
     * @param string $headersAsString
350
     *
351
     * @return array
352
     */
353
    private function getHeadersAsArray($headersAsString)
354
    {
355
        $headers = array();
356
357
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
358
            if (preg_match('/HTTP\/[1-2]/', substr($line, 0, 7)) === 1) { // http code
359
                continue;
360
            }
361
362
            $line = trim($line);
363
            if (empty($line)) {
364
                continue;
365
            }
366
367
            list($key, $value) = explode(': ', $line);
368
369
            if ($key == 'Link') {
370
                $value = array_merge(
371
                    array('_raw' => $value),
372
                    $this->getLinkHeaderAsArray($value)
373
                );
374
            }
375
376
            $headers[$key] = $value;
377
        }
378
379
        return $headers;
380
    }
381
382
    /**
383
     * Extract all rel => URL pairs from the provided Link header value
384
     *
385
     * Mailchimp only implements the URI reference and relation type from
386
     * RFC 5988, so the value of the header is something like this:
387
     *
388
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy",
389
     * <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
390
     *
391
     * @param string $linkHeaderAsString
392
     *
393
     * @return array
394
     */
395
    private function getLinkHeaderAsArray($linkHeaderAsString)
396
    {
397
        $urls = array();
398
399
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
400
            foreach ($matches[2] as $i => $relName) {
401
                $urls[$relName] = $matches[1][$i];
402
            }
403
        }
404
405
        return $urls;
406
    }
407
408
    /**
409
     * Encode the data and attach it to the request
410
     *
411
     * @param   resource $ch   cURL session handle, used by reference
412
     * @param   array    $data Assoc array of data to attach
413
     */
414
    private function attachRequestPayload(&$ch, $data)
415
    {
416
        $encoded                    = json_encode($data);
417
        $this->last_request['body'] = $encoded;
418
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
419
    }
420
421
    /**
422
     * Decode the response and format any error messages for debugging
423
     *
424
     * @param array $response The response from the curl request
425
     *
426
     * @return array|false    The JSON decoded into an array
427
     */
428
    private function formatResponse($response)
429
    {
430
        $this->last_response = $response;
431
432
        if (!empty($response['body'])) {
433
            return json_decode($response['body'], true);
434
        }
435
436
        return false;
437
    }
438
439
    /**
440
     * Do post-request formatting and setting state from the response
441
     *
442
     * @param array    $response        The response from the curl request
443
     * @param string   $responseContent The body of the response from the curl request
444
     * @param resource $ch              The curl resource
445
     *
446
     * @return array    The modified response
447
     */
448
    private function setResponseState($response, $responseContent, $ch)
449
    {
450
        if ($responseContent === false) {
451
            $this->last_error = curl_error($ch);
452
        } else {
453
454
            $headerSize = $response['headers']['header_size'];
455
456
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
457
            $response['body']        = substr($responseContent, $headerSize);
458
459
            if (isset($response['headers']['request_header'])) {
460
                $this->last_request['headers'] = $response['headers']['request_header'];
461
            }
462
        }
463
464
        return $response;
465
    }
466
467
    /**
468
     * Check if the response was successful or a failure. If it failed, store the error.
469
     *
470
     * @param array       $response          The response from the curl request
471
     * @param array|false $formattedResponse The response body payload from the curl request
472
     * @param int         $timeout           The timeout supplied to the curl request.
473
     *
474
     * @return bool     If the request was successful
475
     */
476
    private function determineSuccess($response, $formattedResponse, $timeout)
477
    {
478
        $status = $this->findHTTPStatus($response, $formattedResponse);
479
480
        if ($status >= 200 && $status <= 299) {
481
            $this->request_successful = true;
482
            return true;
483
        }
484
485
        if (isset($formattedResponse['detail'])) {
486
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
487
            return false;
488
        }
489
490
        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
491
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
492
            return false;
493
        }
494
495
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
496
        return false;
497
    }
498
499
    /**
500
     * Find the HTTP status code from the headers or API response body
501
     *
502
     * @param array       $response          The response from the curl request
503
     * @param array|false $formattedResponse The response body payload from the curl request
504
     *
505
     * @return int  HTTP status code
506
     */
507
    private function findHTTPStatus($response, $formattedResponse)
508
    {
509
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
510
            return (int)$response['headers']['http_code'];
511
        }
512
513
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
514
            return (int)$formattedResponse['status'];
515
        }
516
517
        return 418;
518
    }
519
}
520