Completed
Branch master (33e285)
by Drew
02:46 queued 54s
created

MailChimp::prepareStateForRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 1
eloc 14
nc 1
nop 4
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
    const 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
     * @param string $api_key Your MailChimp API key
34
     * @param string $api_endpoint Optional custom API endpoint
35
     * @throws \Exception
36
     */
37
    public function __construct($api_key, $api_endpoint = null)
38
    {
39
        $this->api_key = $api_key;
40
        
41
        if ($api_endpoint === null) {
42
            if (strpos($this->api_key, '-') === false) {
43
                throw new \Exception("Invalid MailChimp API key `{$api_key}` supplied.");
44
            }
45
            list(, $data_center) = explode('-', $this->api_key);
46
            $this->api_endpoint  = str_replace('<dc>', $data_center, $this->api_endpoint);
47
        } else {
48
            $this->api_endpoint  = $api_endpoint;
49
        }
50
51
        $this->last_response = array('headers' => null, 'body' => null);
52
    }
53
54
    /**
55
     * Create a new instance of a Batch request. Optionally with the ID of an existing batch.
56
     * @param string $batch_id Optional ID of an existing batch, if you need to check its status for example.
57
     * @return Batch            New Batch object.
58
     */
59
    public function new_batch($batch_id = null)
60
    {
61
        return new Batch($this, $batch_id);
62
    }
63
64
    /**
65
     * @return string The url to the API endpoint
66
     */
67
    public function getApiEndpoint()
68
    {
69
        return $this->api_endpoint;
70
    }
71
72
73
    /**
74
     * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL
75
     * @param   string $email The subscriber's email address
76
     * @return  string          Hashed version of the input
77
     */
78
    public function subscriberHash($email)
79
    {
80
        return md5(strtolower($email));
81
    }
82
83
    /**
84
     * Was the last request successful?
85
     * @return bool  True for success, false for failure
86
     */
87
    public function success()
88
    {
89
        return $this->request_successful;
90
    }
91
92
    /**
93
     * Get the last error returned by either the network transport, or by the API.
94
     * If something didn't work, this should contain the string describing the problem.
95
     * @return  array|false  describing the error
96
     */
97
    public function getLastError()
98
    {
99
        return $this->last_error ?: false;
100
    }
101
102
    /**
103
     * Get an array containing the HTTP headers and the body of the API response.
104
     * @return array  Assoc array with keys 'headers' and 'body'
105
     */
106
    public function getLastResponse()
107
    {
108
        return $this->last_response;
109
    }
110
111
    /**
112
     * Get an array containing the HTTP headers and the body of the API request.
113
     * @return array  Assoc array
114
     */
115
    public function getLastRequest()
116
    {
117
        return $this->last_request;
118
    }
119
120
    /**
121
     * Make an HTTP DELETE request - for deleting data
122
     * @param   string $method URL of the API request method
123
     * @param   array $args Assoc array of arguments (if any)
124
     * @param   int $timeout Timeout limit for request in seconds
125
     * @return  array|false   Assoc array of API response, decoded from JSON
126
     */
127
    public function delete($method, $args = array(), $timeout = self::TIMEOUT)
128
    {
129
        return $this->makeRequest('delete', $method, $args, $timeout);
130
    }
131
132
    /**
133
     * Make an HTTP GET request - for retrieving data
134
     * @param   string $method URL of the API request method
135
     * @param   array $args Assoc array of arguments (usually your data)
136
     * @param   int $timeout Timeout limit for request in seconds
137
     * @return  array|false   Assoc array of API response, decoded from JSON
138
     */
139
    public function get($method, $args = array(), $timeout = self::TIMEOUT)
140
    {
141
        return $this->makeRequest('get', $method, $args, $timeout);
142
    }
143
144
    /**
145
     * Make an HTTP PATCH request - for performing partial updates
146
     * @param   string $method URL of the API request method
147
     * @param   array $args Assoc array of arguments (usually your data)
148
     * @param   int $timeout Timeout limit for request in seconds
149
     * @return  array|false   Assoc array of API response, decoded from JSON
150
     */
151
    public function patch($method, $args = array(), $timeout = self::TIMEOUT)
152
    {
153
        return $this->makeRequest('patch', $method, $args, $timeout);
154
    }
155
156
    /**
157
     * Make an HTTP POST request - for creating and updating items
158
     * @param   string $method URL of the API request method
159
     * @param   array $args Assoc array of arguments (usually your data)
160
     * @param   int $timeout Timeout limit for request in seconds
161
     * @return  array|false   Assoc array of API response, decoded from JSON
162
     */
163
    public function post($method, $args = array(), $timeout = self::TIMEOUT)
164
    {
165
        return $this->makeRequest('post', $method, $args, $timeout);
166
    }
167
168
    /**
169
     * Make an HTTP PUT request - for creating new items
170
     * @param   string $method URL of the API request method
171
     * @param   array $args Assoc array of arguments (usually your data)
172
     * @param   int $timeout Timeout limit for request in seconds
173
     * @return  array|false   Assoc array of API response, decoded from JSON
174
     */
175
    public function put($method, $args = array(), $timeout = self::TIMEOUT)
176
    {
177
        return $this->makeRequest('put', $method, $args, $timeout);
178
    }
179
180
    /**
181
     * Performs the underlying HTTP request. Not very exciting.
182
     * @param  string $http_verb The HTTP verb to use: get, post, put, patch, delete
183
     * @param  string $method The API method to be called
184
     * @param  array $args Assoc array of parameters to be passed
185
     * @param int $timeout
186
     * @return array|false Assoc array of decoded result
187
     * @throws \Exception
188
     */
189
    private function makeRequest($http_verb, $method, $args = array(), $timeout = self::TIMEOUT)
