Completed
Pull Request — master (#243)
by Dominik
01:21
created

MailChimp::getLastResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace DrewM\MailChimp;
4
5
use GuzzleHttp\ClientInterface;
6
use GuzzleHttp\Psr7\Request;
7
use GuzzleHttp\Exception\RequestException;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
11
/**
12
 * Super-simple, minimum abstraction MailChimp API v3 wrapper
13
 * MailChimp API v3: http://developer.mailchimp.com
14
 * This wrapper: https://github.com/drewm/mailchimp-api
15
 *
16
 * @author  Drew McLellan <[email protected]>
17
 * @version 2.5
18
 */
19
class MailChimp
20
{
21
22
    /**
23
     * @var ClientInterface
24
     */
25
    private $client;
26
27
    private $api_key;
28
    private $api_endpoint = 'https://<dc>.api.mailchimp.com/3.0';
29
30
    const TIMEOUT = 10;
31
32
    private $request_successful = false;
33
    private $last_error         = '';
34
    private $last_response      = array();
35
    private $last_request       = array();
36
37
    /**
38
     * Create a new instance
39
     *
40
     * @param string $api_key      Your MailChimp API key
41
     * @param string $api_endpoint Optional custom API endpoint
42
     *
43
     * @throws \Exception
44
     */
45
    public function __construct(ClientInterface $client, $api_key, $api_endpoint = null)
46
    {
47
        $this->client = $client;
48
49
        $this->api_key = $api_key;
50
51
        if ($api_endpoint === null) {
52
            if (strpos($this->api_key, '-') === false) {
53
                throw new \Exception("Invalid MailChimp API key supplied.");
54
            }
55
            list(, $data_center) = explode('-', $this->api_key);
56
            $this->api_endpoint = str_replace('<dc>', $data_center, $this->api_endpoint);
57
        } else {
58
            $this->api_endpoint = $api_endpoint;
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
    public function delete($method, $args = array(), $timeout = self::TIMEOUT)
148
    {
149
        return $this->makeRequest('delete', $method, $args, $timeout);
150
    }
151
152
    /**
153
     * Make an HTTP GET request - for retrieving data
154
     *
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
     *
159
     * @return  array|false   Assoc array of API response, decoded from JSON
160
     */
161
    public function get($method, $args = array(), $timeout = self::TIMEOUT)
162
    {
163
        return $this->makeRequest('get', $method, $args, $timeout);
164
    }
165
166
    /**
167
     * Make an HTTP PATCH request - for performing partial updates
168
     *
169
     * @param   string $method  URL of the API request method
170
     * @param   array  $args    Assoc array of arguments (usually your data)
171
     * @param   int    $timeout Timeout limit for request in seconds
172
     *
173
     * @return  array|false   Assoc array of API response, decoded from JSON
174
     */
175
    public function patch($method, $args = array(), $timeout = self::TIMEOUT)
176
    {
177
        return $this->makeRequest('patch', $method, $args, $timeout);
178
    }
179
180
    /**
181
     * Make an HTTP POST request - for creating and updating items
182
     *
183
     * @param   string $method  URL of the API request method
184
     * @param   array  $args    Assoc array of arguments (usually your data)
185
     * @param   int    $timeout Timeout limit for request in seconds
186
     *
187
     * @return  array|false   Assoc array of API response, decoded from JSON
188
     */
189
    public function post($method, $args = array(), $timeout = self::TIMEOUT)
190
    {
191
        return $this->makeRequest('post', $method, $args, $timeout);
192
    }
193
194
    /**
195
     * Make an HTTP PUT request - for creating new items
196
     *
197
     * @param   string $method  URL of the API request method
198
     * @param   array  $args    Assoc array of arguments (usually your data)
199
     * @param   int    $timeout Timeout limit for request in seconds
200
     *
201
     * @return  array|false   Assoc array of API response, decoded from JSON
202
     */
203
    public function put($method, $args = array(), $timeout = self::TIMEOUT)
204
    {
205
        return $this->makeRequest('put', $method, $args, $timeout);
206
    }
207
208
    /**
209
     * Performs the underlying HTTP request. Not very exciting.
210
     *
211
     * @param  string $http_verb The HTTP verb to use: get, post, put, patch, delete
212
     * @param  string $method    The API method to be called
213
     * @param  array  $args      Assoc array of parameters to be passed
214
     * @param int     $timeout
215
     *
216
     * @return array|false Assoc array of decoded result
217
     */
218
    private function makeRequest($http_verb, $method, $args = array(), $timeout = self::TIMEOUT)
219
    {
220
        $resource = $method;
221
        $method = strtoupper($http_verb);
222
223
        $uri = $this->api_endpoint . '/' . $resource;
224
225
        $dummyResponse = $this->prepareStateForRequest($method, $resource, $uri, $timeout);
226
227
        $headers = [
228
            'Accept' => ['application/vnd.api+json'],
229
            'Content-Type' => ['application/vnd.api+json'],
230
            'Authorization' => ['apikey ' . $this->api_key],
231
        ];
232
233
        if (isset($args['language'])) {
234
            $headers['Accept-Language'] = [$args['language']];
235
        }
236
237
        $body = null;
238
239
        switch ($method) {
240
            case 'POST':
241
            case 'PATCH':
242
            case 'PUT':
243
                $body = json_encode($args);
244
                $this->last_request['body'] = $body;
245
                break;
246
            case 'GET':
247
                $uri .= '?' . http_build_query($args, '', '&');
248
                break;
249
        }
250
251
        $request = new Request($method, $uri, $headers, $body);
252
253
        $start = microtime(true);
254
255
        $errorMessage = '';
256
257
        try {
258
            $response = $this->client->send($request);
259
        } catch (RequestException $e) {
260
            $errorMessage = $e->getMessage();
261
            if (null === $response = $e->getResponse()) {
262
                throw $e;
263
            }
264
        }
265
266
        $end = microtime(true);
267
268
        $requestHeader = $this->getRequestHeader($request);
269
        $responseHeader = $this->getResponseHeader($response);
270
        $responseBody = $responseHeader . (string) $response->getBody();
271
272
        $dummyResponse['headers'] = [
273
            'http_code' => $response->getStatusCode(),
274
            'header_size' => strlen($responseHeader),
275
            'total_time' => $end - $start,
276
            'request_header' => $requestHeader
277
        ];
278
279
        $dummyResponse = $this->setResponseState(
280
            $dummyResponse,
281
            '' === $errorMessage ? $responseBody : false,
0 ignored issues
show
Security Bug introduced by
It seems like '' === $errorMessage ? $responseBody : false can also be of type false; however, DrewM\MailChimp\MailChimp::setResponseState() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
282
            $errorMessage
283
        );
284
285
        $formattedResponse = $this->formatResponse($dummyResponse);
286
287
        $isSuccess = $this->determineSuccess($dummyResponse, $formattedResponse, $timeout);
288
289
        return is_array($formattedResponse) ? $formattedResponse : $isSuccess;
290
    }
291
292
    /**
293
     * @param RequestInterface $request
294
     * @return string
295
     */
296 View Code Duplication
    private function getRequestHeader(RequestInterface $request): string
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...
297
    {
298
        $requestHeader = sprintf(
299
            "%s %s HTTP/%s\r\n",
300
            $request->getMethod(),
301
            $request->getRequestTarget(),
302
            $request->getProtocolVersion()
303
        );
304
305
        foreach ($request->getHeaders() as $headerName => $headerValues) {
306
            $requestHeader .= sprintf("%s: %s\r\n", $headerName, implode(', ', $headerValues));
307
        }
308
309
        $requestHeader .= "\r\n";
310
311
        return $requestHeader;
312
    }
313
314
    /**
315
     * @param ResponseInterface $response
316
     * @return string
317
     */
318 View Code Duplication
    private function getResponseHeader(ResponseInterface $response): string
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...
319
    {
320
        $responseHeader = sprintf(
321
            "HTTP/%s %s %s\r\n",
322
            $response->getProtocolVersion(),
323
            $response->getStatusCode(),
324
            $response->getReasonPhrase()
325
        );
326
327
        foreach ($response->getHeaders() as $headerName => $headerValues) {
328
            $responseHeader .= sprintf("%s: %s\r\n", $headerName, implode(', ', $headerValues));
329
        }
330
331
        $responseHeader .= "\r\n";
332
333
        return $responseHeader;
334
    }
335
336
    /**
337
     * @param string  $http_verb
338
     * @param string  $method
339
     * @param string  $url
340
     * @param integer $timeout
341
     *
342
     * @return array
343
     */
344
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
345
    {
346
        $this->last_error = '';
347
348
        $this->request_successful = false;
349
350
        $this->last_response = array(
351
            'headers'     => null, // array of details from curl_getinfo()
352
            'httpHeaders' => null, // array of HTTP headers
353
            'body'        => null // content of the response
354
        );
355
356
        $this->last_request = array(
357
            'method'  => $http_verb,
358
            'path'    => $method,
359
            'url'     => $url,
360
            'body'    => '',
361
            'timeout' => $timeout,
362
        );
363
364
        return $this->last_response;
365
    }
366
367
    /**
368
     * Get the HTTP headers as an array of header-name => header-value pairs.
369
     *
370
     * The "Link" header is parsed into an associative array based on the
371
     * rel names it contains. The original value is available under
372
     * the "_raw" key.
373
     *
374
     * @param string $headersAsString
375
     *
376
     * @return array
377
     */
378
    private function getHeadersAsArray($headersAsString)
379
    {
380
        $headers = array();
381
382
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
383
            if ($i === 0) { // HTTP code
384
                continue;
385
            }
386
387
            $line = trim($line);
388
            if (empty($line)) {
389
                continue;
390
            }
391
392
            list($key, $value) = explode(': ', $line);
393
394
            if ($key == 'Link') {
395
                $value = array_merge(
396
                    array('_raw' => $value),
397
                    $this->getLinkHeaderAsArray($value)
398
                );
399
            }
400
401
            $headers[$key] = $value;
402
        }
403
404
        return $headers;
405
    }
406
407
    /**
408
     * Extract all rel => URL pairs from the provided Link header value
409
     *
410
     * Mailchimp only implements the URI reference and relation type from
411
     * RFC 5988, so the value of the header is something like this:
412
     *
413
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy",
414
     * <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
415
     *
416
     * @param string $linkHeaderAsString
417
     *
418
     * @return array
419
     */
420
    private function getLinkHeaderAsArray($linkHeaderAsString)
421
    {
422
        $urls = array();
423
424
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
425
            foreach ($matches[2] as $i => $relName) {
426
                $urls[$relName] = $matches[1][$i];
427
            }
428
        }
429
430
        return $urls;
431
    }
432
433
    /**
434
     * Decode the response and format any error messages for debugging
435
     *
436
     * @param array $response The response from the curl request
437
     *
438
     * @return array|false    The JSON decoded into an array
439
     */
440
    private function formatResponse($response)
441
    {
442
        $this->last_response = $response;
443
444
        if (!empty($response['body'])) {
445
            return json_decode($response['body'], true);
446
        }
447
448
        return false;
449
    }
450
451
    /**
452
     * Do post-request formatting and setting state from the response
453
     *
454
     * @param array  $response        The response from the curl request
455
     * @param string $responseContent The body of the response from the curl request
456
     * @param string $errorMessage    The error message
457
     * @return array The modified response
458
     */
459
    private function setResponseState($response, $responseContent, $errorMessage)
460
    {
461
        if ($responseContent === false) {
462
            $this->last_error = $errorMessage;
463
        } else {
464
            $headerSize = $response['headers']['header_size'];
465
466
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
467
            $response['body']        = substr($responseContent, $headerSize);
468
469
            if (isset($response['headers']['request_header'])) {
470
                $this->last_request['headers'] = $response['headers']['request_header'];
471
            }
472
        }
473
474
        return $response;
475
    }
476
477
    /**
478
     * Check if the response was successful or a failure. If it failed, store the error.
479
     *
480
     * @param array       $response          The response from the curl request
481
     * @param array|false $formattedResponse The response body payload from the curl request
482
     * @param int         $timeout           The timeout supplied to the curl request.
483
     *
484
     * @return bool     If the request was successful
485
     */
486
    private function determineSuccess($response, $formattedResponse, $timeout)
487
    {
488
        $status = $this->findHTTPStatus($response, $formattedResponse);
489
490
        if ($status >= 200 && $status <= 299) {
491
            $this->request_successful = true;
492
            return true;
493
        }
494
495
        if (isset($formattedResponse['detail'])) {
496
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
497
            return false;
498
        }
499
500
        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
501
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
502
            return false;
503
        }
504
505
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
506
        return false;
507
    }
508
509
    /**
510
     * Find the HTTP status code from the headers or API response body
511
     *
512
     * @param array       $response          The response from the curl request
513
     * @param array|false $formattedResponse The response body payload from the curl request
514
     *
515
     * @return int  HTTP status code
516
     */
517
    private function findHTTPStatus($response, $formattedResponse)
518
    {
519
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
520
            return (int)$response['headers']['http_code'];
521
        }
522
523
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
524
            return (int)$formattedResponse['status'];
525
        }
526
527
        return 418;
528
    }
529
}
530