Completed
Pull Request — master (#169)
by
unknown
06:19
created

MailChimp::getLastError()   A

Complexity

Conditions 2
Paths 2

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