190
    {
191
        if (!function_exists('curl_init') || !function_exists('curl_setopt')) {
192
            throw new \Exception("cURL support is required, but can't be found.");
193
        }
194
195
        $url = $this->api_endpoint . '/' . $method;
196
197
        $response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout);
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
        $response['headers'] = curl_getinfo($ch);
244
        curl_close($ch);
245
246
        $response            = $this->setResponseState($response, $responseContent);
247
        $formattedResponse   = $this->formatResponse($response);
248
249
        $this->determineSuccess($response, $formattedResponse, $timeout);
250
251
        return $formattedResponse;
252
    }
253
254
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
255
    {
256
        $this->last_error = '';
257
        
258
        $this->request_successful = false;
259
260
        $this->last_response = array(
261
            'headers'     => null, // array of details from curl_getinfo()
262
            'httpHeaders' => null, // array of HTTP headers
263
            'body'        => null // content of the response
264
        );
265
266
        $this->last_request = array(
267
            'method'  => $http_verb,
268
            'path'    => $method,
269
            'url'     => $url,
270
            'body'    => '',
271
            'timeout' => $timeout,
272
        );
273
274
        return $this->last_response;
275
    }
276
277
    /**
278
     * Get the HTTP headers as an array of header-name => header-value pairs.
279
     * 
280
     * The "Link" header is parsed into an associative array based on the
281
     * rel names it contains. The original value is available under
282
     * the "_raw" key.
283
     * 
284
     * @param string $headersAsString
285
     * @return array
286
     */
287
    private function getHeadersAsArray($headersAsString)
288
    {
289
        $headers = array();
290
        
291
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
292
            if ($i === 0) { // HTTP code
293
                continue;
294
            }
295
            
296
            $line = trim($line);
297
            if (empty($line)) {
298
                continue;
299
            }
300
            
301
            list($key, $value) = explode(': ', $line);
302
            
303
            if ($key == 'Link') {
304
                $value = array_merge(
305
                    array('_raw' => $value),
306
                    $this->getLinkHeaderAsArray($value)
307
                );
308
            }
309
            
310
            $headers[$key] = $value;
311
        }
312
313
        return $headers;
314
    }
315
316
    /**
317
     * Extract all rel => URL pairs from the provided Link header value
318
     * 
319
     * Mailchimp only implements the URI reference and relation type from
320
     * RFC 5988, so the value of the header is something like this:
321
     * 
322
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy", <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
323
     * 
324
     * @param string $linkHeaderAsString
325
     * @return array
326
     */
327
    private function getLinkHeaderAsArray($linkHeaderAsString)
328
    {
329
        $urls = array();
330
        
331
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
332
            foreach ($matches[2] as $i => $relName) {
333
                $urls[$relName] = $matches[1][$i];
334
            }
335
        }
336
        
337
        return $urls;
338
    }
339
340
    /**
341
     * Encode the data and attach it to the request
342
     * @param   resource $ch cURL session handle, used by reference
343
     * @param   array $data Assoc array of data to attach
344
     */
345
    private function attachRequestPayload(&$ch, $data)
346
    {
347
        $encoded = json_encode($data);
348
        $this->last_request['body'] = $encoded;
349
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
350
    }
351
352
    /**
353
     * Decode the response and format any error messages for debugging
354
     * @param array $response The response from the curl request
355
     * @return array|false    The JSON decoded into an array
356
     */
357
    private function formatResponse($response)
358
    {
359
        $this->last_response = $response;
360
361
        if (!empty($response['body'])) {
362
            return json_decode($response['body'], true);
363
        }
364
365
        return false;
366
    }
367
368
    /**
369
     * Do post-request formatting and setting state from the response
370
     * @param array $response The response from the curl request
371
     * @param string $responseContent The body of the response from the curl request
372
     * * @return array    The modified response
373
     */
374
    private function setResponseState($response, $responseContent)
375
    {
376
        if ($responseContent === false) {
377
            $this->last_error = curl_error($ch);
0 ignored issues
show
Bug introduced by
The variable $ch does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
378
        } else {
379
        
380
            $headerSize = $response['headers']['header_size'];
381
            
382
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
383
            $response['body'] = substr($responseContent, $headerSize);
384
385
            if (isset($response['headers']['request_header'])) {
386
                $this->last_request['headers'] = $response['headers']['request_header'];
387
            }
388
        }
389
390
        return $response;
391
    }
392
393
    /**
394
     * Check if the response was successful or a failure. If it failed, store the error.
395
     * @param array $response The response from the curl request
396
     * @param array|false $formattedResponse The response body payload from the curl request
397
     * @param int $timeout The timeout supplied to the curl request.
398
     * @return bool     If the request was successful
399
     */
400
    private function determineSuccess($response, $formattedResponse, $timeout)
401
    {
402
        $status = $this->findHTTPStatus($response, $formattedResponse);
403
404
        if ($status >= 200 && $status <= 299) {
405
            $this->request_successful = true;
406
            return true;
407
        }
408
409
        if (isset($formattedResponse['detail'])) {
410
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
411
            return false;
412
        }
413
414
        if( $timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout ) {
415
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time'] );
416
            return false;
417
        }
418
419
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
420
        return false;
421
    }
422
423
    /**
424
     * Find the HTTP status code from the headers or API response body
425
     * @param array $response The response from the curl request
426
     * @param array|false $formattedResponse The response body payload from the curl request
427
     * @return int  HTTP status code
428
     */
429
    private function findHTTPStatus($response, $formattedResponse)
430
    {
431
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
432
            return (int) $response['headers']['http_code'];
433
        }
434
435
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
436
            return (int) $formattedResponse['status'];
437
        }
438
439
        return 418;
440
    }
441
}
442