From dd10e2724a7dc2a92e8ddb7033349ae00305291f Mon Sep 17 00:00:00 2001 From: vody105 Date: Mon, 25 Feb 2019 15:11:35 +0100 Subject: [PATCH] bump --- .docs/README.md | 112 ++++++++++++++++++++++++ .editorconfig | 16 ++++ .gitattributes | 10 +++ .gitignore | 6 ++ .travis.yml | 63 +++++++++++++ README.md | 62 +++++++++++++ composer.json | 57 ++++++++++++ phpstan.neon | 5 ++ ruleset.xml | 32 +++++++ src/Auth/AccessTokenClient.php | 61 +++++++++++++ src/Auth/AccessTokenSessionProvider.php | 75 ++++++++++++++++ src/Auth/IAccessTokenProvider.php | 13 +++ src/Client/AbstractClient.php | 48 ++++++++++ src/Client/AccountClient.php | 20 +++++ src/Client/MessageClient.php | 81 +++++++++++++++++ src/Config.php | 30 +++++++ src/DI/GoSmsExtension.php | 58 ++++++++++++ src/Entity/AccessToken.php | 73 +++++++++++++++ src/Entity/Message.php | 78 +++++++++++++++++ src/Exception/ClientException.php | 8 ++ src/Exception/RuntimeException.php | 10 +++ src/Http/GuzzletteClient.php | 26 ++++++ src/Http/IHttpClient.php | 13 +++ tests/.coveralls.yml | 4 + tests/.gitignore | 10 +++ tests/bootstrap.php | 13 +++ tests/cases/DI/GosmsExtension.phpt | 38 ++++++++ 27 files changed, 1022 insertions(+) create mode 100644 .docs/README.md create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpstan.neon create mode 100644 ruleset.xml create mode 100644 src/Auth/AccessTokenClient.php create mode 100644 src/Auth/AccessTokenSessionProvider.php create mode 100644 src/Auth/IAccessTokenProvider.php create mode 100644 src/Client/AbstractClient.php create mode 100644 src/Client/AccountClient.php create mode 100644 src/Client/MessageClient.php create mode 100644 src/Config.php create mode 100644 src/DI/GoSmsExtension.php create mode 100644 src/Entity/AccessToken.php create mode 100644 src/Entity/Message.php create mode 100644 src/Exception/ClientException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Http/GuzzletteClient.php create mode 100644 src/Http/IHttpClient.php create mode 100644 tests/.coveralls.yml create mode 100644 tests/.gitignore create mode 100644 tests/bootstrap.php create mode 100644 tests/cases/DI/GosmsExtension.phpt diff --git a/.docs/README.md b/.docs/README.md new file mode 100644 index 0000000..2eceb9b --- /dev/null +++ b/.docs/README.md @@ -0,0 +1,112 @@ +# GoSMS.cz Api Integration + +## Content + +- [Requirements - what do you need](#requirements) +- [Installation - how to register an extension](#installation) +- [Usage - how to use it](#usage) + + +## Requirements + +Create account on GoSMS.cz and copy clientId and clientSecret from administration. + +If you use default HTTP client, you need to install and register [guzzlette](https://github.com/contributte/guzzlette/) extension. + +Default AccessTokenSessionProvider uses [nette/http](https://github.com/nette/http) as its session handler; + +* **clientId** +* **clientSecret** +* **httpClient** +* **accessTokenProvider** + + +## Installation + +```yaml +extensions: + guzzlette: Contributte\Guzzlette\DI\GuzzleExtension # optional for default HTTP client + gosms: App\Model\GoSMS\DI\GoSmsExtension + +gosms: + # Required + clientId: 10185_2jz2pog5jtgkocs0oc0008kow8kkwsccsk8c8ogogggs44cskg + clientSecret: caajrzi80zs4cwgg8400swwo8wgc4kook0s8s48kw8s00sgws + + # Optional + httpClient: + accessTokenProvider: +``` + + +## Usage + +We prepared 2 clients: `AccountClient` and `MessageClient`. They mirror methods from [GoSMS.cz Api documentation](https://doc.gosms.cz/) so read documentation first. All methods except `send` return raw data as received from GoSMS.cz api. + +All methods throw ClientException with error message and code as response status when response status is not 200/201; + +### AccountClient + +* `detail()` - [Organization detail](https://doc.gosms.cz/#detail-organizace) + +### MessageClient + +* `send(Contributte\Gosms\Entity\Message)` - [Sends message](https://doc.gosms.cz/#jak-poslat-zpravu) + * Unfortunately GoSMS.cz does not include newly created message ID. We parse their response for you and include it in result object as `parsedId`. This id is needed by other methods. +* `test(Contributte\Gosms\Entity\Message)` - [Test creating message withou sending](https://doc.gosms.cz/#testovaci-vytvoreni-zpravy-bez-odeslani) +* `detail(string $id)` - [Sent message detail](https://doc.gosms.cz/#detail-zpravy) +* `replies(string $id)` - [List sent message replies](https://doc.gosms.cz/#seznam-odpovedi-u-zpravy) +* `delete(string $id)` - [Delete sent message](https://doc.gosms.cz/#smazani-zpravy) + + +```php +messageClient = $messageClient; + } + + public function handleSend(): void + { + $result = NULL; + $msg = new Message('Message body', ['+420711555444'], 1); + + try { + $result = $this->messageClient->send($msg); + } catch (ClientException $e) { + // Response status + $e->getCode(); + // Response body + $e->getMessage(); + exit; + } + + // Process successful result as you like + $this->saveSentMessage($result->parsedId, $msg); + } + +} + +``` + + +### AccessTokenProvider + +We have two build in AccessToken providers; + +* `AccessTokenClient` - fetches and stores accessToken for 1 request +* `AccessTokenSessionProvider` - fetches and stores accessToken in session until access token expires diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..575834a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = tab +tab_width = 4 + +[{*.json,*.yml,*.md}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c52e7b9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Not archived +.docs export-ignore +tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpstan.neon export-ignore +README.md export-ignore +ruleset.xml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4b95ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# IDE +/.idea + +# Composer +/vendor +/composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8203d96 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,63 @@ +language: php +php: + - 7.2 + - 7.3 + +before_install: + # Turn off XDebug + - phpenv config-rm xdebug.ini || return 0 + +install: + # Composer + - travis_retry composer install --no-progress --prefer-dist + +script: + # Tests + - composer run-script tests + +after_failure: + # Print *.actual content + - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done + +jobs: + include: + - env: title="Lowest Dependencies 7.1" + php: 7.2 + install: + - travis_retry composer update --no-progress --prefer-dist --prefer-lowest + script: + - composer run-script tests + + - stage: Quality Assurance + php: 7.2 + script: + - composer run-script qa + + - stage: Phpstan + php: 7.2 + script: + - composer run-script phpstan + + - stage: Test Coverage + if: branch = master AND type = push + php: 7.2 + script: + - composer run-script coverage + after_script: + - wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.1.0/php-coveralls.phar + - php php-coveralls.phar --verbose --config tests/.coveralls.yml + + - stage: Outdated Dependencies + if: branch = master AND type = cron + php: 7.2 + script: + - composer outdated --direct --strict + + allow_failures: + - stage: Test Coverage + +sudo: false + +cache: + directories: + - $HOME/.composer/cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..940aba1 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# GoSMS.cz Api Integration + +Easy-to-use [GoSMS.cz](https://gosms.cz) API integration for [`Nette Framework`](https://github.com/nette/). + +----- + +[![Build Status](https://img.shields.io/travis/contributte/gosms.svg?style=flat-square)](https://travis-ci.org/contributte/gosms) +[![Code coverage](https://img.shields.io/coveralls/contributte/gosms.svg?style=flat-square)](https://coveralls.io/r/contributte/gosms) +[![Licence](https://img.shields.io/packagist/l/contributte/gosms.svg?style=flat-square)](https://packagist.org/packages/contributte/gosms) +[![Downloads this Month](https://img.shields.io/packagist/dm/contributte/gosms.svg?style=flat-square)](https://packagist.org/packages/contributte/gosms) +[![Downloads total](https://img.shields.io/packagist/dt/contributte/gosms.svg?style=flat-square)](https://packagist.org/packages/contributte/gosms) +[![Latest stable](https://img.shields.io/packagist/v/contributte/gosms.svg?style=flat-square)](https://packagist.org/packages/contributte/gosms) +[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan) + +## Discussion / Help + +[![Join the chat](https://img.shields.io/gitter/room/contributte/contributte.svg?style=flat-square)](http://bit.ly/ctteg) + +## Install + +``` +composer require contributte/gosms +``` + +## Versions + +| State | Version | Branch | PHP | +|-------------|---------|----------|----------| +| stable | `^0.1` | `master` | `>= 7.1` | + +## Overview + +- [Requirements - what do do you need](https://github.com/contributte/gosms/blob/master/.docs/README.md#requirements) +- [Installation - how to register an extension](https://github.com/contributte/gosms/blob/master/.docs/README.md#Installation) +- [Usage - how to use it](https://github.com/contributte/gosms/blob/master/.docs/README.md#usage) + +## Maintainers + + + + + + + + +
+ + + +
+ Milan Felix Šulc +
+ + + +
+ Filip Šuška +
+ +----- + +Thank you for testing, reporting and contributing. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1bbd7ec --- /dev/null +++ b/composer.json @@ -0,0 +1,57 @@ +{ + "name": "contributte/gosms", + "description": "Full featured GoSMS.cz HTTP client for nette", + "keywords": ["contributte", "gosms", "nette", "sms"], + "type": "library", + "license": "MIT", + "homepage": "https://github.com/contributte/gosms", + "authors": [ + { + "name": "Filip Šuška" + } + ], + "require": { + "php": ">= 7.2", + "ext-json": "*", + "guzzlehttp/psr7": "^1.4", + "nette/utils": "~2.4 || ~3.0" + }, + "require-dev": { + "nette/di": "~2.4", + "contributte/guzzlette": "~2.1", + "nette/http": "~2.4.10", + "ninjify/qa": "^0.8.0", + "ninjify/nunjuck": "^0.2.0", + "mockery/mockery": "^1.1.0", + "phpstan/phpstan-shim": "^0.11", + "phpstan/phpstan-deprecation-rules": "^0.11", + "phpstan/phpstan-nette": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11" + }, + "autoload": { + "psr-4": { + "Contributte\\Gosms\\": "src" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "qa": [ + "linter src tests", + "codesniffer src tests" + ], + "tests": [ + "tester -s -p php --colors 1 -C tests/cases" + ], + "coverage": [ + "tester -s -p phpdbg --colors 1 -C --coverage ./coverage.xml --coverage-src ./src tests/cases" + ], + "phpstan": [ + "vendor/bin/phpstan analyse -l max -c phpstan.neon src" + ] + }, + "suggest": { + "contributte/guzzlette": "As default HttpClient", + "nette/http": "As session handler in AccessTokenSessionProvider" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..9e9adeb --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan-nette/extension.neon + - vendor/phpstan/phpstan-nette/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..c5640ac --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + *tests/cases/* + + + + *tests/cases/* + + + + *tests/fixtures/* + + + + /tests/tmp + diff --git a/src/Auth/AccessTokenClient.php b/src/Auth/AccessTokenClient.php new file mode 100644 index 0000000..8cb10fc --- /dev/null +++ b/src/Auth/AccessTokenClient.php @@ -0,0 +1,61 @@ +client = $client; + } + + public function getAccessToken(Config $config): AccessToken + { + // Store AccessToken at least for one request + if ($this->accessToken === null || $this->accessToken->getExpiresAt()->modify('- 5 minutes') < new DateTimeImmutable()) { + $this->accessToken = $this->generateAccessToken($config); + } + + return $this->accessToken; + } + + protected function generateAccessToken(Config $config): AccessToken + { + $body = sprintf('client_id=%s&client_secret=%s&grant_type=client_credentials', $config->getClientId(), $config->getClientSecret()); + + $response = $this->client->sendRequest( + new Request('POST', self::URL, ['Content-Type' => 'application/x-www-form-urlencoded'], $body) + ); + + if ($response->getStatusCode() !== 200) { + throw new ClientException($response->getBody()->getContents(), $response->getStatusCode()); + } + + $data = Json::decode($response->getBody()->getContents()); + + return new AccessToken( + $data->access_token, + $data->expires_in, + $data->token_type, + $data->scope + ); + } + +} diff --git a/src/Auth/AccessTokenSessionProvider.php b/src/Auth/AccessTokenSessionProvider.php new file mode 100644 index 0000000..e967fa5 --- /dev/null +++ b/src/Auth/AccessTokenSessionProvider.php @@ -0,0 +1,75 @@ +session = $session; + } + + public function getAccessToken(Config $config): AccessToken + { + $token = $this->accessToken; + + // If we have it in session we retrieve it + if ($this->accessToken === null) { + $token = $this->accessToken = $this->getSessionAccessToken(); + } + + $this->accessToken = parent::getAccessToken($config); + + if ($token === null || $token->getAccessToken() !== $this->accessToken->getAccessToken()) { + $this->setSessionAccessToken($this->accessToken); + } + + return $this->accessToken; + } + + private function getSessionAccessToken(): ?AccessToken + { + if (!$this->session->exists()) return null; + + $section = $this->session->getSection(self::SESSION_NAME); + if (!isset($section['accessToken'])) return null; + + /** @var DateTimeImmutable $expiresAt */ + $expiresAt = DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $section['expiresAt']); + + return new AccessToken( + $section['accessToken'], + $section['expiresIn'], + $section['tokenType'], + $section['scope'], + $expiresAt + ); + } + + private function setSessionAccessToken(AccessToken $token): void + { + $section = $this->session->getSection(self::SESSION_NAME); + $section->setExpiration($token->getExpiresAt()); + + foreach ($token->toArray() as $k => $v) { + $section[$k] = $v instanceof DateTimeImmutable ? $v->format(DateTimeImmutable::ATOM) : $v; + } + } + +} diff --git a/src/Auth/IAccessTokenProvider.php b/src/Auth/IAccessTokenProvider.php new file mode 100644 index 0000000..e4ce9fc --- /dev/null +++ b/src/Auth/IAccessTokenProvider.php @@ -0,0 +1,13 @@ +client = $client; + $this->config = $config; + $this->accessTokenProvider = $accessTokenProvider; + } + + protected function doRequest(RequestInterface $request): ResponseInterface + { + $token = $this->accessTokenProvider->getAccessToken($this->config); + $request = $request->withHeader('Authorization', 'Bearer ' . $token->getAccessToken()); + + return $this->client->sendRequest($request); + } + + protected function assertResponse(ResponseInterface $response, int $code = 200): void + { + if ($response->getStatusCode() !== $code) { + throw new ClientException($response->getBody()->getContents(), $response->getStatusCode()); + } + } + +} diff --git a/src/Client/AccountClient.php b/src/Client/AccountClient.php new file mode 100644 index 0000000..3dda78b --- /dev/null +++ b/src/Client/AccountClient.php @@ -0,0 +1,20 @@ +doRequest(new Request('GET', self::BASE_URL . '/')); + + $this->assertResponse($response); + + return Json::decode($response->getBody()->getContents()); + } + +} diff --git a/src/Client/MessageClient.php b/src/Client/MessageClient.php new file mode 100644 index 0000000..f068cd7 --- /dev/null +++ b/src/Client/MessageClient.php @@ -0,0 +1,81 @@ +toArray()); + + $response = $this->doRequest( + new Request('POST', self::BASE_URL, ['Content-Type' => 'application/json'], $body) + ); + + $this->assertResponse($response, 201); + + $res = Json::decode($response->getBody()->getContents()); + $res->parsedId = str_replace('messages/', '', Strings::match($res->link, '~messages/\d+~')[0]); + + return $res; + } + + public function test(Message $message): object + { + $body = Json::encode($message->toArray()); + + $response = $this->doRequest( + new Request('POST', self::BASE_URL . '/test', ['Content-Type' => 'application/json'], $body) + ); + + $this->assertResponse($response); + + return Json::decode($response->getBody()->getContents()); + } + + public function detail(string $id): object + { + $url = sprintf('%s/%d', self::BASE_URL, $id); + + $response = $this->doRequest( + new Request('GET', $url) + ); + + $this->assertResponse($response); + + return Json::decode($response->getBody()->getContents()); + } + + public function replies(string $id): object + { + $url = sprintf('%s/%d/replies', self::BASE_URL, $id); + + $response = $this->doRequest( + new Request('GET', $url) + ); + + $this->assertResponse($response); + + return Json::decode($response->getBody()->getContents()); + } + + public function delete(string $id): void + { + $url = sprintf('%s/%d', self::BASE_URL, $id); + + $response = $this->doRequest( + new Request('DELETE', $url) + ); + + $this->assertResponse($response); + } + +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..779a57e --- /dev/null +++ b/src/Config.php @@ -0,0 +1,30 @@ +clientId = $clientId; + $this->clientSecret = $clientSecret; + } + + public function getClientId(): string + { + return $this->clientId; + } + + public function getClientSecret(): string + { + return $this->clientSecret; + } + +} diff --git a/src/DI/GoSmsExtension.php b/src/DI/GoSmsExtension.php new file mode 100644 index 0000000..c989301 --- /dev/null +++ b/src/DI/GoSmsExtension.php @@ -0,0 +1,58 @@ + null, + 'clientSecret' => null, + 'httpClient' => GuzzletteClient::class, + 'accessTokenProvider' => AccessTokenSessionProvider::class, + ]; + + public function loadConfiguration(): void + { + $config = $this->validateConfig($this->defaults); + $builder = $this->getContainerBuilder(); + + Validators::assertField($config, 'clientId', 'string|number'); + Validators::assertField($config, 'clientSecret', 'string'); + Validators::assertField($config, 'httpClient', 'string'); + Validators::assertField($config, 'accessTokenProvider', 'string'); + + $configStatement = new Statement(Config::class, [ + $config['clientId'], + $config['clientSecret'], + ]); + + // HttpClient + $hc = $builder->addDefinition($this->prefix('httpClient')); + Compiler::loadDefinition($hc, $config['httpClient']); + + // AccessTokenProvider + $atp = $builder->addDefinition($this->prefix('accessTokenProvider')); + Compiler::loadDefinition($atp, $config['accessTokenProvider']); + + // Message Client + $builder->addDefinition($this->prefix('message')) + ->setFactory(MessageClient::class, [$configStatement]); + + // Account Client + $builder->addDefinition($this->prefix('account')) + ->setFactory(AccountClient::class, [$configStatement]); + } + +} diff --git a/src/Entity/AccessToken.php b/src/Entity/AccessToken.php new file mode 100644 index 0000000..7446379 --- /dev/null +++ b/src/Entity/AccessToken.php @@ -0,0 +1,73 @@ +accessToken = $accessToken; + $this->expiresIn = $expiresIn; + $this->tokenType = $tokenType; + $this->scope = $scope; + $this->expiresAt = $expiresAt ?? new DateTimeImmutable(sprintf('+%d seconds', $expiresIn)); + } + + public function getAccessToken(): string + { + return $this->accessToken; + } + + public function getExpiresIn(): int + { + return $this->expiresIn; + } + + public function getTokenType(): string + { + return $this->tokenType; + } + + public function getScope(): string + { + return $this->scope; + } + + public function getExpiresAt(): DateTimeImmutable + { + return $this->expiresAt; + } + + /** + * @return mixed[] + */ + public function toArray(): array + { + return [ + 'accessToken' => $this->accessToken, + 'expiresIn' => $this->expiresIn, + 'tokenType' => $this->tokenType, + 'scope' => $this->scope, + 'expiresAt' => $this->expiresAt, + ]; + } + +} diff --git a/src/Entity/Message.php b/src/Entity/Message.php new file mode 100644 index 0000000..5cef702 --- /dev/null +++ b/src/Entity/Message.php @@ -0,0 +1,78 @@ +message = $message; + $this->recipients = $recipients; + $this->channel = $channel; + } + + public function getMessage(): string + { + return $this->message; + } + + /** + * @return mixed[] + */ + public function getRecipients(): array + { + return $this->recipients; + } + + public function getChannel(): int + { + return $this->channel; + } + + public function getExpectedSendStart(): ?DateTimeImmutable + { + return $this->expectedSendStart; + } + + public function setExpectedSendStart(?DateTimeImmutable $expectedSendStart): void + { + $this->expectedSendStart = $expectedSendStart; + } + + /** + * @return mixed[] + */ + public function toArray(): array + { + $arr = [ + 'message' => $this->message, + 'recipients' => $this->recipients, + 'channel' => $this->channel, + ]; + + if ($this->expectedSendStart !== null) { + $arr['expectedSendStart'] = $this->expectedSendStart->format(DateTimeImmutable::ATOM); + } + + return $arr; + } + +} diff --git a/src/Exception/ClientException.php b/src/Exception/ClientException.php new file mode 100644 index 0000000..8081934 --- /dev/null +++ b/src/Exception/ClientException.php @@ -0,0 +1,8 @@ +client = $clientFactory->createClient(['timeout' => 30, 'http_errors' => false]); + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->client->send($request); + } + +} diff --git a/src/Http/IHttpClient.php b/src/Http/IHttpClient.php new file mode 100644 index 0000000..71e7101 --- /dev/null +++ b/src/Http/IHttpClient.php @@ -0,0 +1,13 @@ +load(function (Compiler $compiler): void { + $compiler->addExtension('guz', new GuzzleExtension()); + $compiler->addExtension('http', new HttpExtension()); + $compiler->addExtension('session', new SessionExtension()); + $compiler->addExtension('gosms', new GoSmsExtension()) + ->addConfig([ + 'gosms' => [ + 'clientId' => 'X', + 'clientSecret' => 'Y', + ], + ]); + }, 1); + + /** @var Container $container */ + $container = new $class(); + + // Service created + Assert::type(MessageClient::class, $container->getService('gosms.message')); + Assert::type(AccountClient::class, $container->getService('gosms.account')); +});