From 3f38b434a43582d55d6dc7508ce77382d2826bfb Mon Sep 17 00:00:00 2001 From: ionutcalara Date: Wed, 12 Sep 2018 15:19:36 +0300 Subject: [PATCH] Handle pagination via cursors --- README.md | 27 +-- changelog.txt | 7 + composer.json | 2 +- src/Paylike.php | 3 +- src/Resource/Merchants.php | 51 ++++-- src/Resource/Transactions.php | 48 ++++- src/Utils/Cursor.php | 320 ++++++++++++++++++++++++++++++++++ tests/MerchantsTest.php | 104 ++++++++--- tests/TransactionsTest.php | 107 ++++++++++-- 9 files changed, 591 insertions(+), 78 deletions(-) create mode 100644 src/Utils/Cursor.php diff --git a/README.md b/README.md index 953a9ad..1aefded 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,9 @@ $merchants = $paylike->merchants(); $merchants->create($args); $merchants->fetch($merchant_id); $merchants->update($merchant_id, $args); -$all_merchants = $merchants->get($app_id,$limit,$before); +$all_merchants = $merchants->find($app_id,$args); +$some_merchants = $merchants->before($app_id,$before); +$some_merchants = $merchants->after($app_id,$before); $cards = $paylike->cards(); $cards->create($merchant_id, $args); @@ -75,28 +77,13 @@ $transactions->fetch($transaction_id); $transactions->capture($transaction_id, $args); $transactions->void($transaction_id, $args); $transactions->refund($transaction_id, $args); -$all_transactions = $transactions->get($merchant_id,$limit,$before); +$all_transactions = $transactions->find($merchant_id,$args); +$some_transactions = $transactions->before($merchant_id,$before); +$some_transactions = $transactions->after($merchant_id,$before); ``` ## Pagination -The methods that allow fetching all transactions/merchants support pagination. By default they are limited to the last 10 items. -An example of handling pagination to fetch all available transactions: - -```php -$transactions = array(); -$limit = 10; -$before = null; - -do { - $api_transactions = $this->transactions->get($merchant_id, $limit, $before); - if (count($api_transactions) < $limit) { - $before = null; - } else { - $before = $api_transactions[$limit - 1]['id']; - } - $transactions = array_merge($transactions, $api_transactions); -} while ($before); -``` +The methods that return multiple merchants/transactions (find,after,before) use cursors, so you don't need to worry about pagination, you can access any index, or iterate all the items, this is handled in the background. ## Error handling diff --git a/changelog.txt b/changelog.txt index 4253ed6..ab67267 100644 --- a/changelog.txt +++ b/changelog.txt @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Paylike client (PHP) +## 1.0.2 - 2018-09-12 +### Changed +- Transactions and Merchants now support find and return a filter + +### Removed +- Transactions and Merchants no longer have get method + ## 1.0.1 - 2018-09-04 ### Added - This CHANGELOG file diff --git a/composer.json b/composer.json index 1c0ddd5..d3f19c9 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "paylike/php-api", "description": "PHP SDK to communicate with the Paylike HTTP api", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "authors": [ { diff --git a/src/Paylike.php b/src/Paylike.php index 0a7644e..194966a 100644 --- a/src/Paylike.php +++ b/src/Paylike.php @@ -36,7 +36,8 @@ class Paylike * Paylike constructor. * * @param $api_key - * @param HttpClientInterface $client + * @param HttpClientInterface $client + * @throws Exception\ApiException */ public function __construct($api_key, HttpClientInterface $client = null) { diff --git a/src/Resource/Merchants.php b/src/Resource/Merchants.php index ceec5db..e6b5a33 100644 --- a/src/Resource/Merchants.php +++ b/src/Resource/Merchants.php @@ -2,6 +2,8 @@ namespace Paylike\Resource; +use Paylike\Utils\Cursor; + /** * Class Merchants * @@ -56,20 +58,47 @@ public function update($merchant_id, $args) } /** - * https://github.com/paylike/api-docs#fetch-all-merchants - * @param int $app_id - * @param int $limit - * @param null $before - * @return array + * @link https://github.com/paylike/api-docs#fetch-all-merchants + * + * @param $app_id + * @param array $args + * @return Cursor + * @throws \Exception */ - public function get($app_id, $limit = 10, $before = null) + public function find($app_id, $args = array()) { - $url = 'identities/' . $app_id . '/merchants?limit=' . $limit; - if ($before) { - $url .= '&before=' . $before; + $url = 'identities/' . $app_id . '/merchants'; + if (!isset($args['limit'])) { + $args['limit'] = 10; } - $api_response = $this->paylike->client->request('GET', $url); + $api_response = $this->paylike->client->request('GET', $url, $args); $merchants = $api_response->json; - return $merchants; + return new Cursor($url, $args, $merchants, $this->paylike); + } + + /** + * @link https://github.com/paylike/api-docs#fetch-all-merchants + * + * @param $app_id + * @param $merchant_id + * @return Cursor + * @throws \Exception + */ + public function before($app_id, $merchant_id) + { + return $this->find($app_id, array('before' => $merchant_id)); + } + + /** + * @link https://github.com/paylike/api-docs#fetch-all-merchants + * + * @param $app_id + * @param $merchant_id + * @return Cursor + * @throws \Exception + */ + public function after($app_id, $merchant_id) + { + return $this->find($app_id, array('after' => $merchant_id)); } } diff --git a/src/Resource/Transactions.php b/src/Resource/Transactions.php index a3dca0c..ca4f71a 100644 --- a/src/Resource/Transactions.php +++ b/src/Resource/Transactions.php @@ -2,6 +2,8 @@ namespace Paylike\Resource; +use Paylike\Utils\Cursor; + /** * Class Transactions * @@ -97,16 +99,44 @@ public function refund($transaction_id, $args) * @link https://github.com/paylike/api-docs#fetch-all-transactions * * @param $merchant_id - * @param int $limit - * @param null $before + * @param array $args + * @return Cursor + * @throws \Exception */ - public function get($merchant_id, $limit = 10, $before = null){ - $url = 'merchants/' . $merchant_id . '/transactions?limit=' . $limit; - if ($before) { - $url .= '&before=' . $before; + public function find($merchant_id, $args = array()) + { + $url = 'merchants/' . $merchant_id . '/transactions'; + if (!isset($args['limit'])) { + $args['limit'] = 10; } - $api_response = $this->paylike->client->request('GET', $url); - $merchants = $api_response->json; - return $merchants; + $api_response = $this->paylike->client->request('GET', $url, $args); + $transactions = $api_response->json; + return new Cursor($url, $args, $transactions, $this->paylike); + } + + /** + * @link https://github.com/paylike/api-docs#fetch-all-transactions + * + * @param $merchant_id + * @param $transaction_id + * @return Cursor + * @throws \Exception + */ + public function before($merchant_id, $transaction_id) + { + return $this->find($merchant_id, array('before' => $transaction_id)); + } + + /** + * @link https://github.com/paylike/api-docs#fetch-all-transactions + * + * @param $merchant_id + * @param $transaction_id + * @return Cursor + * @throws \Exception + */ + public function after($merchant_id, $transaction_id) + { + return $this->find($merchant_id, array('after' => $transaction_id)); } } diff --git a/src/Utils/Cursor.php b/src/Utils/Cursor.php new file mode 100644 index 0000000..9f59026 --- /dev/null +++ b/src/Utils/Cursor.php @@ -0,0 +1,320 @@ +endpoint = $endpoint; + $this->collection = $data; + $this->paylike = $paylike; + $this->total_count = count($this->collection); + $this->params = $this->setParams($params); + } + + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() + { + $this->current_index = 0; + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + return $this->collection[$this->current_index]; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + */ + public function key() + { + return $this->current_index; + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return null any returned value is ignored. + */ + public function next() + { + ++$this->current_index; + if (!$this->valid() && $this->couldHaveMoreItems()) { + $this->fetchNext(); + } + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + */ + public function valid() + { + return isset($this->collection[$this->current_index]); + } + + /** + * @return integer + */ + public function count() + { + return max($this->total_count, count($this->collection)); + } + + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($offset) + { + $exists = isset($this->collection[$offset]); + if ($exists) { + return $exists; + } + if ($this->couldHaveMoreItems()) { + $this->fetchNext(); + return $this->offsetExists($offset); + } else { + return $exists; + } + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + */ + public function offsetGet($offset) + { + $value = isset($this->collection[$offset]) ? $this->collection[$offset] : null; + if ($value) { + return $value; + } + if ($this->couldHaveMoreItems()) { + $this->fetchNext(); + return $this->offsetGet($offset); + } else { + return $value; + } + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + */ + public function offsetSet($offset, $value) + { + if ($offset === null) { + $this->collection[] = $value; + } else { + $this->collection[$offset] = $value; + } + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

+ * The offset to unset. + *

+ * @return void + */ + public function offsetUnset($offset) + { + unset($this->collection[$offset]); + } + + /** + * @return $this + */ + private function fetchNext() + { + $this->updateOffset()->fetch(); + return $this; + + } + + /** + * @return $this + */ + private function fetch() + { + $api_response = $this->paylike->client->request('GET', $this->endpoint, $this->params); + $data = $api_response->json; + if (count($data)) { + $this->collection = array_merge($this->collection, $data); + $this->total_count += count($data); + } + return $this; + } + + /** + * If after is set, then we increment it, otherwise we increment before + */ + private function updateOffset() + { + if ($this->after()) { + $this->params['after'] = $this->collection[$this->total_count - 1]['id']; + } else { + $this->params['before'] = $this->collection[$this->total_count - 1]['id']; + } + return $this; + } + + /** + * @return mixed + */ + private function after() + { + return $this->params['after']; + } + + /** + * @return mixed + */ + private function before() + { + return $this->params['before']; + } + + /** + * @return mixed + */ + private function limit() + { + return $this->params['limit']; + } + + /** + * @param $params + * @return array + * @throws \Exception + */ + private function setParams($params) + { + $return = array( + 'after' => null, + 'before' => null, + 'limit' => 10, + 'filter' => array() + ); + + if (isset($params['limit'])) { + $limit = $params['limit']; + if (!is_numeric($limit) || $limit <= 0) { + throw new \Exception('Limit is not valid. It has to be a numerical value (> 0)'); + } + $return['limit'] = $limit; + } + + if (isset($params['after'])) { + $after = $params['after']; + if (!is_string($after)) { + throw new \Exception('After is not valid. It has to be a string'); + } + $return['after'] = $after; + } + + if (isset($params['before'])) { + $before = $params['before']; + if (!is_string($before)) { + throw new \Exception('Before is not valid. It has to be a string'); + } + $return['before'] = $before; + } + + if (isset($params['filter'])) { + $filter = $params['filter']; + if (!is_array($filter)) { + throw new \Exception('Filter is not valid. It has to be an array'); + } + if (isset($filter['merchantId'])) { + if (!is_string($filter['merchantId'])) { + throw new \Exception('Merchant filter is not valid. It has to be an string'); + } + $return['filter']['merchantId'] = $filter['merchantId']; + } + if (isset($filter['transactionId'])) { + if (!is_string($filter['transactionId'])) { + throw new \Exception('Transaction filter is not valid. It has to be an string'); + } + $return['filter']['transactionId'] = $filter['transactionId']; + } + } + return $return; + } + + private function couldHaveMoreItems() + { + return ($this->count() % $this->limit() == 0); + } + +} diff --git a/tests/MerchantsTest.php b/tests/MerchantsTest.php index 58481d5..24e07ec 100644 --- a/tests/MerchantsTest.php +++ b/tests/MerchantsTest.php @@ -11,6 +11,9 @@ class MerchantsTest extends BaseTest */ protected $merchants; + /** + * + */ public function setUp() { parent::setUp(); @@ -18,6 +21,9 @@ public function setUp() } + /** + * + */ public function testCreate() { $merchant_id = $this->merchants->create(array( @@ -35,6 +41,9 @@ public function testCreate() $this->assertInternalType('string', $merchant_id, 'primary key type'); } + /** + * + */ public function testFetch() { $merchant_id = $this->merchant_id; @@ -44,26 +53,9 @@ public function testFetch() $this->assertEquals($merchant['id'], $merchant_id, 'primary key'); } - public function testGetAll() - { - $app_id = $this->app_id; - $merchants = array(); - $limit = 10; - $before = null; - - do { - $api_merchants = $this->merchants->get($app_id, $limit, $before); - if (count($api_merchants) < $limit) { - $before = null; - } else { - $before = $api_merchants[$limit - 1]['id']; - } - $merchants = array_merge($merchants,$api_merchants); - } while ($before); - - $this->assertGreaterThan(0, count($merchants), 'number of merchants'); - } - + /** + * + */ public function testUpdate() { $merchant_id = $this->merchant_id; @@ -72,4 +64,76 @@ public function testUpdate() 'name' => 'Updated Merchant Name' )); } + + /** + * @throws \Exception + */ + public function testGetAllMerchantsCursor() + { + $app_id = $this->app_id; + $api_merchants = $this->merchants->find($app_id); + $ids = array(); + foreach ($api_merchants as $merchant) { + // the merchants array grows as needed + $ids[] = $merchant['id']; + } + + $this->assertGreaterThan(0, count($ids), 'number of merchants'); + } + + + /** + * @throws \Exception + */ + public function testGetAllMerchantsCursorOptions() + { + $app_id = $this->app_id; + $after = '5952889e764d2754c974fe94'; + $before = '5b8e5b8cd294fa04eb4cfbeb'; + $api_merchants = $this->merchants->find($app_id, array( + 'after' => $after, + 'before' => $before + )); + $ids = array(); + foreach ($api_merchants as $merchant) { + // the merchants array grows as needed + $ids[] = $merchant['id']; + } + + $this->assertGreaterThan(0, count($api_merchants), 'number of merchants'); + } + + /** + * @throws \Exception + */ + public function testGetAllMerchantsCursorBefore() + { + $app_id = $this->app_id; + $before = '5b8e5b8cd294fa04eb4cfbeb'; + $api_merchants = $this->merchants->before($app_id, $before); + $ids = array(); + foreach ($api_merchants as $merchant) { + // the merchants array grows as needed + $ids[] = $merchant['id']; + } + + $this->assertGreaterThan(0, count($api_merchants), 'number of merchants'); + } + + /** + * @throws \Exception + */ + public function testGetAllMerchantsCursorAfter() + { + $app_id = $this->app_id; + $after = '5952889e764d2754c974fe94'; + $api_merchants = $this->merchants->after($app_id, $after); + $ids = array(); + foreach ($api_merchants as $merchant) { + // the merchants array grows as needed + $ids[] = $merchant['id']; + } + + $this->assertGreaterThan(0, count($api_merchants), 'number of merchants'); + } } diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php index 58377dd..ae4ecc3 100644 --- a/tests/TransactionsTest.php +++ b/tests/TransactionsTest.php @@ -13,12 +13,18 @@ class TransactionsTest extends BaseTest */ protected $transactions; + /** + * + */ public function setUp() { parent::setUp(); $this->transactions = $this->paylike->transactions(); } + /** + * + */ public function testCreate() { $merchant_id = $this->merchant_id; @@ -37,6 +43,9 @@ public function testCreate() $this->assertInternalType('string', $new_transaction_id, 'primary key type'); } + /** + * + */ public function testFetch() { $transaction_id = $this->transaction_id; @@ -46,12 +55,18 @@ public function testFetch() $this->assertEquals($transaction['id'], $transaction_id, 'primary key'); } + /** + * + */ public function testFailFetch() { $this->setExpectedException(NotFound::class); $this->transactions->fetch('wrong id'); } + /** + * + */ public function testCapture() { $new_transaction_id = $this->createNewTransactionForTest(); @@ -72,6 +87,9 @@ public function testCapture() $this->assertEquals($trail[0]['amount'], 100, 'amount in capture trail'); } + /** + * + */ public function testCaptureBiggerAmount() { $this->setExpectedException(InvalidRequest::class); @@ -83,6 +101,9 @@ public function testCaptureBiggerAmount() )); } + /** + * + */ public function testRefund() { $new_transaction_id = $this->createNewTransactionForTest(); @@ -111,6 +132,9 @@ public function testRefund() $this->assertEquals($trail[1]['amount'], 120, 'amount in refund trail'); } + /** + * + */ public function testVoid() { $new_transaction_id = $this->createNewTransactionForTest(); @@ -150,25 +174,76 @@ private function createNewTransactionForTest() } /** - * + * @throws \Exception + */ + public function testGetAllTransactionsCursor() + { + $merchant_id = $this->merchant_id; + $api_transactions = $this->transactions->find($merchant_id); + $ids = array(); + foreach ($api_transactions as $transaction) { + // the transaction array grows as needed + $ids[] = $transaction['id']; + } + + $this->assertGreaterThan(0, count($ids), 'number of transactions'); + } + + + /** + * @throws \Exception */ - public function testGetAllTransactions() + public function testGetAllTransactionsCursorOptions() { $merchant_id = $this->merchant_id; - $transactions = array(); $limit = 10; - $before = null; - - do { - $api_transactions = $this->transactions->get($merchant_id, $limit, $before); - if (count($api_transactions) < $limit) { - $before = null; - } else { - $before = $api_transactions[$limit - 1]['id']; - } - $transactions = array_merge($transactions, $api_transactions); - } while ($before); - - $this->assertGreaterThan(0, count($transactions), 'number of transactions'); + $after = '5b8e839d7cc76f04ecd3f733'; + $before = '5b98deef882cf804f6108700'; + $api_transactions = $this->transactions->find($merchant_id, array( + 'limit' => $limit, + 'after' => $after, + 'before' => $before + )); + $ids = array(); + foreach ($api_transactions as $transaction) { + // the transaction array grows as needed + $ids[] = $transaction['id']; + } + + $this->assertGreaterThan(0, count($api_transactions), 'number of transactions'); + } + + /** + * @throws \Exception + */ + public function testGetAllTransactionsCursorBefore() + { + $merchant_id = $this->merchant_id; + $before = '5b98deef882cf804f6108700'; + $api_transactions = $this->transactions->before($merchant_id, $before); + $ids = array(); + foreach ($api_transactions as $transaction) { + // the transaction array grows as needed + $ids[] = $transaction['id']; + } + + $this->assertGreaterThan(0, count($api_transactions), 'number of transactions'); + } + + /** + * @throws \Exception + */ + public function testGetAllTransactionsCursorAfter() + { + $merchant_id = $this->merchant_id; + $after = '5b8e839d7cc76f04ecd3f733'; + $api_transactions = $this->transactions->before($merchant_id, $after); + $ids = array(); + foreach ($api_transactions as $transaction) { + // the transaction array grows as needed + $ids[] = $transaction['id']; + } + + $this->assertGreaterThan(0, count($api_transactions), 'number of transactions'); } }