Skip to content

Commit

Permalink
Replace session with cache for storing accessTokens
Browse files Browse the repository at this point in the history
  • Loading branch information
vody105 committed Aug 24, 2019
1 parent f3d31c3 commit 9791a28
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 105 deletions.
5 changes: 2 additions & 3 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down Expand Up @@ -76,7 +76,6 @@ final class SendPaymentsControl extends BaseControl
public function __construct(MessageClient $messageClient)
{
parent::__construct();
$this->messageClient = $messageClient;
}
Expand Down Expand Up @@ -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
71 changes: 71 additions & 0 deletions src/Auth/AccessTokenCacheProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace Contributte\Gosms\Auth;

use Contributte\Gosms\Config;
use Contributte\Gosms\Entity\AccessToken;
use Contributte\Gosms\Http\IHttpClient;
use DateTimeImmutable;
use Nette\Caching\Cache;
use Nette\Caching\IStorage;

class AccessTokenCacheProvider extends AccessTokenClient
{

private const CACHE_NAMESPACE = 'Contributte/Gosms';

/** @var AccessToken|null */
protected $accessToken;

/** @var Cache */
protected $cache;

public function __construct(IHttpClient $client, IStorage $storage)
{
parent::__construct($client);
$this->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(),
]);
}

}
3 changes: 1 addition & 2 deletions src/Auth/AccessTokenClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}

Expand Down
75 changes: 0 additions & 75 deletions src/Auth/AccessTokenSessionProvider.php

This file was deleted.

41 changes: 21 additions & 20 deletions src/DI/GoSmsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/Entity/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
];
}

Expand Down
31 changes: 27 additions & 4 deletions tests/cases/DI/GosmsExtension.phpt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<?php declare(strict_types = 1);

use Contributte\Gosms\Auth\AccessTokenClient;
use Contributte\Gosms\Client\AccountClient;
use Contributte\Gosms\Client\MessageClient;
use Contributte\Gosms\DI\GoSmsExtension;
use Contributte\Gosms\Http\GuzzletteClient;
use Contributte\Guzzlette\DI\GuzzleExtension;
use Nette\Bridges\HttpDI\HttpExtension;
use Nette\Bridges\HttpDI\SessionExtension;
use Nette\Bridges\CacheDI\CacheExtension;
use Nette\DI\Compiler;
use Nette\DI\Container;
use Nette\DI\ContainerLoader;
Expand All @@ -18,8 +19,7 @@ test(function (): void {
$loader = new ContainerLoader(TEMP_DIR, true);
$class = $loader->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' => [
Expand All @@ -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'));
});
20 changes: 20 additions & 0 deletions tests/cases/Entity/AccessToken.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

use Contributte\Gosms\Entity\AccessToken;
use Tester\Assert;

require_once __DIR__ . '/../../bootstrap.php';

test(function (): void {
Assert::false(
(new AccessToken('foo', 3600, 'asdf', 'scope'))->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()
);
});

0 comments on commit 9791a28

Please sign in to comment.