Skip to content

Commit

Permalink
feat(Exchange)!: improve store and invalid cache
Browse files Browse the repository at this point in the history
  • Loading branch information
h4kuna committed Feb 16, 2024
1 parent 9a515cc commit 9e003de
Show file tree
Hide file tree
Showing 45 changed files with 1,389 additions and 1,037 deletions.
83 changes: 50 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ Exchange
[![Total Downloads](https://poser.pugx.org/h4kuna/exchange/downloads?format=flat)](https://packagist.org/packages/h4kuna/exchange)
[![License](https://poser.pugx.org/h4kuna/exchange/license?format=flat)](https://packagist.org/packages/h4kuna/exchange)

Exchange is PHP script works with currencies. You can convert price, format add VAT or only render exchange rates.
Exchange is PHP script works with currencies. You can convert price.

Here is [changelog](changelog.md).

## Extension for framework

- [Nette extension](//github.com/h4kuna/exchange-nette)

Installation via composer
-----------------------
## Installation via composer

```sh
$ composer require h4kuna/exchange
```
Optional packages
```sh
$ composer require guzzlehttp/guzzle guzzlehttp/psr7 h4kuna/dir nette/caching
```

Support PSR-6 for cache.

## How to use

Expand All @@ -31,29 +36,42 @@ For example define own exchange rates:
- 20 CZK = 1 USD

```php
use h4kuna\CriticalCache;use h4kuna\Exchange;

$cacheFactory = new CriticalCache\CacheFactory('exchange');

$exchangeFactory = new Exchange\ExchangeFactory(
from: 'eur',
to: 'usd',
allowedCurrencies: [
'CZK',
'USD',
'eur', // lower case will be changed to upper case
],
cacheFactory: $cacheFactory
);
use h4kuna\Exchange\Currency\Property;
use h4kuna\Exchange\Driver\Cnb\Day;
use h4kuna\Exchange\Exchange;
use h4kuna\Exchange\ExchangeFactory;
use h4kuna\Exchange\RatingList\CacheEntity;
use h4kuna\Exchange\RatingList\RatingList;

{ # by factory
$exchangeFactory = new ExchangeFactory(
from: 'eur',
to: 'usd',
allowedCurrencies: [
'CZK',
'USD',
'eur', // lower case will be changed to upper case
],
);

$exchange = $exchangeFactory->create();
}

$exchange = $exchangeFactory->create();
{ # custom RatingList
$ratingList = new RatingList(new DateTimeImmutable(), new DateTimeImmutable(), null, [
'EUR' => new Property(1, 25.0, 'EUR'),
'USD' => new Property(1, 20.0, 'USD'),
'CZK' => new Property(1, 1.0, 'CZK'),
]);
$exchange = new Exchange('EUR', $ratingList, 'USD');
}

echo $exchange->change(100); // EUR -> USD = 125.0
echo $exchange->change(100) . PHP_EOL; // EUR -> USD = 125.0

// use only upper case
echo $exchange->change(100, 'CZK'); // CZK -> USD = 5.0
echo $exchange->change(100, NULL, 'CZK'); // EUR -> CZK = 2500.0
echo $exchange->change(100, 'USD', 'CZK'); // USD -> CZK = 2000.0
echo $exchange->change(100, 'CZK') . PHP_EOL; // CZK -> USD = 5.0
echo $exchange->change(100, null, 'CZK') . PHP_EOL; // EUR -> CZK = 2500.0
echo $exchange->change(100, 'USD', 'CZK') . PHP_EOL; // USD -> CZK = 2000.0
```

### Change driver and date
Expand All @@ -64,36 +82,35 @@ Download history exchange rates. Make new instance of Exchange with history rate
use h4kuna\Exchange\RatingList;
use h4kuna\Exchange;

$exchangePast = $exchange->modify(cacheEntity: new RatingList\CacheEntity(new \Datetime('2000-12-30'), Exchange\Driver\Cnb\Day::class));
$exchangePast->change(100);
$exchangePast = $exchangeFactory->create(cacheEntity: new CacheEntity(new Datetime('2000-12-30'), new Day));
echo $exchangePast->change(100) . PHP_EOL;
```

### Access and iterator

```php
/* @var $property Exchange\Currenry\Property */
use h4kuna\Exchange\Currency\Property;
/* @var $property Property */
$property = $exchange['EUR'];
var_dump($property);

echo PHP_EOL;

foreach ($exchange as $code => $property) {
/* @var $property Exchange\Currenry\Property */
var_dump($property);
/* @var $property Property */
var_dump($code, $property);
}
```

## Caching

The cache invalid automatic at some time, defined by property `Driver::$refresh`. From this property is counted time to live. Little better is invalid cache by cron. Because one request on server does not lock other requests. Let's run cron max. 15 minutes before invalidate cache.
The cache invalid automatic at some time, defined by property `SourceData::$refresh`. From this property is counted time to live. Little better is invalid cache by cron. Because one request on server does not lock other requests. Let's run cron max. 29 minutes before invalidate cache.
```php
use h4kuna\Exchange\RatingList\RatingListCache;
use h4kuna\Exchange\RatingList\CacheEntity;
use h4kuna\Exchange\Driver\Cnb\Day;

/** @var RatingListCache $ratingListCache */
$ratingListCache->rebuild(new CacheEntity(null, Day::class));
$ratingListCache->rebuild(new CacheEntity(null, new Day));
```

In example, is used `h4kuna\Exchange\Driver\Cnb\Day::$refresh` is defined at 15:00. Run cron 14:55 every day.


In example, is used `h4kuna\Exchange\Driver\Cnb\Day::$refresh` is defined at 14:30 + 30 minute the cache is valid. Run cron 14:32 every day.
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v7.1
- `Exchange::modify()` was removed, replace by ExchangeFactory
- `Exchange::transfer()` was removed, use change($amount, $from, $to) and getTo($to) of getFrom($from)
- improve [RatingListCache.php](./src/RatingList/RatingListCache.php)
- remove dependency `h4kuna/serialize-polyfill`

## v7.0

- for your temporary rate implement by own RatingList
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@
],
"require": {
"php": ">=8.0",
"h4kuna/critical-cache": "^v0.1.3",
"h4kuna/data-type": "^v3.0.7",
"h4kuna/serialize-polyfill": "^0.2.2",
"h4kuna/critical-cache": "^v0.1.5",
"malkusch/lock": "^2.2",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0 || ^2.0"
},
"require-dev": {
"guzzlehttp/psr7": "^2.4",
"guzzlehttp/guzzle": "^7.5",
"guzzlehttp/psr7": "^2.4",
"h4kuna/dir": "^0.1.2",
"mockery/mockery": "^1.6",
"nette/caching": "^3.2",
"nette/tester": "^2.5",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-strict-rules": "^1.5",
"tracy/tracy": "^2.10"
},
Expand Down
49 changes: 30 additions & 19 deletions examples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,35 @@

require_once __DIR__ . '/../vendor/autoload.php';

use h4kuna\CriticalCache;
use h4kuna\Exchange;
use h4kuna\Exchange\RatingList;

$cacheFactory = new CriticalCache\CacheFactory('exchange');

$exchangeFactory = new Exchange\ExchangeFactory(
from: 'eur',
to: 'usd',
allowedCurrencies: [
'CZK',
'USD',
'eur', // lower case will be changed to upper case
],
cacheFactory: $cacheFactory
);
use h4kuna\Exchange\Currency\Property;
use h4kuna\Exchange\Driver\Cnb\Day;
use h4kuna\Exchange\Exchange;
use h4kuna\Exchange\ExchangeFactory;
use h4kuna\Exchange\RatingList\CacheEntity;
use h4kuna\Exchange\RatingList\RatingList;

{ # by factory
$exchangeFactory = new ExchangeFactory(
from: 'eur',
to: 'usd',
allowedCurrencies: [
'CZK',
'USD',
'eur', // lower case will be changed to upper case
],
);

$exchange = $exchangeFactory->create();
}

$exchange = $exchangeFactory->create();
{ # custom RatingList
$ratingList = new RatingList(new DateTimeImmutable(), new DateTimeImmutable(), null, [
'EUR' => new Property(1, 25.0, 'EUR'),
'USD' => new Property(1, 20.0, 'USD'),
'CZK' => new Property(1, 1.0, 'CZK'),
]);
$exchange = new Exchange('EUR', $ratingList, 'USD');
}

echo $exchange->change(100) . PHP_EOL; // EUR -> USD = 125.0

Expand All @@ -30,7 +41,7 @@
echo PHP_EOL;

// History
$exchangePast = $exchange->modify(cacheEntity: new RatingList\CacheEntity(new \Datetime('2000-12-30'), Exchange\Driver\Cnb\Day::class));
$exchangePast = $exchangeFactory->create(cacheEntity: new CacheEntity(new Datetime('2000-12-30'), new Day));
echo $exchangePast->change(100) . PHP_EOL;
echo PHP_EOL;

Expand All @@ -41,6 +52,6 @@

// Iterator
foreach ($exchange as $code => $property) {
/* @var $property Exchange\Currency\Property */
/* @var $property Property */
var_dump($code, $property);
}
5 changes: 1 addition & 4 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ parameters:
- tests/bootstrap.php
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
-
message: "#^Expression \"\\$ratingList\\['QWE'\\]\" on a separate line does not do anything\\.$#"
count: 1
path: tests/src/RatingList/RatingListTest.php
- "#^Call to an undefined method Mockery\\.*#"

includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
20 changes: 20 additions & 0 deletions src/Download/SourceData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types=1);

namespace h4kuna\Exchange\Download;

use DateTimeImmutable;

final class SourceData
{
/**
* @param iterable<mixed> $properties
*/
public function __construct(
public DateTimeImmutable $date,
public string $refresh,
public iterable $properties,
)
{
}

}
88 changes: 88 additions & 0 deletions src/Download/SourceDownload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php declare(strict_types=1);

namespace h4kuna\Exchange\Download;

use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use h4kuna\Exchange\Driver\Source;
use h4kuna\Exchange\RatingList\RatingList;
use h4kuna\Exchange\Utils;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use ReflectionClass;

final class SourceDownload implements SourceDownloadInterface
{
/**
* @var array<string, SourceData>
*/
private array $cache = [];


/**
* @param array<string, int> $allowedCurrencies
*/
public function __construct(
private ClientInterface $client,
private RequestFactoryInterface $requestFactory,
private array $allowedCurrencies = [],
)
{
}


public function execute(Source $sourceExchange, ?DateTimeInterface $date): RatingList
{
$date = Utils::toImmutable($date, $sourceExchange->getTimeZone());
$key = self::makeKey($sourceExchange, $date);

$sourceData = $this->cache[$key]
?? $this->cache[$key] = $sourceExchange->createSourceData(
$this->client->sendRequest(
$this->createRequest($sourceExchange, $date)
)
);

$expire = $date === null ? new DateTime($sourceData->refresh . sprintf(', +%s seconds', Utils::CacheMinutes), $sourceExchange->getTimeZone()) : null;

$properties = [];
foreach ($sourceData->properties as $item) {
$property = $sourceExchange->createProperty($item);
if ($property->rate === 0.0 || ($this->allowedCurrencies !== [] && isset($this->allowedCurrencies[$property->code]) === false)) {
continue;
}

$properties[$property->code] = $property;
}

return new RatingList($sourceData->date, $date, $expire, $properties);
}


private static function makeKey(Source $sourceExchange, ?DateTimeImmutable $date): string
{
$rf = new ReflectionClass($sourceExchange);
do {
$class = $rf->getName();
$rf = $rf->getParentClass();
} while ($rf !== false);

if ($date === null) {
$date = new DateTimeImmutable('now', $sourceExchange->getTimeZone());
}

return $class . '.' . $date->format(Utils::DateFormat);
}


private function createRequest(Source $sourceExchange, ?DateTimeInterface $date): RequestInterface
{
$request = $this->requestFactory->createRequest('GET', $sourceExchange->makeUrl($date));
$request->withHeader('X-Powered-By', 'h4kuna/exchange');

return $request;
}

}
17 changes: 17 additions & 0 deletions src/Download/SourceDownloadInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);

namespace h4kuna\Exchange\Download;

use DateTimeInterface;
use h4kuna\Exchange\Driver\Source;
use h4kuna\Exchange\RatingList\RatingList;
use Psr\Http\Client\ClientExceptionInterface;

interface SourceDownloadInterface
{

/**
* @throws ClientExceptionInterface
*/
function execute(Source $sourceExchange, ?DateTimeInterface $date): RatingList;
}
Loading

0 comments on commit 9e003de

Please sign in to comment.