From 9791a28fb36f504e8b0c715c59150f3ab9925688 Mon Sep 17 00:00:00 2001 From: vody105 Date: Sat, 24 Aug 2019 16:17:44 +0200 Subject: [PATCH] Replace session with cache for storing accessTokens --- .docs/README.md | 5 +- src/Auth/AccessTokenCacheProvider.php | 71 +++++++++++++++++++++++ src/Auth/AccessTokenClient.php | 3 +- src/Auth/AccessTokenSessionProvider.php | 75 ------------------------- src/DI/GoSmsExtension.php | 41 +++++++------- src/Entity/AccessToken.php | 7 ++- tests/cases/DI/GosmsExtension.phpt | 31 ++++++++-- tests/cases/Entity/AccessToken.phpt | 20 +++++++ 8 files changed, 148 insertions(+), 105 deletions(-) create mode 100644 src/Auth/AccessTokenCacheProvider.php delete mode 100644 src/Auth/AccessTokenSessionProvider.php create mode 100644 tests/cases/Entity/AccessToken.phpt diff --git a/.docs/README.md b/.docs/README.md index 2eceb9b..1939d47 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -13,7 +13,7 @@ Create account on GoSMS.cz and copy clientId and clientSecret from administratio 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; +GoSMS.cz access tokens are valid for 3600 seconds. Default AccessTokenCacheProvider stores them in cache using [nette/caching](https://github.com/nette/caching); * **clientId** * **clientSecret** @@ -76,7 +76,6 @@ final class SendPaymentsControl extends BaseControl public function __construct(MessageClient $messageClient) { - parent::__construct(); $this->messageClient = $messageClient; } @@ -109,4 +108,4 @@ final class SendPaymentsControl extends BaseControl 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 +* `AccessTokenCacheProvider` - fetches and stores accessToken in cache until access token expires diff --git a/src/Auth/AccessTokenCacheProvider.php b/src/Auth/AccessTokenCacheProvider.php new file mode 100644 index 0000000..a5cb1d3 --- /dev/null +++ b/src/Auth/AccessTokenCacheProvider.php @@ -0,0 +1,71 @@ +cache = new Cache($storage, self::CACHE_NAMESPACE); + } + + public function getAccessToken(Config $config): AccessToken + { + $token = $this->accessToken; + + // If we have it in cache we retrieve it + if ($this->accessToken === null) { + $token = $this->accessToken = $this->loadAccessToken($config); + } + + $this->accessToken = parent::getAccessToken($config); + + if ($token === null || $token->getAccessToken() !== $this->accessToken->getAccessToken()) { + $this->saveAccessToken($config, $this->accessToken); + } + + return $this->accessToken; + } + + private function loadAccessToken(Config $config): ?AccessToken + { + $token = $this->cache->load($config->getClientId()); + if ($token === null) return null; + + /** @var DateTimeImmutable $expiresAt */ + $expiresAt = DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $token['expiresAt']); + + return new AccessToken( + $token['accessToken'], + $token['expiresIn'], + $token['tokenType'], + $token['scope'], + $expiresAt + ); + } + + private function saveAccessToken(Config $config, AccessToken $token): void + { + $this->cache->save($config->getClientId(), $token->toArray(), [ + Cache::EXPIRE => $token->getExpiresAt()->getTimestamp(), + ]); + } + +} diff --git a/src/Auth/AccessTokenClient.php b/src/Auth/AccessTokenClient.php index 8cb10fc..0f9a7bf 100644 --- a/src/Auth/AccessTokenClient.php +++ b/src/Auth/AccessTokenClient.php @@ -6,7 +6,6 @@ use Contributte\Gosms\Entity\AccessToken; use Contributte\Gosms\Exception\ClientException; use Contributte\Gosms\Http\IHttpClient; -use DateTimeImmutable; use GuzzleHttp\Psr7\Request; use Nette\Utils\Json; @@ -29,7 +28,7 @@ public function __construct(IHttpClient $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()) { + if ($this->accessToken === null || $this->accessToken->isExpired()) { $this->accessToken = $this->generateAccessToken($config); } diff --git a/src/Auth/AccessTokenSessionProvider.php b/src/Auth/AccessTokenSessionProvider.php deleted file mode 100644 index e967fa5..0000000 --- a/src/Auth/AccessTokenSessionProvider.php +++ /dev/null @@ -1,75 +0,0 @@ -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/DI/GoSmsExtension.php b/src/DI/GoSmsExtension.php index 07fd3ab..2640793 100644 --- a/src/DI/GoSmsExtension.php +++ b/src/DI/GoSmsExtension.php @@ -2,46 +2,47 @@ namespace Contributte\Gosms\DI; -use Contributte\Gosms\Auth\AccessTokenSessionProvider; +use Contributte\Gosms\Auth\AccessTokenCacheProvider; use Contributte\Gosms\Client\AccountClient; use Contributte\Gosms\Client\MessageClient; use Contributte\Gosms\Config; use Contributte\Gosms\Http\GuzzletteClient; -use Nette\DI\Compiler; +use Nette; use Nette\DI\CompilerExtension; use Nette\DI\Statement; -use Nette\Utils\Validators; +use Nette\Schema\Expect; +use stdClass; +/** + * @property-read stdClass $config + */ class GoSmsExtension extends CompilerExtension { - /** @var mixed[] */ - private $defaults = [ - 'clientId' => null, - 'clientSecret' => null, - 'httpClient' => GuzzletteClient::class, - 'accessTokenProvider' => AccessTokenSessionProvider::class, - ]; + public function getConfigSchema(): Nette\Schema\Schema + { + return Expect::structure([ + 'clientId' => Expect::string()->required(), + 'clientSecret' => Expect::string()->required(), + 'httpClient' => Expect::anyOf(Expect::string(), Expect::array(), Expect::type(Statement::class)), + 'accessTokenProvider' => Expect::anyOf(Expect::string(), Expect::array(), Expect::type(Statement::class)), + ]); + } public function loadConfiguration(): void { - $config = $this->validateConfig($this->defaults); + $config = $this->config; $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'], + $config->clientId, + $config->clientSecret, ]); // HttpClient, AccessTokenProvider $this->compiler->loadDefinitionsFromConfig([ - $this->prefix('httpClient') => $config['httpClient'], - $this->prefix('accessTokenProvider') => $config['accessTokenProvider'], + $this->prefix('httpClient') => $config->httpClient ?? GuzzletteClient::class, + $this->prefix('accessTokenProvider') => $config->accessTokenProvider ?? AccessTokenCacheProvider::class, ]); // Message Client diff --git a/src/Entity/AccessToken.php b/src/Entity/AccessToken.php index 7446379..8c5d42e 100644 --- a/src/Entity/AccessToken.php +++ b/src/Entity/AccessToken.php @@ -31,6 +31,11 @@ public function __construct(string $accessToken, int $expiresIn, string $tokenTy $this->expiresAt = $expiresAt ?? new DateTimeImmutable(sprintf('+%d seconds', $expiresIn)); } + public function isExpired(): bool + { + return $this->expiresAt->modify('-5 minutes')->getTimestamp() < time(); + } + public function getAccessToken(): string { return $this->accessToken; @@ -66,7 +71,7 @@ public function toArray(): array 'expiresIn' => $this->expiresIn, 'tokenType' => $this->tokenType, 'scope' => $this->scope, - 'expiresAt' => $this->expiresAt, + 'expiresAt' => $this->expiresAt->format(DateTimeImmutable::ATOM), ]; } diff --git a/tests/cases/DI/GosmsExtension.phpt b/tests/cases/DI/GosmsExtension.phpt index 2e59b94..907260b 100644 --- a/tests/cases/DI/GosmsExtension.phpt +++ b/tests/cases/DI/GosmsExtension.phpt @@ -1,11 +1,12 @@ load(function (Compiler $compiler): void { $compiler->addExtension('guz', new GuzzleExtension()); - $compiler->addExtension('http', new HttpExtension()); - $compiler->addExtension('session', new SessionExtension()); + $compiler->addExtension('caching', new CacheExtension(TMP_DIR)); $compiler->addExtension('gosms', new GoSmsExtension()) ->addConfig([ 'gosms' => [ @@ -36,3 +36,26 @@ test(function (): void { Assert::type(MessageClient::class, $container->getService('gosms.message')); Assert::type(AccountClient::class, $container->getService('gosms.account')); }); + +test(function (): void { + $loader = new ContainerLoader(TEMP_DIR, true); + $class = $loader->load(function (Compiler $compiler): void { + $compiler->addExtension('guz', new GuzzleExtension()); + $compiler->addExtension('gosms', new GoSmsExtension()) + ->addConfig([ + 'gosms' => [ + 'clientId' => 'X', + 'clientSecret' => 'Y', + 'httpClient' => GuzzletteClient::class, + 'accessTokenProvider' => ['type' => AccessTokenClient::class], + ], + ]); + }, 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')); +}); diff --git a/tests/cases/Entity/AccessToken.phpt b/tests/cases/Entity/AccessToken.phpt new file mode 100644 index 0000000..0ab3d4f --- /dev/null +++ b/tests/cases/Entity/AccessToken.phpt @@ -0,0 +1,20 @@ +isExpired() + ); + + Assert::false( + (new AccessToken('foo', 3600, 'asdf', 'scope', new DateTimeImmutable('+ 40 minutes')))->isExpired() + ); + + Assert::true( + (new AccessToken('foo', 3600, 'asdf', 'scope', new DateTimeImmutable('+2 minutes')))->isExpired() + ); +});