├── .env.test ├── .github └── contributing.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Batch.php ├── MailChimp.php └── Webhook.php /.env.test: -------------------------------------------------------------------------------- 1 | MC_API_KEY="api-key" 2 | MC_LIST_ID="42" 3 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | ## Submitting issues 2 | 3 | This is quite a popular project, but it's not my job, so please read the below before posting an issue. Thank you! 4 | 5 | - If you have high-level implementation questions about your project ("How do I add this to WordPress", "I've got a form that takes an email address...") you're best to ask somewhere like StackOverflow. 6 | - If you have purchased a commercial product or template that uses this code and now have a problem, *I'm not going to help you with it, sorry.* Talk to the person who took your money. None of it came to me. :smile: 7 | - If your question is about the MailChimp API itself, please check out the [MailChimp Guides](http://developer.mailchimp.com/documentation/mailchimp/guides/). This project doesn't handle any of that logic - we're just helping you form the requests. 8 | 9 | If, however, you think you've found a bug, or would like to discuss a change or improvement, feel free to raise an issue and we'll figure it out between us. 10 | 11 | ## Pull requests 12 | 13 | This is a fairly simple wrapper, but it has been made much better by contributions from those using it. If you'd like to suggest an improvement, please raise an issue to discuss it before making your pull request. 14 | 15 | Pull requests for bugs are more than welcome - please explain the bug you're trying to fix in the message. 16 | 17 | There are a small number of PHPUnit unit tests. To get up and running, copy `.env.example` to `.env` and add your API key details. Unit testing against an API is obviously a bit tricky, but I'd welcome any contributions to this. It would be great to have more test coverage. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Drew McLellan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MailChimp API 2 | ============= 3 | 4 | Super-simple, minimum abstraction MailChimp API v3 wrapper, in PHP. 5 | 6 | I hate complex wrappers. This lets you get from the MailChimp API docs to the code as directly as possible. 7 | 8 | Requires PHP 5.3 and a pulse. Abstraction is for chimps. 9 | 10 | [![Build Status](https://travis-ci.org/drewm/mailchimp-api.svg?branch=master)](https://travis-ci.org/drewm/mailchimp-api) 11 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/drewm/mailchimp-api/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/drewm/mailchimp-api/?branch=master) 12 | [![Packagist](https://img.shields.io/packagist/dt/drewm/mailchimp-api.svg?maxAge=2592000)](https://packagist.org/packages/drewm/mailchimp-api) 13 | 14 | Installation 15 | ------------ 16 | 17 | You can install mailchimp-api using Composer: 18 | 19 | ``` 20 | composer require drewm/mailchimp-api 21 | ``` 22 | 23 | You will then need to: 24 | * run ``composer install`` to get these dependencies added to your vendor directory 25 | * add the autoloader to your application with this line: ``require("vendor/autoload.php")`` 26 | 27 | Alternatively you can just download the `MailChimp.php` file and include it manually: 28 | 29 | ```php 30 | include('./MailChimp.php'); 31 | ``` 32 | 33 | If you wish to use the batch request or webhook interfaces, you'll also need to download and include the `Batch.php` or `Webhook.php` files: 34 | 35 | ```php 36 | include('./Batch.php'); 37 | include('./Webhook.php'); 38 | ``` 39 | 40 | These are optional. If you're not using batches or webhooks you can just skip them. You can always come back and add them later. 41 | 42 | Examples 43 | -------- 44 | 45 | Start by `use`-ing the class and creating an instance with your API key 46 | 47 | ```php 48 | use \DrewM\MailChimp\MailChimp; 49 | 50 | $MailChimp = new MailChimp('abc123abc123abc123abc123abc123-us1'); 51 | ``` 52 | 53 | Then, list all the mailing lists (with a `get` on the `lists` method) 54 | 55 | ```php 56 | $result = $MailChimp->get('lists'); 57 | 58 | print_r($result); 59 | ``` 60 | 61 | Subscribe someone to a list (with a `post` to the `lists/{listID}/members` method): 62 | 63 | ```php 64 | $list_id = 'b1234346'; 65 | 66 | $result = $MailChimp->post("lists/$list_id/members", [ 67 | 'email_address' => 'davy@example.com', 68 | 'status' => 'subscribed', 69 | ]); 70 | 71 | print_r($result); 72 | ``` 73 | 74 | Update a list member with more information (using `patch` to update): 75 | 76 | ```php 77 | $list_id = 'b1234346'; 78 | $subscriber_hash = MailChimp::subscriberHash('davy@example.com'); 79 | 80 | $result = $MailChimp->patch("lists/$list_id/members/$subscriber_hash", [ 81 | 'merge_fields' => ['FNAME'=>'Davy', 'LNAME'=>'Jones'], 82 | 'interests' => ['2s3a384h' => true], 83 | ]); 84 | 85 | print_r($result); 86 | ``` 87 | 88 | Remove a list member using the `delete` method: 89 | 90 | ```php 91 | $list_id = 'b1234346'; 92 | $subscriber_hash = MailChimp::subscriberHash('davy@example.com'); 93 | 94 | $MailChimp->delete("lists/$list_id/members/$subscriber_hash"); 95 | ``` 96 | 97 | Quickly test for a successful action with the `success()` method: 98 | 99 | ```php 100 | $list_id = 'b1234346'; 101 | 102 | $result = $MailChimp->post("lists/$list_id/members", [ 103 | 'email_address' => 'davy@example.com', 104 | 'status' => 'subscribed', 105 | ]); 106 | 107 | if ($MailChimp->success()) { 108 | print_r($result); 109 | } else { 110 | echo $MailChimp->getLastError(); 111 | } 112 | ``` 113 | 114 | Batch Operations 115 | ---------------- 116 | 117 | The MailChimp [Batch Operations](http://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-batch-operations/) functionality enables you to complete multiple operations with a single call. A good example is adding thousands of members to a list - you can perform this in one request rather than thousands. 118 | 119 | ```php 120 | use \DrewM\MailChimp\MailChimp; 121 | use \DrewM\MailChimp\Batch; 122 | 123 | $MailChimp = new MailChimp('abc123abc123abc123abc123abc123-us1'); 124 | $Batch = $MailChimp->new_batch(); 125 | ``` 126 | 127 | You can then make requests on the `Batch` object just as you would normally with the `MailChimp` object. The difference is that you need to set an ID for the operation as the first argument, and also that you won't get a response. The ID is used for finding the result of this request in the combined response from the batch operation. 128 | 129 | ```php 130 | $Batch->post("op1", "lists/$list_id/members", [ 131 | 'email_address' => 'micky@example.com', 132 | 'status' => 'subscribed', 133 | ]); 134 | 135 | $Batch->post("op2", "lists/$list_id/members", [ 136 | 'email_address' => 'michael@example.com', 137 | 'status' => 'subscribed', 138 | ]); 139 | 140 | $Batch->post("op3", "lists/$list_id/members", [ 141 | 'email_address' => 'peter@example.com', 142 | 'status' => 'subscribed', 143 | ]); 144 | ``` 145 | 146 | Once you've finished all the requests that should be in the batch, you need to execute it. 147 | 148 | ```php 149 | $result = $Batch->execute(); 150 | ``` 151 | 152 | The result includes a batch ID. At a later point, you can check the status of your batch: 153 | 154 | ```php 155 | $MailChimp->new_batch($batch_id); 156 | $result = $Batch->check_status(); 157 | ``` 158 | 159 | When your batch is finished, you can download the results from the URL given in the response. In the JSON, the result of each operation will be keyed by the ID you used as the first argument for the request. 160 | 161 | Webhooks 162 | -------- 163 | 164 | **Note:** Use of the Webhooks functionality requires at least PHP 5.4. 165 | 166 | MailChimp [webhooks](http://kb.mailchimp.com/integrations/other-integrations/how-to-set-up-webhooks) enable your code to be notified of changes to lists and campaigns. 167 | 168 | When you set up a webhook you specify a URL on your server for the data to be sent to. This wrapper's Webhook class helps you catch that incoming webhook in a tidy way. It uses a subscription model, with your code subscribing to whichever webhook events it wants to listen for. You provide a callback function that the webhook data is passed to. 169 | 170 | To listen for the `unsubscribe` webhook: 171 | 172 | ```php 173 | use \DrewM\MailChimp\Webhook; 174 | 175 | Webhook::subscribe('unsubscribe', function($data){ 176 | print_r($data); 177 | }); 178 | ``` 179 | 180 | At first glance the _subscribe/unsubscribe_ looks confusing - your code is subscribing to the MailChimp `unsubscribe` webhook event. The callback function is passed as single argument - an associative array containing the webhook data. 181 | 182 | If you'd rather just catch all webhooks and deal with them yourself, you can use: 183 | 184 | ```php 185 | use \DrewM\MailChimp\Webhook; 186 | 187 | $result = Webhook::receive(); 188 | print_r($result); 189 | ``` 190 | 191 | There doesn't appear to be any documentation for the content of the webhook data. It's helpful to use something like [ngrok](https://ngrok.com) for tunneling the webhooks to your development machine - you can then use its web interface to inspect what's been sent and to replay incoming webhooks while you debug your code. 192 | 193 | Troubleshooting 194 | --------------- 195 | 196 | To get the last error returned by either the HTTP client or by the API, use `getLastError()`: 197 | 198 | ```php 199 | echo $MailChimp->getLastError(); 200 | ``` 201 | 202 | For further debugging, you can inspect the headers and body of the response: 203 | 204 | ```php 205 | print_r($MailChimp->getLastResponse()); 206 | ``` 207 | 208 | If you suspect you're sending data in the wrong format, you can look at what was sent to MailChimp by the wrapper: 209 | 210 | ```php 211 | print_r($MailChimp->getLastRequest()); 212 | ``` 213 | 214 | If your server's CA root certificates are not up to date you may find that SSL verification fails and you don't get a response. The correction solution for this [is not to disable SSL verification](http://snippets.webaware.com.au/howto/stop-turning-off-curlopt_ssl_verifypeer-and-fix-your-php-config/). The solution is to update your certificates. If you can't do that, there's an option at the top of the class file. Please don't just switch it off without at least attempting to update your certs -- that's lazy and dangerous. You're not a lazy, dangerous developer are you? 215 | 216 | If you have **high-level implementation questions about your project** ("How do I add this to WordPress", "I've got a form that takes an email address...") please **take them to somewhere like StackOverflow**. If you think you've found a bug, or would like to discuss a change or improvement, feel free to raise an issue and we'll figure it out between us. 217 | 218 | Contributing 219 | ------------ 220 | 221 | This is a fairly simple wrapper, but it has been made much better by contributions from those using it. If you'd like to suggest an improvement, please raise an issue to discuss it before making your pull request. 222 | 223 | Pull requests for bugs are more than welcome - please explain the bug you're trying to fix in the message. 224 | 225 | There are a small number of PHPUnit unit tests. Unit testing against an API is obviously a bit tricky, but I'd welcome any contributions to this. It would be great to have more test coverage. 226 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drewm/mailchimp-api", 3 | "description": "Super-simple, minimum abstraction MailChimp API v3 wrapper", 4 | "license": "MIT", 5 | "require": { 6 | "php": ">=5.3", 7 | "ext-curl": "*", 8 | "ext-json": "*" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "7.0.*", 12 | "vlucas/phpdotenv": "^2.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "DrewM\\MailChimp\\": "src" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "DrewM\\MailChimp\\Tests\\": "tests/" 22 | } 23 | }, 24 | "homepage": "https://github.com/drewm/mailchimp-api", 25 | "authors": [ 26 | { 27 | "name": "Drew McLellan", 28 | "email": "drew.mclellan@gmail.com", 29 | "homepage": "http://allinthehead.com/" 30 | 31 | } 32 | ] 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Batch.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Batch 12 | { 13 | private $MailChimp; 14 | 15 | private $operations = array(); 16 | private $batch_id; 17 | 18 | public function __construct(MailChimp $MailChimp, $batch_id = null) 19 | { 20 | $this->MailChimp = $MailChimp; 21 | $this->batch_id = $batch_id; 22 | } 23 | 24 | /** 25 | * Add an HTTP DELETE request operation to the batch - for deleting data 26 | * 27 | * @param string $id ID for the operation within the batch 28 | * @param string $method URL of the API request method 29 | * 30 | * @return void 31 | */ 32 | public function delete($id, $method) 33 | { 34 | $this->queueOperation('DELETE', $id, $method); 35 | } 36 | 37 | /** 38 | * Add an HTTP GET request operation to the batch - for retrieving data 39 | * 40 | * @param string $id ID for the operation within the batch 41 | * @param string $method URL of the API request method 42 | * @param array $args Assoc array of arguments (usually your data) 43 | * 44 | * @return void 45 | */ 46 | public function get($id, $method, $args = array()) 47 | { 48 | $this->queueOperation('GET', $id, $method, $args); 49 | } 50 | 51 | /** 52 | * Add an HTTP PATCH request operation to the batch - for performing partial updates 53 | * 54 | * @param string $id ID for the operation within the batch 55 | * @param string $method URL of the API request method 56 | * @param array $args Assoc array of arguments (usually your data) 57 | * 58 | * @return void 59 | */ 60 | public function patch($id, $method, $args = array()) 61 | { 62 | $this->queueOperation('PATCH', $id, $method, $args); 63 | } 64 | 65 | /** 66 | * Add an HTTP POST request operation to the batch - for creating and updating items 67 | * 68 | * @param string $id ID for the operation within the batch 69 | * @param string $method URL of the API request method 70 | * @param array $args Assoc array of arguments (usually your data) 71 | * 72 | * @return void 73 | */ 74 | public function post($id, $method, $args = array()) 75 | { 76 | $this->queueOperation('POST', $id, $method, $args); 77 | } 78 | 79 | /** 80 | * Add an HTTP PUT request operation to the batch - for creating new items 81 | * 82 | * @param string $id ID for the operation within the batch 83 | * @param string $method URL of the API request method 84 | * @param array $args Assoc array of arguments (usually your data) 85 | * 86 | * @return void 87 | */ 88 | public function put($id, $method, $args = array()) 89 | { 90 | $this->queueOperation('PUT', $id, $method, $args); 91 | } 92 | 93 | /** 94 | * Execute the batch request 95 | * 96 | * @param int $timeout Request timeout in seconds (optional) 97 | * 98 | * @return array|false Assoc array of API response, decoded from JSON 99 | */ 100 | public function execute($timeout = 10) 101 | { 102 | $req = array('operations' => $this->operations); 103 | 104 | $result = $this->MailChimp->post('batches', $req, $timeout); 105 | 106 | if ($result && isset($result['id'])) { 107 | $this->batch_id = $result['id']; 108 | } 109 | 110 | return $result; 111 | } 112 | 113 | /** 114 | * Check the status of a batch request. If the current instance of the Batch object 115 | * was used to make the request, the batch_id is already known and is therefore optional. 116 | * 117 | * @param string $batch_id ID of the batch about which to enquire 118 | * 119 | * @return array|false Assoc array of API response, decoded from JSON 120 | */ 121 | public function check_status($batch_id = null) 122 | { 123 | if ($batch_id === null && $this->batch_id) { 124 | $batch_id = $this->batch_id; 125 | } 126 | 127 | return $this->MailChimp->get('batches/' . $batch_id); 128 | } 129 | 130 | /** 131 | * Get operations 132 | * 133 | * @return array 134 | */ 135 | public function get_operations() 136 | { 137 | return $this->operations; 138 | } 139 | 140 | /** 141 | * Add an operation to the internal queue. 142 | * 143 | * @param string $http_verb GET, POST, PUT, PATCH or DELETE 144 | * @param string $id ID for the operation within the batch 145 | * @param string $method URL of the API request method 146 | * @param array $args Assoc array of arguments (usually your data) 147 | * 148 | * @return void 149 | */ 150 | private function queueOperation($http_verb, $id, $method, $args = null) 151 | { 152 | $operation = array( 153 | 'operation_id' => $id, 154 | 'method' => $http_verb, 155 | 'path' => $method, 156 | ); 157 | 158 | if ($args) { 159 | if ($http_verb == 'GET') { 160 | $key = 'params'; 161 | $operation[$key] = $args; 162 | } else { 163 | $key = 'body'; 164 | $operation[$key] = json_encode($args); 165 | } 166 | } 167 | 168 | $this->operations[] = $operation; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/MailChimp.php: -------------------------------------------------------------------------------- 1 | 11 | * @version 2.5 12 | */ 13 | class MailChimp 14 | { 15 | private $api_key; 16 | private $api_endpoint = 'https://.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 | * 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) 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('', $data_center, $this->api_endpoint); 53 | } else { 54 | $this->api_endpoint = $api_endpoint; 55 | } 56 | 57 | $this->last_response = array('headers' => null, 'body' => null); 58 | } 59 | 60 | /** 61 | * Create a new instance of a Batch request. Optionally with the ID of an existing batch. 62 | * 63 | * @param string $batch_id Optional ID of an existing batch, if you need to check its status for example. 64 | * 65 | * @return Batch New Batch object. 66 | */ 67 | public function new_batch($batch_id = null) 68 | { 69 | return new Batch($this, $batch_id); 70 | } 71 | 72 | /** 73 | * @return string The url to the API endpoint 74 | */ 75 | public function getApiEndpoint() 76 | { 77 | return $this->api_endpoint; 78 | } 79 | 80 | 81 | /** 82 | * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL 83 | * 84 | * @param string $email The subscriber's email address 85 | * 86 | * @return string Hashed version of the input 87 | */ 88 | public static function subscriberHash($email) 89 | { 90 | return md5(strtolower($email)); 91 | } 92 | 93 | /** 94 | * Was the last request successful? 95 | * 96 | * @return bool True for success, false for failure 97 | */ 98 | public function success() 99 | { 100 | return $this->request_successful; 101 | } 102 | 103 | /** 104 | * Get the last error returned by either the network transport, or by the API. 105 | * If something didn't work, this should contain the string describing the problem. 106 | * 107 | * @return string|false describing the error 108 | */ 109 | public function getLastError() 110 | { 111 | return $this->last_error ?: false; 112 | } 113 | 114 | /** 115 | * Get an array containing the HTTP headers and the body of the API response. 116 | * 117 | * @return array Assoc array with keys 'headers' and 'body' 118 | */ 119 | public function getLastResponse() 120 | { 121 | return $this->last_response; 122 | } 123 | 124 | /** 125 | * Get an array containing the HTTP headers and the body of the API request. 126 | * 127 | * @return array Assoc array 128 | */ 129 | public function getLastRequest() 130 | { 131 | return $this->last_request; 132 | } 133 | 134 | /** 135 | * Make an HTTP DELETE request - for deleting data 136 | * 137 | * @param string $method URL of the API request method 138 | * @param array $args Assoc array of arguments (if any) 139 | * @param int $timeout Timeout limit for request in seconds 140 | * 141 | * @return array|false Assoc array of API response, decoded from JSON 142 | */ 143 | public function delete($method, $args = array(), $timeout = self::TIMEOUT) 144 | { 145 | return $this->makeRequest('delete', $method, $args, $timeout); 146 | } 147 | 148 | /** 149 | * Make an HTTP GET request - for retrieving data 150 | * 151 | * @param string $method URL of the API request method 152 | * @param array $args Assoc array of arguments (usually your data) 153 | * @param int $timeout Timeout limit for request in seconds 154 | * 155 | * @return array|false Assoc array of API response, decoded from JSON 156 | */ 157 | public function get($method, $args = array(), $timeout = self::TIMEOUT) 158 | { 159 | return $this->makeRequest('get', $method, $args, $timeout); 160 | } 161 | 162 | /** 163 | * Make an HTTP PATCH request - for performing partial updates 164 | * 165 | * @param string $method URL of the API request method 166 | * @param array $args Assoc array of arguments (usually your data) 167 | * @param int $timeout Timeout limit for request in seconds 168 | * 169 | * @return array|false Assoc array of API response, decoded from JSON 170 | */ 171 | public function patch($method, $args = array(), $timeout = self::TIMEOUT) 172 | { 173 | return $this->makeRequest('patch', $method, $args, $timeout); 174 | } 175 | 176 | /** 177 | * Make an HTTP POST request - for creating and updating items 178 | * 179 | * @param string $method URL of the API request method 180 | * @param array $args Assoc array of arguments (usually your data) 181 | * @param int $timeout Timeout limit for request in seconds 182 | * 183 | * @return array|false Assoc array of API response, decoded from JSON 184 | */ 185 | public function post($method, $args = array(), $timeout = self::TIMEOUT) 186 | { 187 | return $this->makeRequest('post', $method, $args, $timeout); 188 | } 189 | 190 | /** 191 | * Make an HTTP PUT request - for creating new items 192 | * 193 | * @param string $method URL of the API request method 194 | * @param array $args Assoc array of arguments (usually your data) 195 | * @param int $timeout Timeout limit for request in seconds 196 | * 197 | * @return array|false Assoc array of API response, decoded from JSON 198 | */ 199 | public function put($method, $args = array(), $timeout = self::TIMEOUT) 200 | { 201 | return $this->makeRequest('put', $method, $args, $timeout); 202 | } 203 | 204 | /** 205 | * Performs the underlying HTTP request. Not very exciting. 206 | * 207 | * @param string $http_verb The HTTP verb to use: get, post, put, patch, delete 208 | * @param string $method The API method to be called 209 | * @param array $args Assoc array of parameters to be passed 210 | * @param int $timeout 211 | * 212 | * @return array|false Assoc array of decoded result 213 | */ 214 | private function makeRequest($http_verb, $method, $args = array(), $timeout = self::TIMEOUT) 215 | { 216 | $url = $this->api_endpoint . '/' . $method; 217 | 218 | $response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout); 219 | 220 | $httpHeader = array( 221 | 'Accept: application/vnd.api+json', 222 | 'Content-Type: application/vnd.api+json', 223 | 'Authorization: apikey ' . $this->api_key 224 | ); 225 | 226 | if (isset($args["language"])) { 227 | $httpHeader[] = "Accept-Language: " . $args["language"]; 228 | } 229 | 230 | if ($http_verb === 'put') { 231 | $httpHeader[] = 'Allow: PUT, PATCH, POST'; 232 | } 233 | 234 | $ch = curl_init(); 235 | curl_setopt($ch, CURLOPT_URL, $url); 236 | curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader); 237 | curl_setopt($ch, CURLOPT_USERAGENT, 'DrewM/MailChimp-API/3.0 (github.com/drewm/mailchimp-api)'); 238 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 239 | curl_setopt($ch, CURLOPT_VERBOSE, true); 240 | curl_setopt($ch, CURLOPT_HEADER, true); 241 | curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); 242 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl); 243 | curl_setopt($ch, CURLOPT_ENCODING, ''); 244 | curl_setopt($ch, CURLINFO_HEADER_OUT, true); 245 | 246 | switch ($http_verb) { 247 | case 'post': 248 | curl_setopt($ch, CURLOPT_POST, true); 249 | $this->attachRequestPayload($ch, $args); 250 | break; 251 | 252 | case 'get': 253 | $query = http_build_query($args, '', '&'); 254 | curl_setopt($ch, CURLOPT_URL, $url . '?' . $query); 255 | break; 256 | 257 | case 'delete': 258 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 259 | break; 260 | 261 | case 'patch': 262 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); 263 | $this->attachRequestPayload($ch, $args); 264 | break; 265 | 266 | case 'put': 267 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); 268 | $this->attachRequestPayload($ch, $args); 269 | break; 270 | } 271 | 272 | $responseContent = curl_exec($ch); 273 | $response['headers'] = curl_getinfo($ch); 274 | $response = $this->setResponseState($response, $responseContent, $ch); 275 | $formattedResponse = $this->formatResponse($response); 276 | 277 | curl_close($ch); 278 | 279 | $isSuccess = $this->determineSuccess($response, $formattedResponse, $timeout); 280 | 281 | return is_array($formattedResponse) ? $formattedResponse : $isSuccess; 282 | } 283 | 284 | /** 285 | * @param string $http_verb 286 | * @param string $method 287 | * @param string $url 288 | * @param integer $timeout 289 | * 290 | * @return array 291 | */ 292 | private function prepareStateForRequest($http_verb, $method, $url, $timeout) 293 | { 294 | $this->last_error = ''; 295 | 296 | $this->request_successful = false; 297 | 298 | $this->last_response = array( 299 | 'headers' => null, // array of details from curl_getinfo() 300 | 'httpHeaders' => null, // array of HTTP headers 301 | 'body' => null // content of the response 302 | ); 303 | 304 | $this->last_request = array( 305 | 'method' => $http_verb, 306 | 'path' => $method, 307 | 'url' => $url, 308 | 'body' => '', 309 | 'timeout' => $timeout, 310 | ); 311 | 312 | return $this->last_response; 313 | } 314 | 315 | /** 316 | * Get the HTTP headers as an array of header-name => header-value pairs. 317 | * 318 | * The "Link" header is parsed into an associative array based on the 319 | * rel names it contains. The original value is available under 320 | * the "_raw" key. 321 | * 322 | * @param string $headersAsString 323 | * 324 | * @return array 325 | */ 326 | private function getHeadersAsArray($headersAsString) 327 | { 328 | $headers = array(); 329 | 330 | foreach (explode("\r\n", $headersAsString) as $i => $line) { 331 | if (preg_match('/HTTP\/[1-2]/', substr($line, 0, 7)) === 1) { // http code 332 | continue; 333 | } 334 | 335 | $line = trim($line); 336 | if (empty($line)) { 337 | continue; 338 | } 339 | 340 | list($key, $value) = explode(':', $line); 341 | $value = ltrim($value); 342 | 343 | if ($key == 'Link') { 344 | $value = array_merge( 345 | array('_raw' => $value), 346 | $this->getLinkHeaderAsArray($value) 347 | ); 348 | } 349 | 350 | $headers[$key] = $value; 351 | } 352 | 353 | return $headers; 354 | } 355 | 356 | /** 357 | * Extract all rel => URL pairs from the provided Link header value 358 | * 359 | * Mailchimp only implements the URI reference and relation type from 360 | * RFC 5988, so the value of the header is something like this: 361 | * 362 | * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy", 363 | * ; rel="dashboard"' 364 | * 365 | * @param string $linkHeaderAsString 366 | * 367 | * @return array 368 | */ 369 | private function getLinkHeaderAsArray($linkHeaderAsString) 370 | { 371 | $urls = array(); 372 | 373 | if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) { 374 | foreach ($matches[2] as $i => $relName) { 375 | $urls[$relName] = $matches[1][$i]; 376 | } 377 | } 378 | 379 | return $urls; 380 | } 381 | 382 | /** 383 | * Encode the data and attach it to the request 384 | * 385 | * @param resource $ch cURL session handle, used by reference 386 | * @param array $data Assoc array of data to attach 387 | */ 388 | private function attachRequestPayload(&$ch, $data) 389 | { 390 | $encoded = json_encode($data); 391 | $this->last_request['body'] = $encoded; 392 | curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); 393 | } 394 | 395 | /** 396 | * Decode the response and format any error messages for debugging 397 | * 398 | * @param array $response The response from the curl request 399 | * 400 | * @return array|false The JSON decoded into an array 401 | */ 402 | private function formatResponse($response) 403 | { 404 | $this->last_response = $response; 405 | 406 | if (!empty($response['body'])) { 407 | return json_decode($response['body'], true); 408 | } 409 | 410 | return false; 411 | } 412 | 413 | /** 414 | * Do post-request formatting and setting state from the response 415 | * 416 | * @param array $response The response from the curl request 417 | * @param string $responseContent The body of the response from the curl request 418 | * @param resource $ch The curl resource 419 | * 420 | * @return array The modified response 421 | */ 422 | private function setResponseState($response, $responseContent, $ch) 423 | { 424 | if ($responseContent === false) { 425 | $this->last_error = curl_error($ch); 426 | } else { 427 | 428 | $headerSize = $response['headers']['header_size']; 429 | 430 | $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize)); 431 | $response['body'] = substr($responseContent, $headerSize); 432 | 433 | if (isset($response['headers']['request_header'])) { 434 | $this->last_request['headers'] = $response['headers']['request_header']; 435 | } 436 | } 437 | 438 | return $response; 439 | } 440 | 441 | /** 442 | * Check if the response was successful or a failure. If it failed, store the error. 443 | * 444 | * @param array $response The response from the curl request 445 | * @param array|false $formattedResponse The response body payload from the curl request 446 | * @param int $timeout The timeout supplied to the curl request. 447 | * 448 | * @return bool If the request was successful 449 | */ 450 | private function determineSuccess($response, $formattedResponse, $timeout) 451 | { 452 | $status = $this->findHTTPStatus($response, $formattedResponse); 453 | 454 | if ($status >= 200 && $status <= 299) { 455 | $this->request_successful = true; 456 | return true; 457 | } 458 | 459 | if (isset($formattedResponse['detail'])) { 460 | $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']); 461 | return false; 462 | } 463 | 464 | if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) { 465 | $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']); 466 | return false; 467 | } 468 | 469 | $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.'; 470 | return false; 471 | } 472 | 473 | /** 474 | * Find the HTTP status code from the headers or API response body 475 | * 476 | * @param array $response The response from the curl request 477 | * @param array|false $formattedResponse The response body payload from the curl request 478 | * 479 | * @return int HTTP status code 480 | */ 481 | private function findHTTPStatus($response, $formattedResponse) 482 | { 483 | if (!empty($response['headers']) && isset($response['headers']['http_code'])) { 484 | return (int)$response['headers']['http_code']; 485 | } 486 | 487 | if (!empty($response['body']) && isset($formattedResponse['status'])) { 488 | return (int)$formattedResponse['status']; 489 | } 490 | 491 | return 418; 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /src/Webhook.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Webhook 12 | { 13 | private static $eventSubscriptions = array(); 14 | private static $receivedWebhook = null; 15 | 16 | /** 17 | * Subscribe to an incoming webhook request. The callback will be invoked when a matching webhook is received. 18 | * 19 | * @param string $event Name of the webhook event, e.g. subscribe, unsubscribe, campaign 20 | * @param callable $callback A callable function to invoke with the data from the received webhook 21 | * 22 | * @return void 23 | */ 24 | public static function subscribe($event, callable $callback) 25 | { 26 | if (!isset(self::$eventSubscriptions[$event])) self::$eventSubscriptions[$event] = array(); 27 | self::$eventSubscriptions[$event][] = $callback; 28 | 29 | self::receive(); 30 | } 31 | 32 | /** 33 | * Retrieve the incoming webhook request as sent. 34 | * 35 | * @param string $input An optional raw POST body to use instead of php://input - mainly for unit testing. 36 | * 37 | * @return array|false An associative array containing the details of the received webhook 38 | */ 39 | public static function receive($input = null) 40 | { 41 | if (is_null($input)) { 42 | if (self::$receivedWebhook !== null) { 43 | $input = self::$receivedWebhook; 44 | } else { 45 | $input = file_get_contents("php://input"); 46 | } 47 | } 48 | 49 | if (!is_null($input) && $input != '') { 50 | return self::processWebhook($input); 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Process the raw request into a PHP array and dispatch any matching subscription callbacks 58 | * 59 | * @param string $input The raw HTTP POST request 60 | * 61 | * @return array|false An associative array containing the details of the received webhook 62 | */ 63 | private static function processWebhook($input) 64 | { 65 | self::$receivedWebhook = $input; 66 | parse_str($input, $result); 67 | if ($result && isset($result['type'])) { 68 | self::dispatchWebhookEvent($result['type'], $result['data']); 69 | return $result; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | /** 76 | * Call any subscribed callbacks for this event 77 | * 78 | * @param string $event The name of the callback event 79 | * @param array $data An associative array of the webhook data 80 | * 81 | * @return void 82 | */ 83 | private static function dispatchWebhookEvent($event, $data) 84 | { 85 | if (isset(self::$eventSubscriptions[$event])) { 86 | foreach (self::$eventSubscriptions[$event] as $callback) { 87 | $callback($data); 88 | } 89 | // reset subscriptions 90 | self::$eventSubscriptions[$event] = array(); 91 | } 92 | } 93 | } 94 | --------------------------------------------------------------------------------