From bb959f96bc1b967278e38e1f5b0a357555c93ade Mon Sep 17 00:00:00 2001 From: Hubert Moutot Date: Thu, 14 Apr 2016 20:17:59 +0200 Subject: [PATCH] initial implementation --- .gitignore | 6 ++ CONTRIBUTING.md | 34 +++++++ LICENSE | 22 +++++ README.md | 74 +++++++++++++++ client.php | 84 ----------------- composer.json | 46 +++++++++ examples.php | 16 ---- puli.json | 132 ++++++++++++++++++++++++++ src/Api/AbstractApi.php | 128 +++++++++++++++++++++++++ src/Api/App.php | 89 ++++++++++++++++++ src/Api/Card.php | 30 ++++++ src/Api/Merchant.php | 77 +++++++++++++++ src/Api/Transaction.php | 144 +++++++++++++++++++++++++++++ src/Api/User.php | 57 ++++++++++++ src/Exception/PaylikeException.php | 7 ++ src/HttpClientFactory.php | 42 +++++++++ src/Paylike.php | 77 +++++++++++++++ todo.md | 22 ----- 18 files changed, 965 insertions(+), 122 deletions(-) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md delete mode 100644 client.php create mode 100644 composer.json delete mode 100644 examples.php create mode 100644 puli.json create mode 100644 src/Api/AbstractApi.php create mode 100644 src/Api/App.php create mode 100644 src/Api/Card.php create mode 100644 src/Api/Merchant.php create mode 100644 src/Api/Transaction.php create mode 100644 src/Api/User.php create mode 100644 src/Exception/PaylikeException.php create mode 100644 src/HttpClientFactory.php create mode 100644 src/Paylike.php delete mode 100644 todo.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b7c4c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +*.sublime-project +*.sublime-workspace +composer.lock +vendor +.puli diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4888ade --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +Contributing +============ + +We welcome any contribution: issues, fixes, new features, or documentation. + + +Pull Requests +------------- + +- We follow **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - You can automate your code formatting with **[PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer)** + +- You **HAVE** to add tests, otherwise your contribution is unlikely to be accepted. If you don't know how to do it, please open a pull request anyway and ask us to guide you, we are happy to help. + +- You **HAVE** to document any change in the documentation: especially the [README.md](README.md) file or any other relevant place. + +- You **HAVE** to consider our release cycle: we follow [SemVer v2.0.0](http://semver.org/). You can't break any public API without previous discussion. + +- You **HAVE** to create feature branches and open one pull request per branch. + +If you have any questiion regarding those guidelines, feel free to open an issue, we'll be glad to help you in any way we can. + + +Running Tests +------------- + +``` bash +phpunit +``` + + +Thank you +--------- + +We love contributions :) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2120d44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2016 Paylike ApS + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e5255a --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +Paylike PHP API Wrapper +======================= + +This is a PHP wrapper around the [Paylike](https://paylike.io) API. + + +Warning +------- + +This package is in very early stage of development, things are likely to break +in the future until a stable version is released. Use at your own risks. + + +Installation +------------ + +Install this package via Composer: + +``` bash +$ composer require paylike/php-api +``` + +You then have to install an HTTP Client. We use HTTPlug as an HTTP Client agnostic adapter. +If you don't have one already installed or don't really know what to do at this point, +you can check the [HTTPlug documentation](http://docs.php-http.org/en/latest/httplug/users.html) +or just trust us with a default client: + +``` bash +$ composer require php-http/guzzle6-adapter +``` + +This will install Guzzle 6 and everything you need to plug it with our package. + + +Usage +----- + +```php +transaction()->findOne($transactionId); +``` + + +TODO +---- + +* Implements remainings API methods +* Tests and documentation + + +Credits +------- + +* Paylike ApS - +* Hubert Moutot - + + +Contributing +------------ + +Please refer to the [CONTRIBUTING.MD](CONTRIBUTING.md) file. + + +License +------- + +This package is released under the MIT License. See the bundled [LICENSE](LICENSE) file for details. diff --git a/client.php b/client.php deleted file mode 100644 index d528dc6..0000000 --- a/client.php +++ /dev/null @@ -1,84 +0,0 @@ -key = $key; - } - - public function setKey( $key ){ - $this->key = $key; - } - - public function getKey(){ - return $this->key; - } - - public function __get( $name ){ - switch ($name) { - case 'transactions': - if (!$this->transactions) - $this->transactions = new PaylikeTransactions($this); - - return $this->transactions; - - default: - throw new BadPropertyException($this, $name); - } - } -} - -class PaylikeTransactions extends PaylikeSubsystem { - public function fetch( $transactionId ){ - return $this->request('GET', '/transactions/'.$transactionId); - } - - public function capture( $transactionId, $opts ){ - return $this->request('POST', '/transactions/'.$transactionId.'/captures', $opts); - } - - public function refund( $transactionId, $opts ){ - return $this->request('POST', '/transactions/'.$transactionId.'/refunds', $opts); - } -} - -class PaylikeSubsystem { - private $paylike; - - public function __construct( $paylike ){ - $this->paylike = $paylike; - } - - protected function request( $verb, $path, $data = null ){ - $c = curl_init(); - - curl_setopt($c, CURLOPT_URL, 'https://api.paylike.io'.$path); - - if ($this->paylike->getKey() !== null) - curl_setopt($c, CURLOPT_USERPWD, ':'.$this->paylike->getKey()); - - if (in_array($verb, [ 'POST', 'PUT', 'PATCH' ])) - curl_setopt($c, CURLOPT_POSTFIELDS, $data); - - if (in_array($verb, [ 'GET', 'POST' ])) - curl_setopt($c, CURLOPT_RETURNTRANSFER, true); - - $raw = curl_exec($c); - - $code = curl_getinfo($c, CURLINFO_HTTP_CODE); - - curl_close($c); - - if ($code < 200 || $code > 299) - return false; - - if ($code === 204) // No Content - return true; - - return json_decode($raw); - } -} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..aebb5ca --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "paylike/php-api", + "type": "library", + "description": "A PHP client for Paylike API", + "keywords": [ + "paylike", + "payment", + "client" + ], + "homepage": "https://github.com/paylike/php-api", + "license": "MIT", + "authors": [ + { + "name": "Paylike and contributors", + "homepage": "https://github.com/paylike/php-api/contributors" + } + ], + "require": { + "php": "^5.6|7.*", + "ext-curl": "*", + "ext-json": "*", + "php-http/client-implementation": "^1.0", + "php-http/client-common": "^1.0", + "php-http/discovery": "^0.7", + "puli/composer-plugin": "^1.0@beta", + "php-http/message": "^1.0", + "php-http/plugins": "^1.0", + "symfony/options-resolver": "^2.8|^3.0", + "guzzlehttp/psr7": "^1.2" + }, + "require-dev": { + "php-http/guzzle6-adapter": "^1.0" + }, + "autoload": { + "psr-4": { + "Paylike\\": "src" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "prefer-stable": true, + "minimum-stability": "dev" +} diff --git a/examples.php b/examples.php deleted file mode 100644 index ca21272..0000000 --- a/examples.php +++ /dev/null @@ -1,16 +0,0 @@ -transactions->capture('put a transaction ID here', [ - // GBP 5.99 - 'currency' => 'GBP', - 'amount' => 599, -]); - -if ($capture) - echo 'Successfully captured transaction'; - -var_dump($paylike->transactions->fetch('put a transaction ID here')); diff --git a/puli.json b/puli.json new file mode 100644 index 0000000..7975bbd --- /dev/null +++ b/puli.json @@ -0,0 +1,132 @@ +{ + "version": "1.0", + "name": "paylike/php-api", + "config": { + "bootstrap-file": "vendor/autoload.php" + }, + "packages": { + "clue/stream-filter": { + "install-path": "vendor/clue/stream-filter", + "installer": "composer" + }, + "guzzlehttp/guzzle": { + "install-path": "vendor/guzzlehttp/guzzle", + "installer": "composer", + "env": "dev" + }, + "guzzlehttp/promises": { + "install-path": "vendor/guzzlehttp/promises", + "installer": "composer", + "env": "dev" + }, + "guzzlehttp/psr7": { + "install-path": "vendor/guzzlehttp/psr7", + "installer": "composer" + }, + "justinrainbow/json-schema": { + "install-path": "vendor/justinrainbow/json-schema", + "installer": "composer" + }, + "paragonie/random_compat": { + "install-path": "vendor/paragonie/random_compat", + "installer": "composer" + }, + "php-http/client-common": { + "install-path": "vendor/php-http/client-common", + "installer": "composer" + }, + "php-http/discovery": { + "install-path": "vendor/php-http/discovery", + "installer": "composer" + }, + "php-http/guzzle6-adapter": { + "install-path": "vendor/php-http/guzzle6-adapter", + "installer": "composer", + "env": "dev" + }, + "php-http/httplug": { + "install-path": "vendor/php-http/httplug", + "installer": "composer" + }, + "php-http/message": { + "install-path": "vendor/php-http/message", + "installer": "composer" + }, + "php-http/message-factory": { + "install-path": "vendor/php-http/message-factory", + "installer": "composer" + }, + "php-http/plugins": { + "install-path": "vendor/php-http/plugins", + "installer": "composer" + }, + "php-http/promise": { + "install-path": "vendor/php-http/promise", + "installer": "composer" + }, + "psr/cache": { + "install-path": "vendor/psr/cache", + "installer": "composer" + }, + "psr/http-message": { + "install-path": "vendor/psr/http-message", + "installer": "composer" + }, + "psr/log": { + "install-path": "vendor/psr/log", + "installer": "composer" + }, + "puli/composer-plugin": { + "install-path": "vendor/puli/composer-plugin", + "installer": "composer" + }, + "puli/discovery": { + "install-path": "vendor/puli/discovery", + "installer": "composer" + }, + "puli/repository": { + "install-path": "vendor/puli/repository", + "installer": "composer" + }, + "puli/url-generator": { + "install-path": "vendor/puli/url-generator", + "installer": "composer" + }, + "ramsey/uuid": { + "install-path": "vendor/ramsey/uuid", + "installer": "composer" + }, + "seld/jsonlint": { + "install-path": "vendor/seld/jsonlint", + "installer": "composer" + }, + "symfony/options-resolver": { + "install-path": "vendor/symfony/options-resolver", + "installer": "composer" + }, + "symfony/process": { + "install-path": "vendor/symfony/process", + "installer": "composer" + }, + "webmozart/assert": { + "install-path": "vendor/webmozart/assert", + "installer": "composer" + }, + "webmozart/expression": { + "install-path": "vendor/webmozart/expression", + "installer": "composer" + }, + "webmozart/glob": { + "install-path": "vendor/webmozart/glob", + "installer": "composer" + }, + "webmozart/json": { + "install-path": "vendor/webmozart/json", + "installer": "composer" + }, + "webmozart/path-util": { + "install-path": "vendor/webmozart/path-util", + "installer": "composer" + } + } +} diff --git a/src/Api/AbstractApi.php b/src/Api/AbstractApi.php new file mode 100644 index 0000000..ead67a6 --- /dev/null +++ b/src/Api/AbstractApi.php @@ -0,0 +1,128 @@ +client = $client; + } + + /** + * Build the final URL to send the request to. + * + * @param string $uri + * + * @return string + */ + protected function buildUrl($uri) + { + if (substr($uri, 0, 1) !== '/') { + $uri = '/'.$uri; + } + + return Paylike::BASE_URL.$uri; + } + + /** + * Process the response. + * + * @param ResponseInterface $response + * + * @return array + */ + protected function processResponse(ResponseInterface $response) + { + $statusCode = $response->getStatusCode(); + + if ($statusCode < 200 || $statusCode > 299) { + throw new PaylikeException(sprintf('Something went wrong with Paylike. Status code: %s. Reason: %s', $statusCode, $response->getReasonPhrase())); + } + + return json_decode($response->getBody(), true); + } + + /** + * Send a request to the Paylike API. + * + * @param string $method + * @param string $uri + * @param array $headers + * @param string $body + * + * @return ResponseInterface + */ + protected function send($method, $uri, array $headers = [], $body = null) + { + return $this->client->sendRequest($this->messageFactory->createRequest( + $method, + $uri, + $headers, + $body + )); + } + + /** + * Send a POST request to the Paylike API. + * + * @param string $uri + * @param array $data + * + * @return array + */ + protected function post($uri, $data) + { + return $this->processResponse($this->client->post($this->buildUrl($uri), ['Content-Type' => 'application/json'], json_encode($data))); + } + + /** + * Send a PUT request to the Paylike API. + * + * @param string $uri + * @param array $data + * + * @return array + */ + protected function put($uri, $data) + { + return $this->processResponse($this->client->put($this->buildUrl($uri), ['Content-Type' => 'application/json'], json_encode($data))); + } + + /** + * Send a DELETE request to the Paylike API. + * + * @param string $uri + * @param array $data + * + * @return array + */ + protected function delete($uri, $data) + { + return $this->processResponse($this->client->delete($this->buildUrl($uri), ['Content-Type' => 'application/json'], json_encode($data))); + } + + /** + * Send a GET request to the Paylike API. + * + * @param string $uri + * + * @return array + */ + protected function get($uri) + { + return $this->processResponse($this->client->get($this->buildUrl($uri))); + } +} diff --git a/src/Api/App.php b/src/Api/App.php new file mode 100644 index 0000000..9f91a1b --- /dev/null +++ b/src/Api/App.php @@ -0,0 +1,89 @@ +setDefined(['name']); + + $options = $resolver->resolve($options); + + $uri = '/apps'; + + return $this->post($uri, $options); + } + + /** + * Fetch the current app. + * + * @return array + */ + public function findOne() + { + $uri = '/me'; + + return $this->get($uri); + } + + /** + * Add an app to a merchant. + * + * @param string $merchantId + * @param array $options + * + * @return array + */ + public function add($merchantId, $options) + { + $resolver = new OptionsResolver(); + $resolver + ->setRequired(['appId']); + + $options = $resolver->resolve($options); + + $uri = '/merchants/'.$merchantId.'/apps'; + + return $this->post($uri, $options); + } + + /** + * Revoke an app from a merchant. + * + * @param string $merchantId + * @param string $appId + * + * @return array + */ + public function revoke($merchantId, $appId) + { + $uri = '/merchants/'.$merchantId.'/apps/'.$appId; + + return $this->delete($uri, $options); + } + + /** + * Fetch all apps. + * + * @param string $merchantId + * @param array $options + * + * @return array + */ + public function find($merchantId, $options) + { + throw new \Exception('Not yet implemented'); + } +} diff --git a/src/Api/Card.php b/src/Api/Card.php new file mode 100644 index 0000000..b659e8a --- /dev/null +++ b/src/Api/Card.php @@ -0,0 +1,30 @@ +setRequired(['transactionId']) + ->setDefined(['notes']); + + $options = $resolver->resolve($options); + + $uri = '/merchants/'.$merchantId.'/cards'; + + return $this->post($uri, $options); + } +} diff --git a/src/Api/Merchant.php b/src/Api/Merchant.php new file mode 100644 index 0000000..de8d2e1 --- /dev/null +++ b/src/Api/Merchant.php @@ -0,0 +1,77 @@ +setRequired(['currency', 'email', 'website', 'descriptor']) + ->setDefined(['name', 'test', 'bank']); + + $options = $resolver->resolve($options); + + $uri = '/merchants'; + + return $this->post($uri, $options); + } + + /** + * Update a merchant. + * + * @param string $merchantId + * @param array $options + * + * @return array + */ + public function update($merchantId, $options) + { + $resolver = new OptionsResolver(); + $resolver + ->setDefined(['name', 'email', 'descriptor']); + + $options = $resolver->resolve($options); + + $uri = '/merchants/'.$merchantId; + + return $this->put($uri, $options); + } + + /** + * Fetch a single merchant. + * + * @param string $merchantId + * + * @return array + */ + public function findOne($merchantId) + { + $uri = '/merchants/'.$merchantId; + + return $this->get($uri); + } + + /** + * Fetch all merchants. + * + * @param string $appId + * @param array $options + * + * @return array + */ + public function find($appId, $options) + { + throw new \Exception('Not yet implemented'); + } +} diff --git a/src/Api/Transaction.php b/src/Api/Transaction.php new file mode 100644 index 0000000..d0beb26 --- /dev/null +++ b/src/Api/Transaction.php @@ -0,0 +1,144 @@ +setRequired(['transactionId', 'amount', 'currency']) + ->setDefined(['descriptor', 'custom']); + + $options = $resolver->resolve($options); + + $uri = '/merchants/'.$merchantId.'/transactions'; + + return $this->post($uri, $options); + } + + /** + * Create a new transaction based on a previously saved card. + * + * @param string $merchantId + * @param array $options + * + * @return array + */ + public function createFromCard($merchantId, $options) + { + $resolver = new OptionsResolver(); + $resolver + ->setRequired(['cardId', 'amount', 'currency']) + ->setDefined(['descriptor', 'custom']); + + $options = $resolver->resolve($options); + + $uri = '/merchants/'.$merchantId.'/transactions'; + + return $this->post($uri, $options); + } + + /** + * Fetch a single transaction. + * + * @param string $transactionId + * + * @return array + */ + public function findOne($transactionId) + { + $uri = '/transactions/'.$transactionId; + + return $this->get($uri); + } + + /** + * Fetch all transactions. + * + * @param string $merchantId + * @param array $options + * + * @return array + */ + public function find($merchantId, $options) + { + throw new \Exception('Not yet implemented'); + } + + /** + * Capture a payment. + * + * @param string $transactionId + * @param array $options + * + * @return array + */ + public function capture($transactionId, $options) + { + $resolver = new OptionsResolver(); + $resolver + ->setRequired(['amount']) + ->setDefined(['currency', 'descriptor']); + + $options = $resolver->resolve($options); + + $uri = '/transactions/'.$transactionId.'/captures'; + + return $this->post($uri, $options); + } + + /** + * Refund a payment. + * + * @param string $transactionId + * @param arrat $options + * + * @return arrat + */ + public function refund($transactionId, $options) + { + $resolver = new OptionsResolver(); + $resolver + ->setRequired(['amount']) + ->setDefined(['descriptor']); + + $options = $resolver->resolve($options); + + $uri = '/transactions/'.$transactionId.'/refunds'; + + return $this->post($uri, $options); + } + + /** + * Voids a complete or partial reserved amount. + * + * @param string $transactionId + * @param array $options + * + * @return array + */ + public function void($transactionId, $options) + { + $resolver = new OptionsResolver(); + $resolver + ->setRequired(['amount']); + + $options = $resolver->resolve($options); + + $uri = '/transactions/'.$transactionId.'/voids'; + + return $this->post($uri, $options); + } +} diff --git a/src/Api/User.php b/src/Api/User.php new file mode 100644 index 0000000..05ce292 --- /dev/null +++ b/src/Api/User.php @@ -0,0 +1,57 @@ +setRequired(['email']); + + $options = $resolver->resolve($options); + + $uri = '/merchants/'.$merchantId.'/users'; + + return $this->post($uri, $options); + } + + /** + * Revoke a user from a merchant. + * + * @param string $merchantId + * @param string $userId + * + * @return array + */ + public function revoke($merchantId, $userId) + { + $uri = '/merchants/'.$merchantId.'/users/'.$userId; + + return $this->delete($uri, $options); + } + + /** + * Fetch all users. + * + * @param string $merchantId + * @param array $options + * + * @return array + */ + public function find($merchantId, $options) + { + throw new \Exception('Not yet implemented'); + } +} diff --git a/src/Exception/PaylikeException.php b/src/Exception/PaylikeException.php new file mode 100644 index 0000000..99a3cba --- /dev/null +++ b/src/Exception/PaylikeException.php @@ -0,0 +1,7 @@ +client = $client; + } + + /** + * Retrieve the Transation API. + * + * @return Transaction + */ + public function transaction() + { + return new Transaction($this->client); + } + + /** + * Retrieve the App API. + * + * @return App + */ + public function app() + { + return new App($this->client); + } + + /** + * Retrieve the Merchant API. + * + * @return Merchant + */ + public function merchant() + { + return new Merchant($this->client); + } + + /** + * Retrieve the Card API. + * + * @return Card + */ + public function card() + { + return new Card($this->client); + } + + /** + * Retrieve the User API. + * + * @return User + */ + public function user() + { + return new User($this->client); + } +} diff --git a/todo.md b/todo.md deleted file mode 100644 index 1063c9b..0000000 --- a/todo.md +++ /dev/null @@ -1,22 +0,0 @@ -# TODO - -A number of features have been introduced to PHP and the community since I -left it back in 2012 so I am probably out of touch with best practices. Any -help and guidance is appreciated. - -- use namespaces? -- put it on a package manager (composer/packagist) -- replace cURL? - - How well is support? Is there a better alternative? - -- add methods - - All supported API methods should be added (see - https://github.com/paylike/node-api and - https://github.com/paylike/api-docs for a reference) - -- Test Windows support - - I know there have been problems related to Windows not working with TLS - 1.3 which can be fixed by setting a flag.