From 894ecb404826a0c706039aa8fbcceb55a7a2f90b Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 14:24:36 -0500 Subject: [PATCH 01/47] Initial reworking --- composer.json | 6 +++--- src/Drip_API.class.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 2d19919..b45c914 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,8 @@ "description": "An object-oriented PHP wrapper for Drip's REST API v2.0", "homepage": "https://github.com/DripEmail/drip-php", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Drip\\": "src/" + } } } \ No newline at end of file diff --git a/src/Drip_API.class.php b/src/Drip_API.class.php index f65ab96..42ff463 100644 --- a/src/Drip_API.class.php +++ b/src/Drip_API.class.php @@ -4,7 +4,7 @@ * Drip API * @author Svetoslav Marinov (SLAVI) */ -Class Drip_Api { +class Client { private $version = "2"; private $api_token = ''; private $error_code = ''; From 48513f992ae9c784df687a711dae55cf9c214f1c Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 14:25:19 -0500 Subject: [PATCH 02/47] Namespace --- src/Drip_API.class.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Drip_API.class.php b/src/Drip_API.class.php index 42ff463..49f09b3 100644 --- a/src/Drip_API.class.php +++ b/src/Drip_API.class.php @@ -1,5 +1,7 @@ Date: Tue, 29 May 2018 14:29:00 -0500 Subject: [PATCH 03/47] Pull in PHPUnit --- .gitignore | 3 +++ composer.json | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90372b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Composer files. +/vendor +/composer.lock diff --git a/composer.json b/composer.json index b45c914..afcecf1 100644 --- a/composer.json +++ b/composer.json @@ -6,5 +6,8 @@ "psr-4": { "Drip\\": "src/" } + }, + "require-dev": { + "phpunit/phpunit": "^5" } -} \ No newline at end of file +} From bbe0332799eef3f83c8971437771835a27d6db2c Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 14:29:33 -0500 Subject: [PATCH 04/47] Rename class file --- src/{Drip_API.class.php => Client.class.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{Drip_API.class.php => Client.class.php} (100%) diff --git a/src/Drip_API.class.php b/src/Client.class.php similarity index 100% rename from src/Drip_API.class.php rename to src/Client.class.php From e926207f2b58dae096ad804ba59c079bbea30144 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 14:29:52 -0500 Subject: [PATCH 05/47] Remove trailing spaces --- src/Client.class.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Client.class.php b/src/Client.class.php index 49f09b3..e39d234 100644 --- a/src/Client.class.php +++ b/src/Client.class.php @@ -26,7 +26,7 @@ class Client { /** * Accepts the token and saves it internally. - * + * * @param string $api_token e.g. qsor48ughrjufyu2dadraasfa1212424 * @throws Exception */ @@ -142,7 +142,7 @@ public function get_accounts() { /** * Sends a request to add a subscriber and returns its record or false - * + * * @param array $params * @param array/bool $account */ @@ -150,10 +150,10 @@ public function create_or_update_subscriber($params) { if (empty($params['account_id'])) { throw new Exception("Account ID not specified"); } - + $account_id = $params['account_id']; unset($params['account_id']); // clear it from the params - + $api_action = "/$account_id/subscribers"; $url = $this->api_end_point . $api_action; @@ -176,7 +176,7 @@ public function create_or_update_subscriber($params) { } /** - * + * * @param array $params * @param array $params */ @@ -220,7 +220,7 @@ public function fetch_subscriber($params) { /** * Subscribes a user to a given campaign for a given account. - * + * * @param array $params * @param array $accounts */ @@ -269,9 +269,9 @@ public function subscribe_subscriber($params) { } /** - * + * * Some keys are removed from the params so they don't get send with the other data to Drip. - * + * * @param array $params * @param array $params */ @@ -297,7 +297,7 @@ public function unsubscribe_subscriber($params) { $api_action = "$account_id/subscribers/$subscriber_id/unsubscribe"; $url = $this->api_end_point . $api_action; - + $req_params = $params; $res = $this->make_request($url, $req_params, self::POST); @@ -323,7 +323,7 @@ public function unsubscribe_subscriber($params) { */ public function tag_subscriber($params) { $status = false; - + if (empty($params['account_id'])) { throw new Exception("Account ID not specified"); } @@ -357,7 +357,7 @@ public function tag_subscriber($params) { /** * * This calls DELETE /:account_id/tags to remove the tags. It just returns some status code no content - * + * * @param array $params * @param bool $status success or failure */ @@ -429,7 +429,7 @@ public function record_event($params) { return $status; } - + /** * * @param string $url @@ -490,7 +490,7 @@ public function make_request($url, $params = array(), $req_method = self::GET) { $buffer = curl_exec($ch); $status = !empty($buffer); - + $data = array( 'url' => $url, 'params' => $params, @@ -529,7 +529,7 @@ public function get_request_info() { public function get_error_message() { return $this->error_message; } - + /** * Retruns whatever was accumultaed in error_code * @return string @@ -566,7 +566,7 @@ public function _parse_error($res) { */ if (!empty($json_arr['errors'])) { // JSON $messages = $error_codes = array(); - + foreach ($json_arr['errors'] as $rec) { $messages[] = $rec['message']; $error_codes[] = $rec['code']; @@ -576,7 +576,7 @@ public function _parse_error($res) { $this->error_message = join("\n", $messages); } else { // There's no JSON in the reply so we'll extract the message from the HTML page by removing the HTML. $msg = $res['buffer']; - + $msg = preg_replace('#.*?]*>#si', '', $msg); $msg = preg_replace('#]*>.*#si', '', $msg); $msg = strip_tags($msg); From d6783b0f84ddbef381278a0b8f4303f02b7a614d Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 14:30:50 -0500 Subject: [PATCH 06/47] Rename file --- src/{Client.class.php => Client.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{Client.class.php => Client.php} (100%) diff --git a/src/Client.class.php b/src/Client.php similarity index 100% rename from src/Client.class.php rename to src/Client.php From 190fbed52869de63ddaef49ac710472e71a998a1 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:01:38 -0500 Subject: [PATCH 07/47] Initial test --- composer.json | 4 +--- phpunit.xml | 26 ++++++++++++++++++++++++++ tests/ClientTest.php | 11 +++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/ClientTest.php diff --git a/composer.json b/composer.json index afcecf1..81924b8 100644 --- a/composer.json +++ b/composer.json @@ -3,9 +3,7 @@ "description": "An object-oriented PHP wrapper for Drip's REST API v2.0", "homepage": "https://github.com/DripEmail/drip-php", "autoload": { - "psr-4": { - "Drip\\": "src/" - } + "psr-4": { "Drip\\": "src" } }, "require-dev": { "phpunit/phpunit": "^5" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..308aa1b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,26 @@ + + + + + + ./tests/ + + + + + + ./ + + ./tests + ./vendor + + + + diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..70640b4 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,11 @@ +assertInstanceOf(\Drip\Client::class, $client); + } +} From 38b11fb6b85641db668b64fa0293765baa9cf60a Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:06:55 -0500 Subject: [PATCH 08/47] New changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..86a0025 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- Set up composer package +- Make PSR-4 compatible +- Move to namespace `\Drip\Client` +- Add initial tests using PHPUnit From 87876761e4f91adb0b7a05d99fdcbe336ae0d554 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:22:48 -0500 Subject: [PATCH 09/47] Expect account ID in constructor --- CHANGELOG.md | 1 + README.md | 31 +++++++++++++++++++-- src/Client.php | 20 +++++++++---- src/Exception/InvalidAccountIdException.php | 5 ++++ src/Exception/InvalidApiTokenException.php | 5 ++++ tests/ClientTest.php | 14 +++++++++- 6 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 src/Exception/InvalidAccountIdException.php create mode 100644 src/Exception/InvalidApiTokenException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a0025..555f49b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Make PSR-4 compatible - Move to namespace `\Drip\Client` - Add initial tests using PHPUnit +- Pass account_id into client constructor diff --git a/README.md b/README.md index 97906a4..28fbebe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,29 @@ -Drip API Wrapper - PHP -=============== +# Drip API Wrapper - PHP -An object-oriented PHP wrapper for Drip's REST API v2.0 +An object-oriented PHP wrapper for Drip's REST API v2.0 + +[![Build Status](https://travis-ci.org/DripEmail/drip-php.svg?branch=master)](https://travis-ci.org/DripEmail/drip-php) + +## Installation + +Run `composer require dripemail/drip-php` + +## Authentication + +For private integrations, you may use your personal API Token (found +[here](https://www.getdrip.com/user/edit)) via the `api_key` setting: + +```php +$client = new \Drip\Client("YOUR_API_KEY", "YOUR_ACCOUNT_ID"); +``` + +For public integrations, pass in the user's OAuth token via the `access_token` +setting: + +```php +$client = new \Drip\Client("YOUR_ACCESS_TOKEN", "YOUR_ACCOUNT_ID"); +``` + +Your account ID can be found [here](https://www.getdrip.com/settings/site). +Most API actions require an account ID, with the exception of methods like +the "list accounts" endpoint. diff --git a/src/Client.php b/src/Client.php index e39d234..b25b403 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,6 +2,9 @@ namespace Drip; +use Drip\Exception\InvalidApiTokenException; +use Drip\Exception\InvalidAccountIdException; + /** * Drip API * @author Svetoslav Marinov (SLAVI) @@ -9,6 +12,7 @@ class Client { private $version = "2"; private $api_token = ''; + private $account_id = ''; private $error_code = ''; private $error_message = ''; private $user_agent = "Drip API PHP Wrapper (getdrip.com)"; @@ -28,16 +32,22 @@ class Client { * Accepts the token and saves it internally. * * @param string $api_token e.g. qsor48ughrjufyu2dadraasfa1212424 + * @param string $account_id e.g. 123456 * @throws Exception */ - public function __construct($api_token) { + public function __construct($api_token, $account_id) { $api_token = trim($api_token); - if (empty($api_token) || !preg_match('#^[\w-]+$#si', $api_token)) { - throw new Exception("Missing or invalid Drip API token."); + throw new InvalidApiTokenException("Missing or invalid Drip API token."); } - $this->api_token = $api_token; + + + $account_id = trim($account_id); + if (empty($account_id) || !preg_match('#^[\w-]+$#si', $account_id)) { + throw new InvalidAccountIdException("Missing or invalid Drip API token."); + } + $this->account_id = $account_id; } /** @@ -601,4 +611,4 @@ public function _parse_error($res) { public function __call($method, $args) { return array(); } -} \ No newline at end of file +} diff --git a/src/Exception/InvalidAccountIdException.php b/src/Exception/InvalidAccountIdException.php new file mode 100644 index 0000000..8c81f8f --- /dev/null +++ b/src/Exception/InvalidAccountIdException.php @@ -0,0 +1,5 @@ +assertInstanceOf(\Drip\Client::class, $client); } + + public function testInvalidApiToken() + { + $this->expectException(Drip\Exception\InvalidApiTokenException::class); + new \Drip\Client("", "1234"); + } + + public function testInvalidAccountId() + { + $this->expectException(Drip\Exception\InvalidAccountIdException::class); + new \Drip\Client("abc123", ""); + } } From 54dc71d353943f1a9663d597cc0b008429ae8450 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:26:54 -0500 Subject: [PATCH 10/47] Use account id everywhere --- src/Client.php | 122 +++++++++++-------------------------------------- 1 file changed, 27 insertions(+), 95 deletions(-) diff --git a/src/Client.php b/src/Client.php index b25b403..136c1eb 100644 --- a/src/Client.php +++ b/src/Client.php @@ -56,22 +56,15 @@ public function __construct($api_token, $account_id) { * @return array */ public function get_campaigns($params) { - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (isset($params['status'])) { if (!in_array($params['status'], array('active', 'draft', 'paused', 'all'))) { - throw new Exception("Invalid campaign status."); + throw new \Exception("Invalid campaign status."); } } elseif (0) { $params['status'] = 'active'; // api defaults to all but we want active ones } - $url = $this->api_end_point . "$account_id/campaigns"; + $url = $this->api_end_point . "$this->account_id/campaigns"; $res = $this->make_request($url, $params); if (!empty($res['buffer'])) { @@ -91,25 +84,18 @@ public function get_campaigns($params) { /** * Fetch a campaign for the given account based on it's ID. - * @param array (account_id, campaign_id) + * @param array (campaign_id) * @return array */ public function fetch_campaign($params) { - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (!empty($params['campaign_id'])) { $campaign_id = $params['campaign_id']; unset($params['campaign_id']); // clear it from the params } else { - throw new Exception("Campaign ID was not specified. You must specify a Campaign ID"); + throw new \Exception("Campaign ID was not specified. You must specify a Campaign ID"); } - $url = $this->api_end_point . "$account_id/campaigns/$campaign_id"; + $url = $this->api_end_point . "$this->account_id/campaigns/$campaign_id"; $res = $this->make_request($url, $params); if (!empty($res['buffer'])) { @@ -157,14 +143,7 @@ public function get_accounts() { * @param array/bool $account */ public function create_or_update_subscriber($params) { - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - - $api_action = "/$account_id/subscribers"; + $api_action = "/$this->account_id/subscribers"; $url = $this->api_end_point . $api_action; // The API wants the params to be JSON encoded @@ -191,13 +170,6 @@ public function create_or_update_subscriber($params) { * @param array $params */ public function fetch_subscriber($params) { - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (!empty($params['subscriber_id'])) { $subscriber_id = $params['subscriber_id']; unset($params['subscriber_id']); // clear it from the params @@ -205,12 +177,12 @@ public function fetch_subscriber($params) { $subscriber_id = $params['email']; unset($params['email']); // clear it from the params } else { - throw new Exception("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); + throw new \Exception("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); } $subscriber_id = urlencode($subscriber_id); - $api_action = "$account_id/subscribers/$subscriber_id"; + $api_action = "$this->account_id/subscribers/$subscriber_id"; $url = $this->api_end_point . $api_action; $res = $this->make_request($url); @@ -235,29 +207,22 @@ public function fetch_subscriber($params) { * @param array $accounts */ public function subscribe_subscriber($params) { - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (empty($params['campaign_id'])) { - throw new Exception("Campaign ID not specified"); + throw new \Exception("Campaign ID not specified"); } $campaign_id = $params['campaign_id']; unset($params['campaign_id']); // clear it from the params if (empty($params['email'])) { - throw new Exception("Email not specified"); + throw new \Exception("Email not specified"); } if (!isset($params['double_optin'])) { $params['double_optin'] = true; } - $api_action = "$account_id/campaigns/$campaign_id/subscribers"; + $api_action = "$this->account_id/campaigns/$campaign_id/subscribers"; $url = $this->api_end_point . $api_action; // The API wants the params to be JSON encoded @@ -286,13 +251,6 @@ public function subscribe_subscriber($params) { * @param array $params */ public function unsubscribe_subscriber($params) { - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (!empty($params['subscriber_id'])) { $subscriber_id = $params['subscriber_id']; unset($params['subscriber_id']); // clear it from the params @@ -300,12 +258,12 @@ public function unsubscribe_subscriber($params) { $subscriber_id = $params['email']; unset($params['email']); // clear it from the params } else { - throw new Exception("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); + throw new \Exception("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); } $subscriber_id = urlencode($subscriber_id); - $api_action = "$account_id/subscribers/$subscriber_id/unsubscribe"; + $api_action = "$this->account_id/subscribers/$subscriber_id/unsubscribe"; $url = $this->api_end_point . $api_action; $req_params = $params; @@ -334,22 +292,15 @@ public function unsubscribe_subscriber($params) { public function tag_subscriber($params) { $status = false; - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (empty($params['email'])) { - throw new Exception("Email was not specified"); + throw new \Exception("Email was not specified"); } if (empty($params['tag'])) { - throw new Exception("Tag was not specified"); + throw new \Exception("Tag was not specified"); } - $api_action = "$account_id/tags"; + $api_action = "$this->account_id/tags"; $url = $this->api_end_point . $api_action; // The API wants the params to be JSON encoded @@ -374,22 +325,15 @@ public function tag_subscriber($params) { public function untag_subscriber($params) { $status = false; - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - if (empty($params['email'])) { - throw new Exception("Email was not specified"); + throw new \Exception("Email was not specified"); } if (empty($params['tag'])) { - throw new Exception("Tag was not specified"); + throw new \Exception("Tag was not specified"); } - $api_action = "$account_id/tags"; + $api_action = "$this->account_id/tags"; $url = $this->api_end_point . $api_action; // The API wants the params to be JSON encoded @@ -414,18 +358,11 @@ public function untag_subscriber($params) { public function record_event($params) { $status = false; - if (empty($params['account_id'])) { - throw new Exception("Account ID not specified"); - } - if (empty($params['action'])) { - throw new Exception("Action was not specified"); + throw new \Exception("Action was not specified"); } - $account_id = $params['account_id']; - unset($params['account_id']); // clear it from the params - - $api_action = "$account_id/events"; + $api_action = "$this->account_id/events"; $url = $this->api_end_point . $api_action; // The API wants the params to be JSON encoded @@ -448,9 +385,9 @@ public function record_event($params) { * @return type * @throws Exception */ - public function make_request($url, $params = array(), $req_method = self::GET) { + private function make_request($url, $params = array(), $req_method = self::GET) { if (!function_exists('curl_init')) { - throw new Exception("Cannot find cURL php extension or it's not loaded."); + throw new \Exception("Cannot find cURL php extension or it's not loaded."); } $ch = curl_init(); @@ -528,7 +465,7 @@ public function make_request($url, $params = array(), $req_method = self::GET) { * This returns the RAW data from the each request that has been sent (if any). * @return arraay of arrays */ - public function get_request_info() { + private function get_request_info() { return $this->recent_req_info; } @@ -536,7 +473,7 @@ public function get_request_info() { * Retruns whatever was accumultaed in error_message * @param string */ - public function get_error_message() { + private function get_error_message() { return $this->error_message; } @@ -544,7 +481,7 @@ public function get_error_message() { * Retruns whatever was accumultaed in error_code * @return string */ - public function get_error_code() { + private function get_error_code() { return $this->error_code; } @@ -554,7 +491,7 @@ public function get_error_code() { * @param array $params * @param array */ - public function _parse_error($res) { + private function _parse_error($res) { if (empty($res['http_code']) || $res['http_code'] >= 200 && $res['http_code'] <= 299) { return true; } @@ -606,9 +543,4 @@ public function _parse_error($res) { $this->error_code = $res['http_code']; } } - - // tmp - public function __call($method, $args) { - return array(); - } } From ce03f99062e66c559ece6abb917fdbb75e3afdc9 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:28:01 -0500 Subject: [PATCH 11/47] Delete useless code --- src/Client.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 136c1eb..03ffbec 100644 --- a/src/Client.php +++ b/src/Client.php @@ -60,8 +60,6 @@ public function get_campaigns($params) { if (!in_array($params['status'], array('active', 'draft', 'paused', 'all'))) { throw new \Exception("Invalid campaign status."); } - } elseif (0) { - $params['status'] = 'active'; // api defaults to all but we want active ones } $url = $this->api_end_point . "$this->account_id/campaigns"; From 9d46c5b00b1e10d6af822d159df9b4a3ce52f7ac Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:29:36 -0500 Subject: [PATCH 12/47] Remove useless version --- src/Client.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 03ffbec..a6a0263 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,7 +10,6 @@ * @author Svetoslav Marinov (SLAVI) */ class Client { - private $version = "2"; private $api_token = ''; private $account_id = ''; private $error_code = ''; From 63c120749989ea40aac7a06ac8f1e2aa2618f1f3 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Tue, 29 May 2018 16:30:12 -0500 Subject: [PATCH 13/47] Set client version --- src/Client.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Client.php b/src/Client.php index a6a0263..2475d8b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,6 +10,8 @@ * @author Svetoslav Marinov (SLAVI) */ class Client { + const VERSION = '1.0.0'; + private $api_token = ''; private $account_id = ''; private $error_code = ''; From 8da6f176841db7cbd5534e2d363332d306b3fc0b Mon Sep 17 00:00:00 2001 From: William Johnston Date: Thu, 31 May 2018 11:06:53 -0500 Subject: [PATCH 14/47] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 555f49b..8d6ebc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Move to namespace `\Drip\Client` - Add initial tests using PHPUnit - Pass account_id into client constructor +- Make some client methods private: + - `\Drip\Client#make_request` + - `\Drip\Client#get_request_info` + - `\Drip\Client#get_error_message` + - `\Drip\Client#get_error_code` + - `\Drip\Client#_parse_error` From 88686365bab2cd12f55a0dc86eef1b9c7271b90b Mon Sep 17 00:00:00 2001 From: William Johnston Date: Thu, 31 May 2018 11:07:16 -0500 Subject: [PATCH 15/47] Fix security issue with cUrl usage --- CHANGELOG.md | 1 + src/Client.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d6ebc1..2e5b96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `\Drip\Client#get_error_message` - `\Drip\Client#get_error_code` - `\Drip\Client#_parse_error` +- Use secure cURL settings by default diff --git a/src/Client.php b/src/Client.php index 2475d8b..2a6a3ca 100644 --- a/src/Client.php +++ b/src/Client.php @@ -402,8 +402,8 @@ private function make_request($url, $params = array(), $req_method = self::GET) curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout); curl_setopt($ch, CURLOPT_USERPWD, $this->api_token . ":" . ''); // no pwd From 5ca68a9fcb8a57915f298ac89acf06371cbc06a2 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Thu, 31 May 2018 11:08:00 -0500 Subject: [PATCH 16/47] Add user agent --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 2a6a3ca..d1720b1 100644 --- a/src/Client.php +++ b/src/Client.php @@ -407,7 +407,7 @@ private function make_request($url, $params = array(), $req_method = self::GET) curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout); curl_setopt($ch, CURLOPT_USERPWD, $this->api_token . ":" . ''); // no pwd - curl_setopt($ch, CURLOPT_USERAGENT, empty($params['user_agent']) ? $this->user_agent : $params['user_agent']); + curl_setopt($ch, CURLOPT_USERAGENT, empty($params['user_agent']) ? "$this->user_agent. Version " . self::VERSION : $params['user_agent']); if ($req_method == self::POST) { // We want post but no params to supply. Probably we have a nice link structure which includes all the info. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); From 4898f7b4616480a7d18c9d607b63619c98820160 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Thu, 31 May 2018 11:08:30 -0500 Subject: [PATCH 17/47] Optional options --- CHANGELOG.md | 1 + src/Client.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e5b96a..cc98091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,3 +18,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `\Drip\Client#get_error_code` - `\Drip\Client#_parse_error` - Use secure cURL settings by default +- Allow setting of API endpoint diff --git a/src/Client.php b/src/Client.php index d1720b1..fd582ef 100644 --- a/src/Client.php +++ b/src/Client.php @@ -36,7 +36,9 @@ class Client { * @param string $account_id e.g. 123456 * @throws Exception */ - public function __construct($api_token, $account_id) { + public function __construct($api_token, $account_id, $options) { + if ($options['api_end_point']) $this->api_end_point = $options['api_end_point']; + $api_token = trim($api_token); if (empty($api_token) || !preg_match('#^[\w-]+$#si', $api_token)) { throw new InvalidApiTokenException("Missing or invalid Drip API token."); From 12c9c456d3a8cb2fef4cf8f3d5e5969157d23e19 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Fri, 1 Jun 2018 09:41:01 -0500 Subject: [PATCH 18/47] Exception classes --- src/Exception/DripException.php | 5 +++++ src/Exception/InvalidAccountIdException.php | 2 +- src/Exception/InvalidApiTokenException.php | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/Exception/DripException.php diff --git a/src/Exception/DripException.php b/src/Exception/DripException.php new file mode 100644 index 0000000..fd20a11 --- /dev/null +++ b/src/Exception/DripException.php @@ -0,0 +1,5 @@ + Date: Fri, 1 Jun 2018 10:44:39 -0500 Subject: [PATCH 19/47] Pull in Guzzle --- CHANGELOG.md | 6 +++--- composer.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc98091..bf5735e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Make PSR-4 compatible - Move to namespace `\Drip\Client` - Add initial tests using PHPUnit -- Pass account_id into client constructor -- Make some client methods private: +- Pass account_id into client constructor (matches semantics of Ruby API client better) +- Remove some client methods to reduce abstraction leakage: - `\Drip\Client#make_request` - `\Drip\Client#get_request_info` - `\Drip\Client#get_error_message` - `\Drip\Client#get_error_code` - `\Drip\Client#_parse_error` -- Use secure cURL settings by default +- Switch to Guzzle HTTP Client - Allow setting of API endpoint diff --git a/composer.json b/composer.json index 81924b8..c857695 100644 --- a/composer.json +++ b/composer.json @@ -7,5 +7,8 @@ }, "require-dev": { "phpunit/phpunit": "^5" + }, + "require": { + "guzzlehttp/guzzle": "^6.3" } } From f96d4bae257a7a5377c88858a12eef09c400a093 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Fri, 1 Jun 2018 10:45:14 -0500 Subject: [PATCH 20/47] New exception class --- src/Exception/UnexpectedHttpVerbException.php | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/Exception/UnexpectedHttpVerbException.php diff --git a/src/Exception/UnexpectedHttpVerbException.php b/src/Exception/UnexpectedHttpVerbException.php new file mode 100644 index 0000000..07eee89 --- /dev/null +++ b/src/Exception/UnexpectedHttpVerbException.php @@ -0,0 +1,5 @@ + Date: Fri, 1 Jun 2018 11:37:57 -0500 Subject: [PATCH 21/47] Error classes --- src/Error.php | 40 ++++++++++++++++++++++ src/ErrorResponse.php | 67 +++++++++++++++++++++++++++++++++++++ src/ResponseInterface.php | 10 ++++++ tests/ErrorResponseTest.php | 34 +++++++++++++++++++ tests/ErrorTest.php | 13 +++++++ 5 files changed, 164 insertions(+) create mode 100644 src/Error.php create mode 100644 src/ErrorResponse.php create mode 100644 src/ResponseInterface.php create mode 100644 tests/ErrorResponseTest.php create mode 100644 tests/ErrorTest.php diff --git a/src/Error.php b/src/Error.php new file mode 100644 index 0000000..95e5e48 --- /dev/null +++ b/src/Error.php @@ -0,0 +1,40 @@ +code = $code; + $this->message = $message; + } + + /** + * The coded error reason. + * + * @return string + */ + public function get_code() { + return $this->code; + } + + /** + * The human readable error message. + * + * @return string + */ + public function get_message() { + return $this->message; + } +} diff --git a/src/ErrorResponse.php b/src/ErrorResponse.php new file mode 100644 index 0000000..6851212 --- /dev/null +++ b/src/ErrorResponse.php @@ -0,0 +1,67 @@ +url = $url; + $this->params = $params; + $this->response = $response; + $this->body = json_decode($response->getBody(), true); + } + + public function get_errors() { + if (!empty($this->body['errors'])) { // JSON + return array_map(function($rec) { + return new Error($rec['code'], $rec['message']); + }, $this->body['errors']); + } else { + return []; + } + } + + /** + * Whether the response is successfull. + * + * @return boolean + */ + public function is_success() { + return false; + } + + /** + * The url of the request. + * + * @return string + */ + public function get_url() { + return $this->url; + } + + /** + * The parameters of the request. + * + * @return array + */ + public function get_params() { + return $this->params; + } + + /** + * The http response code + * + * @return integer + */ + public function get_http_code() { + return $this->response->getStatusCode(); + } +} diff --git a/src/ResponseInterface.php b/src/ResponseInterface.php new file mode 100644 index 0000000..e845120 --- /dev/null +++ b/src/ResponseInterface.php @@ -0,0 +1,10 @@ + 'bar'], $response); + $errors = $err_response->get_errors(); + $this->assertContainsOnlyInstancesOf(\Drip\Error::class, $errors); + $this->assertEquals("blah", $errors[0]->get_code()); + $this->assertEquals("all the blah", $errors[0]->get_message()); + $this->assertFalse($err_response->is_success()); + $this->assertEquals('http://www.example.com/blah', $err_response->get_url()); + $this->assertEquals(['blah' => 'bar'], $err_response->get_params()); + $this->assertEquals(400, $err_response->get_http_code()); + } + + public function testHTMLResponse() + { + $response = new \GuzzleHttp\Psr7\Response(500, [], '

Error message

', '1.1'); + $client = new \Drip\ErrorResponse("http://www.example.com/blah", ['blah' => 'bar'], $response); + $this->assertCount(0, $client->get_errors()); + } + + public function testMissingErrors() + { + $response = new \GuzzleHttp\Psr7\Response(400, [], '{}', '1.1'); + $client = new \Drip\ErrorResponse("http://www.example.com/blah", ['blah' => 'bar'], $response); + $this->assertCount(0, $client->get_errors()); + } +} diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php new file mode 100644 index 0000000..4d40363 --- /dev/null +++ b/tests/ErrorTest.php @@ -0,0 +1,13 @@ +assertEquals("blahcode", $error->get_code()); + $this->assertEquals("blah all the things", $error->get_message()); + } +} From 913669cf4693cd621a9b51630943fb100481256a Mon Sep 17 00:00:00 2001 From: William Johnston Date: Fri, 1 Jun 2018 11:48:29 -0500 Subject: [PATCH 22/47] Add response message --- src/ErrorResponse.php | 9 +++++++++ src/ResponseInterface.php | 1 + tests/ErrorResponseTest.php | 1 + 3 files changed, 11 insertions(+) diff --git a/src/ErrorResponse.php b/src/ErrorResponse.php index 6851212..7ffc416 100644 --- a/src/ErrorResponse.php +++ b/src/ErrorResponse.php @@ -64,4 +64,13 @@ public function get_params() { public function get_http_code() { return $this->response->getStatusCode(); } + + /** + * The http response message + * + * @return string + */ + public function get_http_message() { + return $this->response->getReasonPhrase(); + } } diff --git a/src/ResponseInterface.php b/src/ResponseInterface.php index e845120..da9f66e 100644 --- a/src/ResponseInterface.php +++ b/src/ResponseInterface.php @@ -7,4 +7,5 @@ public function is_success(); public function get_url(); public function get_params(); public function get_http_code(); + public function get_http_message(); } diff --git a/tests/ErrorResponseTest.php b/tests/ErrorResponseTest.php index 93a9e01..1ba1c31 100644 --- a/tests/ErrorResponseTest.php +++ b/tests/ErrorResponseTest.php @@ -16,6 +16,7 @@ public function testNormalErrors() $this->assertEquals('http://www.example.com/blah', $err_response->get_url()); $this->assertEquals(['blah' => 'bar'], $err_response->get_params()); $this->assertEquals(400, $err_response->get_http_code()); + $this->assertEquals('Bad Request', $err_response->get_http_message()); } public function testHTMLResponse() From 1261972753cf1efc03489f73cc13194b68832819 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Fri, 1 Jun 2018 12:08:22 -0500 Subject: [PATCH 23/47] Refactor response objects --- src/AbstractResponse.php | 69 ++++++++++++++++++++++++++++++++++ src/ErrorResponse.php | 71 +++++------------------------------ src/SuccessResponse.php | 9 +++++ tests/SuccessResponseTest.php | 17 +++++++++ 4 files changed, 104 insertions(+), 62 deletions(-) create mode 100644 src/AbstractResponse.php create mode 100644 src/SuccessResponse.php create mode 100644 tests/SuccessResponseTest.php diff --git a/src/AbstractResponse.php b/src/AbstractResponse.php new file mode 100644 index 0000000..d533cc4 --- /dev/null +++ b/src/AbstractResponse.php @@ -0,0 +1,69 @@ +url = $url; + $this->params = $params; + $this->response = $response; + $this->body = json_decode($response->getBody(), true); + } + + /** + * Whether the response is successfull. + * + * @return boolean + */ + public function is_success() { + return $this->get_http_code() >= 200 && $this->get_http_code() <= 299; + } + + /** + * The url of the request. + * + * @return string + */ + public function get_url() { + return $this->url; + } + + /** + * The parameters of the request. + * + * @return array + */ + public function get_params() { + return $this->params; + } + + /** + * The http response code + * + * @return integer + */ + public function get_http_code() { + return $this->response->getStatusCode(); + } + + /** + * The http response message + * + * @return string + */ + public function get_http_message() { + return $this->response->getReasonPhrase(); + } +} diff --git a/src/ErrorResponse.php b/src/ErrorResponse.php index 7ffc416..536854d 100644 --- a/src/ErrorResponse.php +++ b/src/ErrorResponse.php @@ -2,23 +2,15 @@ namespace Drip; -class ErrorResponse implements ResponseInterface { - /** @var string */ - private $url; - /** @var array */ - private $params; - /** @var \Psr\Http\Message\ResponseInterface */ - private $response; - /** @var array */ - private $body; - - public function __construct($url, $params, \Psr\Http\Message\ResponseInterface $response) { - $this->url = $url; - $this->params = $params; - $this->response = $response; - $this->body = json_decode($response->getBody(), true); - } - +/** + * Error response + */ +class ErrorResponse extends AbstractResponse { + /** + * Array of errors from the response. + * + * @return \Drip\Error[] + */ public function get_errors() { if (!empty($this->body['errors'])) { // JSON return array_map(function($rec) { @@ -28,49 +20,4 @@ public function get_errors() { return []; } } - - /** - * Whether the response is successfull. - * - * @return boolean - */ - public function is_success() { - return false; - } - - /** - * The url of the request. - * - * @return string - */ - public function get_url() { - return $this->url; - } - - /** - * The parameters of the request. - * - * @return array - */ - public function get_params() { - return $this->params; - } - - /** - * The http response code - * - * @return integer - */ - public function get_http_code() { - return $this->response->getStatusCode(); - } - - /** - * The http response message - * - * @return string - */ - public function get_http_message() { - return $this->response->getReasonPhrase(); - } } diff --git a/src/SuccessResponse.php b/src/SuccessResponse.php new file mode 100644 index 0000000..52adef1 --- /dev/null +++ b/src/SuccessResponse.php @@ -0,0 +1,9 @@ + 'bar'], $response); + $this->assertTrue($err_response->is_success()); + $this->assertEquals('http://www.example.com/blah', $err_response->get_url()); + $this->assertEquals(['blah' => 'bar'], $err_response->get_params()); + $this->assertEquals(200, $err_response->get_http_code()); + $this->assertEquals('OK', $err_response->get_http_message()); + } +} From 95bb74e1289eec0029bc5021fd80ead8f09afc5f Mon Sep 17 00:00:00 2001 From: William Johnston Date: Fri, 1 Jun 2018 12:30:48 -0500 Subject: [PATCH 24/47] Allow access to contents --- src/SuccessResponse.php | 8 ++++++++ tests/SuccessResponseTest.php | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/SuccessResponse.php b/src/SuccessResponse.php index 52adef1..8cc5edc 100644 --- a/src/SuccessResponse.php +++ b/src/SuccessResponse.php @@ -6,4 +6,12 @@ * A successful response */ class SuccessResponse extends AbstractResponse { + /** + * API Response contents + * + * @return array + */ + public function get_contents() { + return $this->body; + } } diff --git a/tests/SuccessResponseTest.php b/tests/SuccessResponseTest.php index 05291b9..51044cf 100644 --- a/tests/SuccessResponseTest.php +++ b/tests/SuccessResponseTest.php @@ -7,11 +7,18 @@ final class SuccessResponseTest extends TestCase public function testNormalErrors() { $response = new \GuzzleHttp\Psr7\Response(200, [], '{"errors":[{"code":"blah","message":"all the blah"}]}', '1.1'); - $err_response = new \Drip\SuccessResponse('http://www.example.com/blah', ['blah' => 'bar'], $response); - $this->assertTrue($err_response->is_success()); - $this->assertEquals('http://www.example.com/blah', $err_response->get_url()); - $this->assertEquals(['blah' => 'bar'], $err_response->get_params()); - $this->assertEquals(200, $err_response->get_http_code()); - $this->assertEquals('OK', $err_response->get_http_message()); + $succ_response = new \Drip\SuccessResponse('http://www.example.com/blah', ['blah' => 'bar'], $response); + $this->assertTrue($succ_response->is_success()); + $this->assertEquals('http://www.example.com/blah', $succ_response->get_url()); + $this->assertEquals(['blah' => 'bar'], $succ_response->get_params()); + $this->assertEquals(200, $succ_response->get_http_code()); + $this->assertEquals('OK', $succ_response->get_http_message()); + } + + public function testContents() + { + $response = new \GuzzleHttp\Psr7\Response(200, [], '{"blah":"stuff!!!"}', '1.1'); + $succ_response = new \Drip\SuccessResponse('http://www.example.com/blah', ['blah' => 'bar'], $response); + $this->assertEquals("stuff!!!", $succ_response->get_contents()['blah']); } } From 4448e756e6ebc973c3b8e14c5d0f6680431d5b3e Mon Sep 17 00:00:00 2001 From: William Johnston Date: Fri, 1 Jun 2018 12:31:03 -0500 Subject: [PATCH 25/47] Response object comment --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5735e..0575d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,3 +19,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `\Drip\Client#_parse_error` - Switch to Guzzle HTTP Client - Allow setting of API endpoint +- Return response object instead of array. From 62848298f3600ca6403e7b09bdac97504fe5db7d Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 09:51:02 -0500 Subject: [PATCH 26/47] Ignore vscode stuff --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 90372b8..c9e8ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Composer files. /vendor /composer.lock +/.vscode From 99787165f733822d34308be5e6cf0ebb6766c0ae Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 09:51:18 -0500 Subject: [PATCH 27/47] Pull in another exception class --- src/Exception/InvalidArgumentException.php | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/Exception/InvalidArgumentException.php diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..2a745b1 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,5 @@ + Date: Mon, 4 Jun 2018 10:46:39 -0500 Subject: [PATCH 28/47] Client tests! --- phpunit.xml | 5 + src/Client.php | 435 ++++++++------------------------ tests/ClientTest.php | 357 ++++++++++++++++++++++++++ tests/support/GuzzleHelpers.php | 26 ++ 4 files changed, 489 insertions(+), 334 deletions(-) create mode 100644 tests/support/GuzzleHelpers.php diff --git a/phpunit.xml b/phpunit.xml index 308aa1b..44ce035 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,6 +8,11 @@ failOnRisky="true" failOnWarning="true" > + + + ./tests/support + + ./tests/ diff --git a/src/Client.php b/src/Client.php index fd582ef..c98e3f5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,8 +2,11 @@ namespace Drip; +use Drip\Exception\DripException; +use Drip\Exception\InvalidArgumentException; use Drip\Exception\InvalidApiTokenException; use Drip\Exception\InvalidAccountIdException; +use Drip\Exception\UnexpectedHttpVerbException; /** * Drip API @@ -14,20 +17,17 @@ class Client { private $api_token = ''; private $account_id = ''; - private $error_code = ''; - private $error_message = ''; - private $user_agent = "Drip API PHP Wrapper (getdrip.com)"; private $api_end_point = 'https://api.getdrip.com/v2/'; - //private $api_end_point = 'http://localhost/echo/'; // dbg only - private $recent_req_info = array(); // holds dbg info from a recent request private $timeout = 30; private $connect_timeout = 30; - private $debug = false; // Requests headers and other info to be fetched from the request. Command-line windows will show info in STDERR - const GET = 1; - const POST = 2; - const DELETE = 3; - const PUT = 4; + /** @var callable */ + private $guzzle_stack_constructor; + + const GET = "GET"; + const POST = "POST"; + const DELETE = "DELETE"; + const PUT = "PUT"; /** * Accepts the token and saves it internally. @@ -36,8 +36,11 @@ class Client { * @param string $account_id e.g. 123456 * @throws Exception */ - public function __construct($api_token, $account_id, $options) { - if ($options['api_end_point']) $this->api_end_point = $options['api_end_point']; + public function __construct($api_token, $account_id, $options = []) { + if (\array_key_exists('api_end_point', $options)) $this->api_end_point = $options['api_end_point']; + // NOTE: For testing. Could break at any time, please do not depend on this. + if (\array_key_exists('guzzle_stack_constructor', $options)) $this->guzzle_stack_constructor = $options['guzzle_stack_constructor']; + // TODO: allow setting timeouts $api_token = trim($api_token); if (empty($api_token) || !preg_match('#^[\w-]+$#si', $api_token)) { @@ -55,120 +58,68 @@ public function __construct($api_token, $account_id, $options) { /** * Requests the campaigns for the given account. - * @param array - * @return array + * @param array $params Set of arguments + * - status (optional) + * @return \Drip\ResponseInterface */ public function get_campaigns($params) { if (isset($params['status'])) { if (!in_array($params['status'], array('active', 'draft', 'paused', 'all'))) { - throw new \Exception("Invalid campaign status."); + throw new InvalidArgumentException("Invalid campaign status."); } } - $url = $this->api_end_point . "$this->account_id/campaigns"; - $res = $this->make_request($url, $params); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); - } - - // here we distinguish errors from no campaigns. - // when there's no json that's an error - $campaigns = empty($raw_json) - ? false - : empty($raw_json['campaigns']) - ? array() - : $raw_json['campaigns']; - - return $campaigns; + return $this->make_request("$this->account_id/campaigns", $params); } /** * Fetch a campaign for the given account based on it's ID. - * @param array (campaign_id) - * @return array + * @param array $params Set of arguments + * - campaign_id (required) + * @return \Drip\ResponseInterface */ public function fetch_campaign($params) { - if (!empty($params['campaign_id'])) { - $campaign_id = $params['campaign_id']; - unset($params['campaign_id']); // clear it from the params - } else { - throw new \Exception("Campaign ID was not specified. You must specify a Campaign ID"); - } - - $url = $this->api_end_point . "$this->account_id/campaigns/$campaign_id"; - $res = $this->make_request($url, $params); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); + if (empty($params['campaign_id'])) { + throw new InvalidArgumentException("campaign_id was not specified"); } - // here we distinguish errors from no campaign - // when there's no json that's an error - $campaigns = empty($raw_json) - ? false - : empty($raw_json['campaigns']) - ? array() - : $raw_json['campaigns']; + $campaign_id = $params['campaign_id']; + unset($params['campaign_id']); // clear it from the params - return $campaigns; + return $this->make_request("$this->account_id/campaigns/$campaign_id", $params); } /** * Requests the accounts for the given account. * Parses the response JSON and returns an array which contains: id, name, created_at etc * @param void - * @return bool/array + * @return \Drip\ResponseInterface */ public function get_accounts() { - $url = $this->api_end_point . 'accounts'; - $res = $this->make_request($url); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); - } - - $data = empty($raw_json) - ? false - : empty($raw_json['accounts']) - ? array() - : $raw_json['accounts']; - - return $data; + return $this->make_request('accounts'); } /** * Sends a request to add a subscriber and returns its record or false * * @param array $params - * @param array/bool $account + * @param array $account + * @return \Drip\ResponseInterface */ public function create_or_update_subscriber($params) { - $api_action = "/$this->account_id/subscribers"; - $url = $this->api_end_point . $api_action; - // The API wants the params to be JSON encoded - $req_params = array('subscribers' => array($params)); - - $res = $this->make_request($url, $req_params, self::POST); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); - } - - $data = empty($raw_json) - ? false - : empty($raw_json['subscribers']) - ? array() - : $raw_json['subscribers'][0]; - - return $data; + return $this->make_request( + "$this->account_id/subscribers", + array('subscribers' => array($params)), + self::POST + ); } /** + * Returns info regarding a particular subscriber * * @param array $params - * @param array $params + * @return \Drip\ResponseInterface */ public function fetch_subscriber($params) { if (!empty($params['subscriber_id'])) { @@ -178,27 +129,12 @@ public function fetch_subscriber($params) { $subscriber_id = $params['email']; unset($params['email']); // clear it from the params } else { - throw new \Exception("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); + throw new InvalidArgumentException("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); } $subscriber_id = urlencode($subscriber_id); - $api_action = "$this->account_id/subscribers/$subscriber_id"; - $url = $this->api_end_point . $api_action; - - $res = $this->make_request($url); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); - } - - $data = empty($raw_json) - ? false - : empty($raw_json['subscribers']) - ? array() - : $raw_json['subscribers'][0]; - - return $data; + return $this->make_request("$this->account_id/subscribers/$subscriber_id"); } /** @@ -209,39 +145,24 @@ public function fetch_subscriber($params) { */ public function subscribe_subscriber($params) { if (empty($params['campaign_id'])) { - throw new \Exception("Campaign ID not specified"); + throw new InvalidArgumentException("Campaign ID not specified"); } $campaign_id = $params['campaign_id']; unset($params['campaign_id']); // clear it from the params if (empty($params['email'])) { - throw new \Exception("Email not specified"); + throw new InvalidArgumentException("Email not specified"); } if (!isset($params['double_optin'])) { $params['double_optin'] = true; } - $api_action = "$this->account_id/campaigns/$campaign_id/subscribers"; - $url = $this->api_end_point . $api_action; - // The API wants the params to be JSON encoded $req_params = array('subscribers' => array($params)); - $res = $this->make_request($url, $req_params, self::POST); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); - } - - $data = empty($raw_json) - ? false - : empty($raw_json['subscribers']) - ? array() - : $raw_json['subscribers'][0]; - - return $data; + return $this->make_request("$this->account_id/campaigns/$campaign_id/subscribers", $req_params, self::POST); } /** @@ -259,28 +180,11 @@ public function unsubscribe_subscriber($params) { $subscriber_id = $params['email']; unset($params['email']); // clear it from the params } else { - throw new \Exception("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); + throw new InvalidArgumentException("Subscriber ID or Email was not specified. You must specify either Subscriber ID or Email."); } $subscriber_id = urlencode($subscriber_id); - - $api_action = "$this->account_id/subscribers/$subscriber_id/unsubscribe"; - $url = $this->api_end_point . $api_action; - - $req_params = $params; - $res = $this->make_request($url, $req_params, self::POST); - - if (!empty($res['buffer'])) { - $raw_json = json_decode($res['buffer'], true); - } - - $data = empty($raw_json) - ? false - : empty($raw_json['subscribers']) - ? array() - : $raw_json['subscribers'][0]; - - return $data; + return $this->make_request("$this->account_id/subscribers/$subscriber_id/unsubscribe", $params, self::POST); } /** @@ -291,29 +195,18 @@ public function unsubscribe_subscriber($params) { * @param bool $status */ public function tag_subscriber($params) { - $status = false; - if (empty($params['email'])) { - throw new \Exception("Email was not specified"); + throw new InvalidArgumentException("Email was not specified"); } if (empty($params['tag'])) { - throw new \Exception("Tag was not specified"); + throw new InvalidArgumentException("Tag was not specified"); } - $api_action = "$this->account_id/tags"; - $url = $this->api_end_point . $api_action; - // The API wants the params to be JSON encoded $req_params = array('tags' => array($params)); - $res = $this->make_request($url, $req_params, self::POST); - - if ($res['http_code'] == 201) { - $status = true; - } - - return $status; + return $this->make_request("$this->account_id/tags", $req_params, self::POST); } /** @@ -324,29 +217,18 @@ public function tag_subscriber($params) { * @param bool $status success or failure */ public function untag_subscriber($params) { - $status = false; - if (empty($params['email'])) { - throw new \Exception("Email was not specified"); + throw new InvalidArgumentException("Email was not specified"); } if (empty($params['tag'])) { - throw new \Exception("Tag was not specified"); + throw new InvalidArgumentException("Tag was not specified"); } - $api_action = "$this->account_id/tags"; - $url = $this->api_end_point . $api_action; - // The API wants the params to be JSON encoded $req_params = array('tags' => array($params)); - $res = $this->make_request($url, $req_params, self::DELETE); - - if ($res['http_code'] == 204) { - $status = true; - } - - return $status; + return $this->make_request("$this->account_id/tags", $req_params, self::DELETE); } /** @@ -357,191 +239,76 @@ public function untag_subscriber($params) { * @param bool */ public function record_event($params) { - $status = false; - if (empty($params['action'])) { - throw new \Exception("Action was not specified"); + throw new InvalidArgumentException("Action was not specified"); } - $api_action = "$this->account_id/events"; - $url = $this->api_end_point . $api_action; - // The API wants the params to be JSON encoded $req_params = array('events' => array($params)); - $res = $this->make_request($url, $req_params, self::POST); - - if ($res['http_code'] == 204) { - $status = true; - } - - return $status; - } - - /** - * - * @param string $url - * @param array $params - * @param int $req_method - * @return type - * @throws Exception - */ - private function make_request($url, $params = array(), $req_method = self::GET) { - if (!function_exists('curl_init')) { - throw new \Exception("Cannot find cURL php extension or it's not loaded."); - } - - $ch = curl_init(); - - if ($this->debug) { - //curl_setopt($ch, CURLOPT_HEADER, true); - // TRUE to output verbose information. Writes output to STDERR, or the file specified using CURLOPT_STDERR. - curl_setopt($ch, CURLOPT_VERBOSE, true); - } - - curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); - curl_setopt($ch, CURLOPT_FORBID_REUSE, true); - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout); - curl_setopt($ch, CURLOPT_USERPWD, $this->api_token . ":" . ''); // no pwd - curl_setopt($ch, CURLOPT_USERAGENT, empty($params['user_agent']) ? "$this->user_agent. Version " . self::VERSION : $params['user_agent']); - - if ($req_method == self::POST) { // We want post but no params to supply. Probably we have a nice link structure which includes all the info. - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - } elseif ($req_method == self::DELETE) { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); - } elseif ($req_method == self::PUT) { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); - } - - if (!empty($params)) { - if ((isset($params['__req']) && strtolower($params['__req']) == 'get') - || $req_method == self::GET) { - unset($params['__req']); - $url .= '?' . http_build_query($params); - } elseif ($req_method == self::POST || $req_method == self::DELETE) { - $params_str = is_array($params) ? json_encode($params) : $params; - curl_setopt($ch, CURLOPT_POSTFIELDS, $params_str); - } - } - - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - 'Accept:application/json, text/javascript, */*; q=0.01', - 'Content-Type: application/vnd.api+json', - )); - - $buffer = curl_exec($ch); - $status = !empty($buffer); - - $data = array( - 'url' => $url, - 'params' => $params, - 'status' => $status, - 'error' => empty($buffer) ? curl_error($ch) : '', - 'error_no' => empty($buffer) ? curl_errno($ch) : '', - 'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), - 'debug' => $this->debug ? curl_getinfo($ch) : '', - ); - - curl_close($ch); - - // remove some weird headers HTTP/1.1 100 Continue or HTTP/1.1 200 OK - $buffer = preg_replace('#HTTP/[\d.]+\s+\d+\s+\w+[\r\n]+#si', '', $buffer); - $buffer = trim($buffer); - $data['buffer'] = $buffer; - - $this->_parse_error($data); - $this->recent_req_info = $data; - - return $data; - } - - /** - * This returns the RAW data from the each request that has been sent (if any). - * @return arraay of arrays - */ - private function get_request_info() { - return $this->recent_req_info; + return $this->make_request("$this->account_id/events", $req_params, self::POST); } /** - * Retruns whatever was accumultaed in error_message - * @param string + * @return string */ - private function get_error_message() { - return $this->error_message; + private function user_agent() { + return "Drip API PHP Wrapper (getdrip.com). Version " . self::VERSION; } /** - * Retruns whatever was accumultaed in error_code - * @return string + * Determines whether the response is a success. + * + * @param int $code + * @return boolean */ - private function get_error_code() { - return $this->error_code; + private function is_success_response($code) { + return $code >= 200 && $code <= 299; } /** - * Some keys are removed from the params so they don't get send with the other data to Drip. * + * @param string $url * @param array $params - * @param array + * @param int $req_method + * @return \Drip\ResponseInterface + * @throws Exception */ - private function _parse_error($res) { - if (empty($res['http_code']) || $res['http_code'] >= 200 && $res['http_code'] <= 299) { - return true; + private function make_request($url, $params = array(), $req_method = self::GET) { + $stack = $this->guzzle_stack_constructor ? ($this->guzzle_stack_constructor)() : \GuzzleHttp\HandlerStack::create(); + $client = new \GuzzleHttp\Client([ + 'base_uri' => $this->api_end_point, + 'handler' => $stack, + ]); + + $req_params = [ + 'auth' => [$this->api_token, ''], + 'timeout' => $this->timeout, + 'connect_timeout' => $this->connect_timeout, + 'headers' => [ + 'User-Agent' => $this->user_agent(), + 'Accept' => 'application/json, text/javascript, */*; q=0.01', + 'Content-Type' => 'application/vnd.api+json', + ], + ]; + + switch ($req_method) { + case self::GET: + $req_params['query'] = $params; + break; + case self::POST: + case self::DELETE: + case self::PUT: + $req_params['body'] = is_array($params) ? json_encode($params) : $params; + break; + default: + throw new UnexpectedHttpVerbException("Unexpected HTTP verb $req_method"); + break; } - if (empty($res['buffer'])) { - $this->error_message = "Response from the server."; - $this->error_code = $res['http_code']; - } elseif (!empty($res['buffer'])) { - $json_arr = json_decode($res['buffer'], true); - - // The JSON error response looks like this. - /* - { - "errors": [{ - "code": "authorization_error", - "message": "You are not authorized to access this resource" - }] - } - */ - if (!empty($json_arr['errors'])) { // JSON - $messages = $error_codes = array(); - - foreach ($json_arr['errors'] as $rec) { - $messages[] = $rec['message']; - $error_codes[] = $rec['code']; - } - - $this->error_code = join(", ", $error_codes); - $this->error_message = join("\n", $messages); - } else { // There's no JSON in the reply so we'll extract the message from the HTML page by removing the HTML. - $msg = $res['buffer']; - - $msg = preg_replace('#.*?]*>#si', '', $msg); - $msg = preg_replace('#]*>.*#si', '', $msg); - $msg = strip_tags($msg); - $msg = preg_replace('#[\r\n]#si', '', $msg); - $msg = preg_replace('#\s+#si', ' ', $msg); - $msg = trim($msg); - $msg = substr($msg, 0, 256); - - $this->error_code = $res['http_code']; - $this->error_message = $msg; - } - } elseif ($res['http_code'] >= 400 || $res['http_code'] <= 499) { - $this->error_message = "Not authorized."; - $this->error_code = $res['http_code']; - } elseif ($res['http_code'] >= 500 || $res['http_code'] <= 599) { - $this->error_message = "Internal Server Error."; - $this->error_code = $res['http_code']; - } + $res = $client->request($req_method, $url, $req_params); + + $success_klass = $this->is_success_response($res->getStatusCode()) ? \Drip\SuccessResponse::class : \Drip\ErrorResponse::class; + return new $success_klass($url, $params, $res); } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index f08af4c..f311b19 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -1,6 +1,12 @@ expectException(Drip\Exception\InvalidAccountIdException::class); new \Drip\Client("abc123", ""); } + + ////////////////////////// C A M P A I G N S ////////////////////////// + + // #get_campaigns + + public function testGetCampaignsBaseCase() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->get_campaigns([]); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $uri = $mocked_requests[0]['request']->getUri(); + $this->assertEquals('http://api.example.com/v9001/12345/campaigns', $uri); + } + + public function testGetCampaignsValidStatus() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->get_campaigns(['status' => 'active']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $uri = $mocked_requests[0]['request']->getUri(); + $this->assertEquals('http://api.example.com/v9001/12345/campaigns?status=active', $uri); + } + + public function testGetCampaignsInvalidStatus() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $client->get_campaigns(['status' => 'blah']); + } + + public function testGetCampaignsArbitraryParam() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->get_campaigns(['myparam' => 'blah']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $uri = $mocked_requests[0]['request']->getUri(); + $this->assertEquals('http://api.example.com/v9001/12345/campaigns?myparam=blah', $uri); + } + + // #fetch_campaign + + public function testFetchCampaignBaseCase() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->fetch_campaign(['campaign_id' => 13579]); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $uri = $mocked_requests[0]['request']->getUri(); + $this->assertEquals('http://api.example.com/v9001/12345/campaigns/13579', $uri); + } + + public function testFetchCampaignMissingId() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $client->fetch_campaign([]); + } + + ////////////////////////// A C C O U N T S ////////////////////////// + + // #get_accounts + + public function testGetAccountsBaseCase() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->get_accounts(); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $uri = $mocked_requests[0]['request']->getUri(); + $this->assertEquals('http://api.example.com/v9001/accounts', $uri); + } + + ////////////////////////// S U B S C R I B E R S ////////////////////////// + + // #create_or_update_subscriber + + public function testCreateOrUpdateSubscriberBaseCase() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->create_or_update_subscriber(['blahparam' => 'blahvalue']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/subscribers', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + $this->assertEquals('{"subscribers":[{"blahparam":"blahvalue"}]}', (string) $req->getBody()); + } + + // #fetch_subscriber + + public function testFetchSubscriberById() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->fetch_subscriber(['subscriber_id' => '1234']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/subscribers/1234', $req->getUri()); + $this->assertEquals('GET', $req->getMethod()); + } + + public function testFetchSubscriberByEmail() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->fetch_subscriber(['email' => 'test@example.com']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/subscribers/test%40example.com', $req->getUri()); + $this->assertEquals('GET', $req->getMethod()); + } + + public function testFetchSubscriberWithNeitherEmailNorId() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $response = $client->fetch_subscriber([]); + } + + // #subscribe_subscriber + + public function testSubscribeSubscriberBaseCase() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->subscribe_subscriber(['campaign_id' => '1234', 'email' => 'test@example.com']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/campaigns/1234/subscribers', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + $this->assertEquals('{"subscribers":[{"email":"test@example.com","double_optin":true}]}', (string) $req->getBody()); + } + + public function testSubscribeSubscriberWithDoubleOptin() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->subscribe_subscriber(['campaign_id' => '1234', 'email' => 'test@example.com', 'double_optin' => false]); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/campaigns/1234/subscribers', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + $this->assertEquals('{"subscribers":[{"email":"test@example.com","double_optin":false}]}', (string) $req->getBody()); + } + + public function testSubscribeSubscriberWithoutCampaignId() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $client->subscribe_subscriber(['email' => 'test@example.com', 'double_optin' => false]); + } + + public function testSubscribeSubscriberWithoutEmail() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $client->subscribe_subscriber(['campaign_id' => '1234', 'double_optin' => false]); + } + + // #unsubscribe_subscriber + + public function testUnsubscribeSubscriberById() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->unsubscribe_subscriber(['subscriber_id' => '1234']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/subscribers/1234/unsubscribe', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + } + + public function testUnsubscribeSubscriberByEmail() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->unsubscribe_subscriber(['email' => 'test@example.com']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/subscribers/test%40example.com/unsubscribe', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + } + + public function testUnsubscribeSubscriberWithNeitherEmailNorId() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $response = $client->unsubscribe_subscriber([]); + } + + // #tag_subscriber + + public function testTagSubscriberBaseCase() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->tag_subscriber(['email' => 'test@example.com', 'tag' => 'blahblah']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/tags', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + $this->assertEquals('{"tags":[{"email":"test@example.com","tag":"blahblah"}]}', (string) $req->getBody()); + } + + public function testTagSubscriberWithoutEmail() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $response = $client->tag_subscriber(['tag' => 'blahblah']); + } + + public function testTagSubscriberWithoutTag() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $response = $client->tag_subscriber(['email' => 'test@example.com']); + } + + // #untag_subscriber + + public function testUntagSubscriberBaseCase() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->untag_subscriber(['email' => 'test@example.com', 'tag' => 'blahblah']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/tags', $req->getUri()); + $this->assertEquals('DELETE', $req->getMethod()); + $this->assertEquals('{"tags":[{"email":"test@example.com","tag":"blahblah"}]}', (string) $req->getBody()); + } + + public function testUntagSubscriberWithoutEmail() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $response = $client->untag_subscriber(['tag' => 'blahblah']); + } + + public function testUntagSubscriberWithoutTag() { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(Drip\Exception\InvalidArgumentException::class); + $response = $client->untag_subscriber(['email' => 'test@example.com']); + } + + ////////////////////////// E V E N T S ////////////////////////// + + // #record_event + + public function testRecordEventBaseCase() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $response = $client->record_event(['action' => 'blahaction']); + $this->assertTrue($response->is_success()); + $this->assertEquals('hello', $response->get_contents()['blah']); + + $this->assertCount(1, $mocked_requests); + $req = $mocked_requests[0]['request']; + $this->assertEquals('http://api.example.com/v9001/12345/events', $req->getUri()); + $this->assertEquals('POST', $req->getMethod()); + $this->assertEquals('{"events":[{"action":"blahaction"}]}', (string) $req->getBody()); + } } diff --git a/tests/support/GuzzleHelpers.php b/tests/support/GuzzleHelpers.php new file mode 100644 index 0000000..a17fc92 --- /dev/null +++ b/tests/support/GuzzleHelpers.php @@ -0,0 +1,26 @@ + 'http://api.example.com/v9001/', + 'guzzle_stack_constructor' => function() use (&$history_object, $responses) { + $mock = new MockHandler($responses); + $stack = \GuzzleHttp\HandlerStack::create($mock); + $stack->push(\GuzzleHttp\Middleware::history($history_object)); + return $stack; + } + ]); + } +} From b3b59e664e1b898e8fcce6d1f340441db6a82b22 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 12:04:12 -0500 Subject: [PATCH 29/47] Set up Travis --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7a82ed0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: php +php: + - '5.5' + - '5.6' + - '7.0' + - '7.1' + - 'hhvm' From afa95344b4675017457efa63588db549e24bcfcd Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:10:50 -0500 Subject: [PATCH 30/47] Pull in linter --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c857695..8cbe815 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "psr-4": { "Drip\\": "src" } }, "require-dev": { - "phpunit/phpunit": "^5" + "phpunit/phpunit": "^5", + "squizlabs/php_codesniffer": "3.*" }, "require": { "guzzlehttp/guzzle": "^6.3" From c02680c0461dcb2b181324740762a74ed8a084fe Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:11:01 -0500 Subject: [PATCH 31/47] Tune Travis --- .travis.yml | 2 ++ Makefile | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100644 Makefile diff --git a/.travis.yml b/.travis.yml index 7a82ed0..3f2e69c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,5 @@ php: - '7.0' - '7.1' - 'hhvm' +script: + - 'make test' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4c9fc26 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY test lint install + +install: + composer install + +test: + ./vendor/bin/phpunit + +lint: + ./vendor/bin/phpcs test/ src/ From 532b44103ad4e88193ad4c1bb3dd127fa93c9696 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:12:25 -0500 Subject: [PATCH 32/47] Fix make syntax --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4c9fc26..02b5db7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY test lint install +.PHONY: test lint install install: composer install From cab2249bd5061ebbf7e9785ffde59e5345ca549e Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:16:18 -0500 Subject: [PATCH 33/47] Install composer packages --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3f2e69c..5b3d5a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,7 @@ php: - '7.0' - '7.1' - 'hhvm' +install: + - 'make install' script: - 'make test' From 5980e2ca0e98cb982b7baa32d225c3612832221b Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:19:06 -0500 Subject: [PATCH 34/47] Drop PHP 5.5 support. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5b3d5a8..d03d406 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: php php: - - '5.5' - '5.6' - '7.0' - '7.1' From 1ef09caec87479c4a1b9b9c9e336263ed9b21f52 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:19:28 -0500 Subject: [PATCH 35/47] Add PHP 7.2 support --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d03d406..056a435 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ php: - '5.6' - '7.0' - '7.1' + - '7.2' - 'hhvm' install: - 'make install' From ed4debdc68743779beb9923b6ce71b8b4b761ea4 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:21:10 -0500 Subject: [PATCH 36/47] Try to support PHP 5.6 --- src/Client.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index c98e3f5..30b7f1c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -275,7 +275,12 @@ private function is_success_response($code) { * @throws Exception */ private function make_request($url, $params = array(), $req_method = self::GET) { - $stack = $this->guzzle_stack_constructor ? ($this->guzzle_stack_constructor)() : \GuzzleHttp\HandlerStack::create(); + if ($this->guzzle_stack_constructor) { + $fn = $this->guzzle_stack_constructor; + $stack = $fn(); + } else { + $stack = \GuzzleHttp\HandlerStack::create(); + } $client = new \GuzzleHttp\Client([ 'base_uri' => $this->api_end_point, 'handler' => $stack, From f6d43d16f17151b12d272623f3d9907a69cb46f2 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:22:21 -0500 Subject: [PATCH 37/47] Add note about future dev --- src/Client.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Client.php b/src/Client.php index 30b7f1c..fca4511 100644 --- a/src/Client.php +++ b/src/Client.php @@ -276,6 +276,7 @@ private function is_success_response($code) { */ private function make_request($url, $params = array(), $req_method = self::GET) { if ($this->guzzle_stack_constructor) { + // This can be replaced with `($this->guzzle_stack_constructor)()` once we drop PHP5 support. $fn = $this->guzzle_stack_constructor; $stack = $fn(); } else { From 0ec4cd2d2eb7bb3fe5d9d78b5d3253873272ba64 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 13:24:29 -0500 Subject: [PATCH 38/47] Add note about PHP version support --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 28fbebe..d9a656c 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,10 @@ $client = new \Drip\Client("YOUR_ACCESS_TOKEN", "YOUR_ACCOUNT_ID"); Your account ID can be found [here](https://www.getdrip.com/settings/site). Most API actions require an account ID, with the exception of methods like the "list accounts" endpoint. + + +## PHP version support + +We attempt to support versions of PHP that are supported upstream: http://php.net/supported-versions.php + +For the actual supported list, see `.travis.yml`. From 1d4b87f1012120b5017c40feee721236634478b2 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 14:01:41 -0500 Subject: [PATCH 39/47] Initial linter --- Makefile | 2 +- phpcs.xml | 141 ++++++++++++++++++ src/AbstractResponse.php | 21 ++- src/Client.php | 53 ++++--- src/Error.php | 12 +- src/ErrorResponse.php | 8 +- src/Exception/DripException.php | 4 +- src/Exception/InvalidAccountIdException.php | 4 +- src/Exception/InvalidApiTokenException.php | 4 +- src/Exception/InvalidArgumentException.php | 4 +- src/Exception/UnexpectedHttpVerbException.php | 4 +- src/ResponseInterface.php | 3 +- src/SuccessResponse.php | 6 +- tests/ClientTest.php | 56 ++++--- tests/ErrorResponseTest.php | 2 + tests/ErrorTest.php | 2 + tests/SuccessResponseTest.php | 2 + tests/support/GuzzleHelpers.php | 10 +- 18 files changed, 277 insertions(+), 61 deletions(-) create mode 100644 phpcs.xml diff --git a/Makefile b/Makefile index 02b5db7..1325a4d 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ test: ./vendor/bin/phpunit lint: - ./vendor/bin/phpcs test/ src/ + ./vendor/bin/phpcs -s diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..ea85910 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,141 @@ + + + The coding standard for Drip. + + src/ + tests/ + + */Standards/*/Tests/*\.(inc|css|js) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AbstractResponse.php b/src/AbstractResponse.php index d533cc4..6a92936 100644 --- a/src/AbstractResponse.php +++ b/src/AbstractResponse.php @@ -5,7 +5,8 @@ /** * Base response class */ -abstract class AbstractResponse implements ResponseInterface { +abstract class AbstractResponse implements ResponseInterface +{ /** @var string */ protected $url; /** @var array */ @@ -15,7 +16,8 @@ abstract class AbstractResponse implements ResponseInterface { /** @var array */ protected $body; - public function __construct($url, $params, \Psr\Http\Message\ResponseInterface $response) { + public function __construct($url, $params, \Psr\Http\Message\ResponseInterface $response) + { $this->url = $url; $this->params = $params; $this->response = $response; @@ -27,7 +29,8 @@ public function __construct($url, $params, \Psr\Http\Message\ResponseInterface $ * * @return boolean */ - public function is_success() { + public function is_success() + { return $this->get_http_code() >= 200 && $this->get_http_code() <= 299; } @@ -36,7 +39,8 @@ public function is_success() { * * @return string */ - public function get_url() { + public function get_url() + { return $this->url; } @@ -45,7 +49,8 @@ public function get_url() { * * @return array */ - public function get_params() { + public function get_params() + { return $this->params; } @@ -54,7 +59,8 @@ public function get_params() { * * @return integer */ - public function get_http_code() { + public function get_http_code() + { return $this->response->getStatusCode(); } @@ -63,7 +69,8 @@ public function get_http_code() { * * @return string */ - public function get_http_message() { + public function get_http_message() + { return $this->response->getReasonPhrase(); } } diff --git a/src/Client.php b/src/Client.php index fca4511..557e994 100644 --- a/src/Client.php +++ b/src/Client.php @@ -12,7 +12,8 @@ * Drip API * @author Svetoslav Marinov (SLAVI) */ -class Client { +class Client +{ const VERSION = '1.0.0'; private $api_token = ''; @@ -36,10 +37,15 @@ class Client { * @param string $account_id e.g. 123456 * @throws Exception */ - public function __construct($api_token, $account_id, $options = []) { - if (\array_key_exists('api_end_point', $options)) $this->api_end_point = $options['api_end_point']; + public function __construct($api_token, $account_id, $options = []) + { + if (\array_key_exists('api_end_point', $options)) { + $this->api_end_point = $options['api_end_point']; + } // NOTE: For testing. Could break at any time, please do not depend on this. - if (\array_key_exists('guzzle_stack_constructor', $options)) $this->guzzle_stack_constructor = $options['guzzle_stack_constructor']; + if (\array_key_exists('guzzle_stack_constructor', $options)) { + $this->guzzle_stack_constructor = $options['guzzle_stack_constructor']; + } // TODO: allow setting timeouts $api_token = trim($api_token); @@ -62,7 +68,8 @@ public function __construct($api_token, $account_id, $options = []) { * - status (optional) * @return \Drip\ResponseInterface */ - public function get_campaigns($params) { + public function get_campaigns($params) + { if (isset($params['status'])) { if (!in_array($params['status'], array('active', 'draft', 'paused', 'all'))) { throw new InvalidArgumentException("Invalid campaign status."); @@ -78,7 +85,8 @@ public function get_campaigns($params) { * - campaign_id (required) * @return \Drip\ResponseInterface */ - public function fetch_campaign($params) { + public function fetch_campaign($params) + { if (empty($params['campaign_id'])) { throw new InvalidArgumentException("campaign_id was not specified"); } @@ -95,7 +103,8 @@ public function fetch_campaign($params) { * @param void * @return \Drip\ResponseInterface */ - public function get_accounts() { + public function get_accounts() + { return $this->make_request('accounts'); } @@ -106,7 +115,8 @@ public function get_accounts() { * @param array $account * @return \Drip\ResponseInterface */ - public function create_or_update_subscriber($params) { + public function create_or_update_subscriber($params) + { // The API wants the params to be JSON encoded return $this->make_request( "$this->account_id/subscribers", @@ -121,7 +131,8 @@ public function create_or_update_subscriber($params) { * @param array $params * @return \Drip\ResponseInterface */ - public function fetch_subscriber($params) { + public function fetch_subscriber($params) + { if (!empty($params['subscriber_id'])) { $subscriber_id = $params['subscriber_id']; unset($params['subscriber_id']); // clear it from the params @@ -143,7 +154,8 @@ public function fetch_subscriber($params) { * @param array $params * @param array $accounts */ - public function subscribe_subscriber($params) { + public function subscribe_subscriber($params) + { if (empty($params['campaign_id'])) { throw new InvalidArgumentException("Campaign ID not specified"); } @@ -172,7 +184,8 @@ public function subscribe_subscriber($params) { * @param array $params * @param array $params */ - public function unsubscribe_subscriber($params) { + public function unsubscribe_subscriber($params) + { if (!empty($params['subscriber_id'])) { $subscriber_id = $params['subscriber_id']; unset($params['subscriber_id']); // clear it from the params @@ -194,7 +207,8 @@ public function unsubscribe_subscriber($params) { * @param array $params * @param bool $status */ - public function tag_subscriber($params) { + public function tag_subscriber($params) + { if (empty($params['email'])) { throw new InvalidArgumentException("Email was not specified"); } @@ -216,7 +230,8 @@ public function tag_subscriber($params) { * @param array $params * @param bool $status success or failure */ - public function untag_subscriber($params) { + public function untag_subscriber($params) + { if (empty($params['email'])) { throw new InvalidArgumentException("Email was not specified"); } @@ -238,7 +253,8 @@ public function untag_subscriber($params) { * @param array $params * @param bool */ - public function record_event($params) { + public function record_event($params) + { if (empty($params['action'])) { throw new InvalidArgumentException("Action was not specified"); } @@ -252,7 +268,8 @@ public function record_event($params) { /** * @return string */ - private function user_agent() { + private function user_agent() + { return "Drip API PHP Wrapper (getdrip.com). Version " . self::VERSION; } @@ -262,7 +279,8 @@ private function user_agent() { * @param int $code * @return boolean */ - private function is_success_response($code) { + private function is_success_response($code) + { return $code >= 200 && $code <= 299; } @@ -274,7 +292,8 @@ private function is_success_response($code) { * @return \Drip\ResponseInterface * @throws Exception */ - private function make_request($url, $params = array(), $req_method = self::GET) { + private function make_request($url, $params = array(), $req_method = self::GET) + { if ($this->guzzle_stack_constructor) { // This can be replaced with `($this->guzzle_stack_constructor)()` once we drop PHP5 support. $fn = $this->guzzle_stack_constructor; diff --git a/src/Error.php b/src/Error.php index 95e5e48..f492e0f 100644 --- a/src/Error.php +++ b/src/Error.php @@ -5,7 +5,8 @@ /** * A reason for a response failure. */ -class Error { +class Error +{ /** @var string */ private $code; /** @var string */ @@ -15,7 +16,8 @@ class Error { * @param string $code Coded error reason * @param string $message Human readable error message. */ - public function __construct($code, $message) { + public function __construct($code, $message) + { $this->code = $code; $this->message = $message; } @@ -25,7 +27,8 @@ public function __construct($code, $message) { * * @return string */ - public function get_code() { + public function get_code() + { return $this->code; } @@ -34,7 +37,8 @@ public function get_code() { * * @return string */ - public function get_message() { + public function get_message() + { return $this->message; } } diff --git a/src/ErrorResponse.php b/src/ErrorResponse.php index 536854d..0c96b14 100644 --- a/src/ErrorResponse.php +++ b/src/ErrorResponse.php @@ -5,15 +5,17 @@ /** * Error response */ -class ErrorResponse extends AbstractResponse { +class ErrorResponse extends AbstractResponse +{ /** * Array of errors from the response. * * @return \Drip\Error[] */ - public function get_errors() { + public function get_errors() + { if (!empty($this->body['errors'])) { // JSON - return array_map(function($rec) { + return array_map(function ($rec) { return new Error($rec['code'], $rec['message']); }, $this->body['errors']); } else { diff --git a/src/Exception/DripException.php b/src/Exception/DripException.php index fd20a11..af55e20 100644 --- a/src/Exception/DripException.php +++ b/src/Exception/DripException.php @@ -2,4 +2,6 @@ namespace Drip\Exception; -class DripException extends \Exception {} +class DripException extends \Exception +{ +} diff --git a/src/Exception/InvalidAccountIdException.php b/src/Exception/InvalidAccountIdException.php index fd75f5c..e98f0b8 100644 --- a/src/Exception/InvalidAccountIdException.php +++ b/src/Exception/InvalidAccountIdException.php @@ -2,4 +2,6 @@ namespace Drip\Exception; -class InvalidAccountIdException extends DripException {} +class InvalidAccountIdException extends DripException +{ +} diff --git a/src/Exception/InvalidApiTokenException.php b/src/Exception/InvalidApiTokenException.php index 92a2b02..501c3da 100644 --- a/src/Exception/InvalidApiTokenException.php +++ b/src/Exception/InvalidApiTokenException.php @@ -2,4 +2,6 @@ namespace Drip\Exception; -class InvalidApiTokenException extends DripException {} +class InvalidApiTokenException extends DripException +{ +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 2a745b1..ae016fc 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -2,4 +2,6 @@ namespace Drip\Exception; -class InvalidArgumentException extends DripException {} +class InvalidArgumentException extends DripException +{ +} diff --git a/src/Exception/UnexpectedHttpVerbException.php b/src/Exception/UnexpectedHttpVerbException.php index 07eee89..f52026c 100644 --- a/src/Exception/UnexpectedHttpVerbException.php +++ b/src/Exception/UnexpectedHttpVerbException.php @@ -2,4 +2,6 @@ namespace Drip\Exception; -class UnexpectedHttpVerbException extends DripException {} +class UnexpectedHttpVerbException extends DripException +{ +} diff --git a/src/ResponseInterface.php b/src/ResponseInterface.php index da9f66e..40d5123 100644 --- a/src/ResponseInterface.php +++ b/src/ResponseInterface.php @@ -2,7 +2,8 @@ namespace Drip; -interface ResponseInterface { +interface ResponseInterface +{ public function is_success(); public function get_url(); public function get_params(); diff --git a/src/SuccessResponse.php b/src/SuccessResponse.php index 8cc5edc..e1ce20c 100644 --- a/src/SuccessResponse.php +++ b/src/SuccessResponse.php @@ -5,13 +5,15 @@ /** * A successful response */ -class SuccessResponse extends AbstractResponse { +class SuccessResponse extends AbstractResponse +{ /** * API Response contents * * @return array */ - public function get_contents() { + public function get_contents() + { return $this->body; } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index f311b19..6f86dc7 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -1,5 +1,7 @@ assertEquals('GET', $req->getMethod()); } - public function testFetchSubscriberByEmail() { + public function testFetchSubscriberByEmail() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -183,7 +189,8 @@ public function testFetchSubscriberByEmail() { $this->assertEquals('GET', $req->getMethod()); } - public function testFetchSubscriberWithNeitherEmailNorId() { + public function testFetchSubscriberWithNeitherEmailNorId() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -194,7 +201,8 @@ public function testFetchSubscriberWithNeitherEmailNorId() { // #subscribe_subscriber - public function testSubscribeSubscriberBaseCase() { + public function testSubscribeSubscriberBaseCase() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -210,7 +218,8 @@ public function testSubscribeSubscriberBaseCase() { $this->assertEquals('{"subscribers":[{"email":"test@example.com","double_optin":true}]}', (string) $req->getBody()); } - public function testSubscribeSubscriberWithDoubleOptin() { + public function testSubscribeSubscriberWithDoubleOptin() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -226,7 +235,8 @@ public function testSubscribeSubscriberWithDoubleOptin() { $this->assertEquals('{"subscribers":[{"email":"test@example.com","double_optin":false}]}', (string) $req->getBody()); } - public function testSubscribeSubscriberWithoutCampaignId() { + public function testSubscribeSubscriberWithoutCampaignId() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -235,7 +245,8 @@ public function testSubscribeSubscriberWithoutCampaignId() { $client->subscribe_subscriber(['email' => 'test@example.com', 'double_optin' => false]); } - public function testSubscribeSubscriberWithoutEmail() { + public function testSubscribeSubscriberWithoutEmail() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -246,7 +257,8 @@ public function testSubscribeSubscriberWithoutEmail() { // #unsubscribe_subscriber - public function testUnsubscribeSubscriberById() { + public function testUnsubscribeSubscriberById() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -261,7 +273,8 @@ public function testUnsubscribeSubscriberById() { $this->assertEquals('POST', $req->getMethod()); } - public function testUnsubscribeSubscriberByEmail() { + public function testUnsubscribeSubscriberByEmail() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -276,7 +289,8 @@ public function testUnsubscribeSubscriberByEmail() { $this->assertEquals('POST', $req->getMethod()); } - public function testUnsubscribeSubscriberWithNeitherEmailNorId() { + public function testUnsubscribeSubscriberWithNeitherEmailNorId() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -287,7 +301,8 @@ public function testUnsubscribeSubscriberWithNeitherEmailNorId() { // #tag_subscriber - public function testTagSubscriberBaseCase() { + public function testTagSubscriberBaseCase() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -303,7 +318,8 @@ public function testTagSubscriberBaseCase() { $this->assertEquals('{"tags":[{"email":"test@example.com","tag":"blahblah"}]}', (string) $req->getBody()); } - public function testTagSubscriberWithoutEmail() { + public function testTagSubscriberWithoutEmail() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -312,7 +328,8 @@ public function testTagSubscriberWithoutEmail() { $response = $client->tag_subscriber(['tag' => 'blahblah']); } - public function testTagSubscriberWithoutTag() { + public function testTagSubscriberWithoutTag() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -323,7 +340,8 @@ public function testTagSubscriberWithoutTag() { // #untag_subscriber - public function testUntagSubscriberBaseCase() { + public function testUntagSubscriberBaseCase() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -339,7 +357,8 @@ public function testUntagSubscriberBaseCase() { $this->assertEquals('{"tags":[{"email":"test@example.com","tag":"blahblah"}]}', (string) $req->getBody()); } - public function testUntagSubscriberWithoutEmail() { + public function testUntagSubscriberWithoutEmail() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), @@ -348,7 +367,8 @@ public function testUntagSubscriberWithoutEmail() { $response = $client->untag_subscriber(['tag' => 'blahblah']); } - public function testUntagSubscriberWithoutTag() { + public function testUntagSubscriberWithoutTag() + { $mocked_requests = []; $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), diff --git a/tests/ErrorResponseTest.php b/tests/ErrorResponseTest.php index 1ba1c31..e680a8e 100644 --- a/tests/ErrorResponseTest.php +++ b/tests/ErrorResponseTest.php @@ -1,5 +1,7 @@ 'http://api.example.com/v9001/', - 'guzzle_stack_constructor' => function() use (&$history_object, $responses) { + 'guzzle_stack_constructor' => function () use (&$history_object, $responses) { $mock = new MockHandler($responses); $stack = \GuzzleHttp\HandlerStack::create($mock); $stack->push(\GuzzleHttp\Middleware::history($history_object)); From f3633f4c3b5b81f59cb2321dc8d01aedb43de4c3 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 14:08:53 -0500 Subject: [PATCH 40/47] Fix tests and start linting --- .travis.yml | 2 +- tests/ClientTest.php | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 056a435..c73cf0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,4 @@ php: install: - 'make install' script: - - 'make test' + - 'make test lint' diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6f86dc7..99b971a 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -19,13 +19,13 @@ public function testInitializedWithApiToken() public function testInvalidApiToken() { - $this->expectException(Drip\Exception\InvalidApiTokenException::class); + $this->expectException(\Drip\Exception\InvalidApiTokenException::class); new \Drip\Client("", "1234"); } public function testInvalidAccountId() { - $this->expectException(Drip\Exception\InvalidAccountIdException::class); + $this->expectException(\Drip\Exception\InvalidAccountIdException::class); new \Drip\Client("abc123", ""); } @@ -69,7 +69,7 @@ public function testGetCampaignsInvalidStatus() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $client->get_campaigns(['status' => 'blah']); } @@ -111,7 +111,7 @@ public function testFetchCampaignMissingId() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $client->fetch_campaign([]); } @@ -195,7 +195,7 @@ public function testFetchSubscriberWithNeitherEmailNorId() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $response = $client->fetch_subscriber([]); } @@ -241,7 +241,7 @@ public function testSubscribeSubscriberWithoutCampaignId() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $client->subscribe_subscriber(['email' => 'test@example.com', 'double_optin' => false]); } @@ -251,7 +251,7 @@ public function testSubscribeSubscriberWithoutEmail() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $client->subscribe_subscriber(['campaign_id' => '1234', 'double_optin' => false]); } @@ -295,7 +295,7 @@ public function testUnsubscribeSubscriberWithNeitherEmailNorId() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $response = $client->unsubscribe_subscriber([]); } @@ -324,7 +324,7 @@ public function testTagSubscriberWithoutEmail() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $response = $client->tag_subscriber(['tag' => 'blahblah']); } @@ -334,7 +334,7 @@ public function testTagSubscriberWithoutTag() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $response = $client->tag_subscriber(['email' => 'test@example.com']); } @@ -363,7 +363,7 @@ public function testUntagSubscriberWithoutEmail() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $response = $client->untag_subscriber(['tag' => 'blahblah']); } @@ -373,7 +373,7 @@ public function testUntagSubscriberWithoutTag() $client = GuzzleHelpers::mocked_client($mocked_requests, [ new Response(200, [], '{"blah":"hello"}'), ]); - $this->expectException(Drip\Exception\InvalidArgumentException::class); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); $response = $client->untag_subscriber(['email' => 'test@example.com']); } From fbf41ae0d7406dbefbf27c6dca860464ef7a393c Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 14:33:19 -0500 Subject: [PATCH 41/47] A little more linter cleanups --- phpcs.xml | 121 +++++-------------------------------------------- src/Client.php | 9 +++- 2 files changed, 19 insertions(+), 111 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index ea85910..83ee3c1 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -12,130 +12,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Client.php b/src/Client.php index 557e994..9a6c246 100644 --- a/src/Client.php +++ b/src/Client.php @@ -16,10 +16,15 @@ class Client { const VERSION = '1.0.0'; + /** @var string */ private $api_token = ''; + /** @var string */ private $account_id = ''; + /** @var string */ private $api_end_point = 'https://api.getdrip.com/v2/'; + /** @var integer */ private $timeout = 30; + /** @var integer */ private $connect_timeout = 30; /** @var callable */ @@ -136,7 +141,7 @@ public function fetch_subscriber($params) if (!empty($params['subscriber_id'])) { $subscriber_id = $params['subscriber_id']; unset($params['subscriber_id']); // clear it from the params - } elseif (!empty($params['email'])) { + } else if (!empty($params['email'])) { $subscriber_id = $params['email']; unset($params['email']); // clear it from the params } else { @@ -189,7 +194,7 @@ public function unsubscribe_subscriber($params) if (!empty($params['subscriber_id'])) { $subscriber_id = $params['subscriber_id']; unset($params['subscriber_id']); // clear it from the params - } elseif (!empty($params['email'])) { + } else if (!empty($params['email'])) { $subscriber_id = $params['email']; unset($params['email']); // clear it from the params } else { From 4c603938b543a6f7d0d1b214f41a7beb987d9749 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 14:35:22 -0500 Subject: [PATCH 42/47] Set license --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 8cbe815..7e49ab7 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,7 @@ "name": "dripemail/drip-php", "description": "An object-oriented PHP wrapper for Drip's REST API v2.0", "homepage": "https://github.com/DripEmail/drip-php", + "license": "MIT", "autoload": { "psr-4": { "Drip\\": "src" } }, From a502ffac850ed3094f4a8596ed075029a3732729 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 14:45:11 -0500 Subject: [PATCH 43/47] Code coverage and an additional test --- phpunit.xml | 5 +++++ src/Client.php | 6 ++++++ tests/ClientTest.php | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index 44ce035..93c7295 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -28,4 +28,9 @@ + + + + + diff --git a/src/Client.php b/src/Client.php index 9a6c246..377cc89 100644 --- a/src/Client.php +++ b/src/Client.php @@ -304,7 +304,9 @@ private function make_request($url, $params = array(), $req_method = self::GET) $fn = $this->guzzle_stack_constructor; $stack = $fn(); } else { + // @codeCoverageIgnoreStart $stack = \GuzzleHttp\HandlerStack::create(); + // @codeCoverageIgnoreEnd } $client = new \GuzzleHttp\Client([ 'base_uri' => $this->api_end_point, @@ -328,12 +330,16 @@ private function make_request($url, $params = array(), $req_method = self::GET) break; case self::POST: case self::DELETE: + // @codeCoverageIgnoreStart case self::PUT: + // @codeCoverageIgnoreEnd $req_params['body'] = is_array($params) ? json_encode($params) : $params; break; default: + // @codeCoverageIgnoreStart throw new UnexpectedHttpVerbException("Unexpected HTTP verb $req_method"); break; + // @codeCoverageIgnoreEnd } $res = $client->request($req_method, $url, $req_params); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 99b971a..3176eed 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -397,4 +397,14 @@ public function testRecordEventBaseCase() $this->assertEquals('POST', $req->getMethod()); $this->assertEquals('{"events":[{"action":"blahaction"}]}', (string) $req->getBody()); } + + public function testRecordEventMissingAction() + { + $mocked_requests = []; + $client = GuzzleHelpers::mocked_client($mocked_requests, [ + new Response(200, [], '{"blah":"hello"}'), + ]); + $this->expectException(\Drip\Exception\InvalidArgumentException::class); + $client->record_event([]); + } } From 9c016e9fdd3dd540da835b9305e4161fce0bd7d0 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Mon, 4 Jun 2018 14:52:46 -0500 Subject: [PATCH 44/47] Update Changelog --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0575d13..182e683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Set up composer package - Make PSR-4 compatible - Move to namespace `\Drip\Client` -- Add initial tests using PHPUnit - Pass account_id into client constructor (matches semantics of Ruby API client better) - Remove some client methods to reduce abstraction leakage: - `\Drip\Client#make_request` @@ -20,3 +19,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Switch to Guzzle HTTP Client - Allow setting of API endpoint - Return response object instead of array. +- Fairly complete test suite +- Code coverage metrics +- Linter + +## 0.0.2 + +* Introduces Composer + +## 0.0.1 + +* Initial release From cdd73ddb393e75603b62aadf9c6bea85a60d2e01 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Wed, 6 Jun 2018 12:49:47 -0500 Subject: [PATCH 45/47] Add doc note for constructor --- src/Client.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Client.php b/src/Client.php index 377cc89..cacf872 100644 --- a/src/Client.php +++ b/src/Client.php @@ -40,6 +40,9 @@ class Client * * @param string $api_token e.g. qsor48ughrjufyu2dadraasfa1212424 * @param string $account_id e.g. 123456 + * @param array $options + * * `api_end_point` (mostly for Drip internal testing) + * * `guzzle_stack_constructor` (for test suite, may break at any time, do not use) * @throws Exception */ public function __construct($api_token, $account_id, $options = []) From 8d63277d32b000289c5e5725f95ca15ac2056d73 Mon Sep 17 00:00:00 2001 From: William Johnston Date: Wed, 6 Jun 2018 12:50:35 -0500 Subject: [PATCH 46/47] Remove a couple redundant comments --- src/Client.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index cacf872..8b2a054 100644 --- a/src/Client.php +++ b/src/Client.php @@ -120,7 +120,6 @@ public function get_accounts() * Sends a request to add a subscriber and returns its record or false * * @param array $params - * @param array $account * @return \Drip\ResponseInterface */ public function create_or_update_subscriber($params) @@ -160,7 +159,6 @@ public function fetch_subscriber($params) * Subscribes a user to a given campaign for a given account. * * @param array $params - * @param array $accounts */ public function subscribe_subscriber($params) { From c063d68e630e8ee1d87ed29d595ed847dcba571b Mon Sep 17 00:00:00 2001 From: William Johnston Date: Wed, 6 Jun 2018 12:52:37 -0500 Subject: [PATCH 47/47] Missed a dup comment --- src/Client.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 8b2a054..0a6660b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -188,7 +188,6 @@ public function subscribe_subscriber($params) * Some keys are removed from the params so they don't get send with the other data to Drip. * * @param array $params - * @param array $params */ public function unsubscribe_subscriber($params) {