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.