Completed
Pull Request — master (#236)
by
unknown
03:51
created

MailChimp::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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
 * @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
        if ( array_key_exists('errors', $formattedResponse ) )
290
        {
291
        	$this->errors_array = $formattedResponse['errors'];
292
        }
293
294
        $isSuccess = $this->determineSuccess($response, $formattedResponse, $timeout);
295
296
        return is_array($formattedResponse) ? $formattedResponse : $isSuccess;
297
    }
298
299
    /**
300
     * @param string  $http_verb
301
     * @param string  $method
302
     * @param string  $url
303
     * @param integer $timeout
304
     *
305
     * @return array
306
     */
307
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
308
    {
309
        $this->last_error = '';
310
311
        $this->request_successful = false;
312
313
        $this->last_response = array(
314
            'headers'     => null, // array of details from curl_getinfo()
315
            'httpHeaders' => null, // array of HTTP headers
316
            'body'        => null // content of the response
317
        );
318
319
        $this->last_request = array(
320
            'method'  => $http_verb,
321
            'path'    => $method,
322
            'url'     => $url,
323
            'body'    => '',
324
            'timeout' => $timeout,
325
        );
326
327
        return $this->last_response;
328
    }
329
330
    /**
331
     * Get the HTTP headers as an array of header-name => header-value pairs.
332
     *
333
     * The "Link" header is parsed into an associative array based on the
334
     * rel names it contains. The original value is available under
335
     * the "_raw" key.
336
     *
337
     * @param string $headersAsString
338
     *
339
     * @return array
340
     */
341
    private function getHeadersAsArray($headersAsString)
342
    {
343
        $headers = array();
344
345
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
346
            if ($i === 0) { // HTTP code
347
                continue;
348
            }
349
350
            $line = trim($line);
351
            if (empty($line)) {
352
                continue;
353
            }
354
355
            list($key, $value) = explode(': ', $line);
356
357
            if ($key == 'Link') {
358
                $value = array_merge(
359
                    array('_raw' => $value),
360
                    $this->getLinkHeaderAsArray($value)
361
                );
362
            }
363
364
            $headers[$key] = $value;
365
        }
366
367
        return $headers;
368
    }
369
370
    /**
371
     * Extract all rel => URL pairs from the provided Link header value
372
     *
373
     * Mailchimp only implements the URI reference and relation type from
374
     * RFC 5988, so the value of the header is something like this:
375
     *
376
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy",
377
     * <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
378
     *
379
     * @param string $linkHeaderAsString
380
     *
381
     * @return array
382
     */
383
    private function getLinkHeaderAsArray($linkHeaderAsString)
384
    {
385
        $urls = array();
386
387
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
388
            foreach ($matches[2] as $i => $relName) {
389
                $urls[$relName] = $matches[1][$i];
390
            }
391
        }
392
393
        return $urls;
394
    }
395
396
    /**
397
     * Encode the data and attach it to the request
398
     *
399
     * @param   resource $ch   cURL session handle, used by reference
400
     * @param   array    $data Assoc array of data to attach
401
     */
402
    private function attachRequestPayload(&$ch, $data)
403
    {
404
        $encoded                    = json_encode($data);
405
        $this->last_request['body'] = $encoded;
406
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
407
    }
408
409
    /**
410
     * Decode the response and format any error messages for debugging
411
     *
412
     * @param array $response The response from the curl request
413
     *
414
     * @return array|false    The JSON decoded into an array
415
     */
416
    private function formatResponse($response)
417
    {
418
        $this->last_response = $response;
419
420
        if (!empty($response['body'])) {
421
            return json_decode($response['body'], true);
422
        }
423
424
        return false;
425
    }
426
427
    /**
428
     * Do post-request formatting and setting state from the response
429
     *
430
     * @param array    $response        The response from the curl request
431
     * @param string   $responseContent The body of the response from the curl request
432
     * @param resource $ch              The curl resource
433
     *
434
     * @return array    The modified response
435
     */
436
    private function setResponseState($response, $responseContent, $ch)
437
    {
438
        if ($responseContent === false) {
439
            $this->last_error = curl_error($ch);
440
        } else {
441
442
            $headerSize = $response['headers']['header_size'];
443
444
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
445
            $response['body']        = substr($responseContent, $headerSize);
446
447
            if (isset($response['headers']['request_header'])) {
448
                $this->last_request['headers'] = $response['headers']['request_header'];
449
            }
450
        }
451
452
        return $response;
453
    }
454
455
    /**
456
     * Check if the response was successful or a failure. If it failed, store the error.
457
     *
458
     * @param array       $response          The response from the curl request
459
     * @param array|false $formattedResponse The response body payload from the curl request
460
     * @param int         $timeout           The timeout supplied to the curl request.
461
     *
462
     * @return bool     If the request was successful
463
     */
464
    private function determineSuccess($response, $formattedResponse, $timeout)
465
    {
466
        $status = $this->findHTTPStatus($response, $formattedResponse);
467
468
        if ($status >= 200 && $status <= 299) {
469
            $this->request_successful = true;
470
            return true;
471
        }
472
473
        if (isset($formattedResponse['detail'])) {
474
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
475
            return false;
476
        }
477
478
        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
479
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
480
            return false;
481
        }
482
483
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
484
        return false;
485
    }
486
487
    /**
488
     * Find the HTTP status code from the headers or API response body
489
     *
490
     * @param array       $response          The response from the curl request
491
     * @param array|false $formattedResponse The response body payload from the curl request
492
     *
493
     * @return int  HTTP status code
494
     */
495
    private function findHTTPStatus($response, $formattedResponse)
496
    {
497
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
498
            return (int)$response['headers']['http_code'];
499
        }
500
501
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
502
            return (int)$formattedResponse['status'];
503
        }
504
505
        return 418;
506
    }
507
}
508