From 3a40d594a1dd2771dca109b518b4ac862533ad0f Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 30 Oct 2024 17:23:42 -0500 Subject: [PATCH 1/3] Add Cache implementation for Key Value Store --- README.md | 16 ++- composer.json | 1 + docs/cache.md | 20 ++++ phpunit.xml.dist | 40 +++---- src/Cache/KeyValueCacheEngine.php | 128 +++++++++++++++++++++ tests/CachePSR16Test.php | 184 ++++++++++++++++++++++++++++++ 6 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 docs/cache.md create mode 100644 src/Cache/KeyValueCacheEngine.php create mode 100644 tests/CachePSR16Test.php diff --git a/README.md b/README.md index 11ce326..8aa2816 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,19 @@ See below the current implemented drivers: | Datasource | Connection String | |---------------------------------------------|----------------------------------------------------------| -| [MongoDB](MongoDB.md) | mongodb://username:password@hostname:port/database | -| [Cloudflare KV](CloudFlareKV.md) | kv://username:password@accountid/namespaceid | -| [S3](AwsS3KeyValue.md) | s3://accesskey:secretkey@region/bucket?params | -| [AWS DynamoDB](AwsDynamoDbKeyValue.md) | dynamodb://accesskey:secretkey@hostname/tablename?params | +| [MongoDB](docs/MongoDB.md) | mongodb://username:password@hostname:port/database | +| [S3](docs/AwsS3KeyValue.md) | s3://accesskey:secretkey@region/bucket?params | +| [Cloudflare KV](docs/CloudFlareKV.md) | kv://username:password@accountid/namespaceid | +| [AWS DynamoDB](docs/AwsDynamoDbKeyValue.md) | dynamodb://accesskey:secretkey@hostname/tablename?params | +## PSR-16 Cache Interface + +This package provides a PSR-16 cache implementation for the Key-Value store and it is compatible with any class +use the PSR-16 interface. + +For more details see: [Cache Inteface for Key Value](docs/cache.md) + ## Examples Check implementation examples on [https://opensource.byjg.com/php/anydataset-nosql](https://opensource.byjg.com/php/anydataset-nosql) @@ -94,6 +101,7 @@ flowchart TD byjg/anydataset-nosql --> byjg/anydataset-array byjg/anydataset-nosql --> byjg/serializer byjg/anydataset-nosql --> byjg/webrequest + byjg/anydataset-nosql --> byjg/cache-engine byjg/anydataset-nosql --> ext-json ``` diff --git a/composer.json b/composer.json index cce517f..c03b5e9 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "php": ">=8.1 <8.4", "ext-curl": "*", "aws/aws-sdk-php": "3.*", + "byjg/cache-engine": "^5.0", "byjg/anydataset": "^5.0", "byjg/anydataset-array": "^5.0", "byjg/serializer": "^5.0", diff --git a/docs/cache.md b/docs/cache.md new file mode 100644 index 0000000..4ea0820 --- /dev/null +++ b/docs/cache.md @@ -0,0 +1,20 @@ +# Cache Interface + +This package provides a PSR-16 cache implementation for the Key-Value store. + +To use as a cache store you just need to: + +```php +set('key', 'value'); +echo $cache->get('key'); +``` \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e44d77d..2472842 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,32 +4,32 @@ To change this license header, choose License Headers in Project Properties. To change this template file, choose Tools | Templates and open the template in the editor. --> - - + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> - - - - - + + + + + - - - ./src - - + + + ./src + + - - - ./tests/ - - - \ No newline at end of file + + + ./tests/ + + + diff --git a/src/Cache/KeyValueCacheEngine.php b/src/Cache/KeyValueCacheEngine.php new file mode 100644 index 0000000..358f75e --- /dev/null +++ b/src/Cache/KeyValueCacheEngine.php @@ -0,0 +1,128 @@ +keyValue = $keyValue; + $this->logger = $logger; + if (is_null($logger)) { + $this->logger = new NullLogger(); + } + } + + /** + * Determines whether an item is present in the cache. + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * @return bool + * @throws InvalidArgumentException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function has(string $key): bool + { + $key = $this->getKeyFromContainer($key); + if ($this->keyValue->has($key)) { + if ($this->keyValue->has("$key.ttl") && time() >= $this->keyValue->get("$key.ttl")) { + $this->delete($key); + return false; + } + + return true; + } + + return false; + } + + /** + * @param string $key The object KEY + * @param mixed $default IGNORED IN MEMCACHED. + * @return mixed Description + * @throws ContainerExceptionInterface + * @throws InvalidArgumentException + * @throws NotFoundExceptionInterface + */ + public function get(string $key, mixed $default = null): mixed + { + if ($this->has($key)) { + $key = $this->getKeyFromContainer($key); + $this->logger->info("[KeyValueInterface] Get '$key' fromCache"); + return unserialize($this->keyValue->get($key)); + } else { + $this->logger->info("[KeyValueInterface] Not found '$key'"); + return $default; + } + } + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * @param string $key The key of the item to store. + * @param mixed $value The value of the item to store, must be serializable. + * @param null|int|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * MUST be thrown if the $key string is not a legal value. + */ + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool + { + $key = $this->getKeyFromContainer($key); + + $this->logger->info("[KeyValueInterface] Set '$key' in Cache"); + + $this->keyValue->put($key, serialize($value)); + if (!empty($ttl)) { + $this->keyValue->put("$key.ttl", $this->addToNow($ttl)); + } + + return true; + } + + public function clear(): bool + { + return false; + } + + /** + * Unlock resource + * + * @param string $key + * @return bool + */ + public function delete(string $key): bool + { + $key = $this->getKeyFromContainer($key); + + $this->keyValue->remove($key); + $this->keyValue->remove("$key.ttl"); + return true; + } + + public function isAvailable(): bool + { + return true; + } +} diff --git a/tests/CachePSR16Test.php b/tests/CachePSR16Test.php new file mode 100644 index 0000000..50cf0b7 --- /dev/null +++ b/tests/CachePSR16Test.php @@ -0,0 +1,184 @@ +withQueryKeyValue("use_path_style_endpoint", "true"); + $object = Factory::getInstance($uri); + $object->remove("KEY"); + $object->remove("ANOTHER"); + $result[] = [new KeyValueCacheEngine($object)]; + } + + return $result; + } + + protected function tearDown(): void + { + if (empty($this->cacheEngine)) { + return; + } + $this->cacheEngine->deleteMultiple(['chave', 'chave2', 'chave3']); + $this->cacheEngine = null; + } + + + /** + * @dataProvider CachePoolProvider + * @param BaseCacheEngine $cacheEngine + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function testGetOneItem(BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $item = $cacheEngine->get('chave', null); + $this->assertNull($item); + $item = $cacheEngine->get('chave', 'default'); + $this->assertEquals('default', $item); + + // Set object + $cacheEngine->set('chave', 'valor'); + + // Get Object + if (!($cacheEngine instanceof NoCacheEngine)) { + $item2 = $cacheEngine->get('chave', 'default'); + $this->assertEquals('valor', $item2); + } + + // Remove + $cacheEngine->delete('chave'); + + // Check Removed + $item = $cacheEngine->get('chave'); + $this->assertNull($item); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param BaseCacheEngine $cacheEngine + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function testGetMultipleItems(BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $items = [...$cacheEngine->getMultiple(['chave1', 'chave2'])]; + $this->assertNull($items['chave1']); + $this->assertNull($items['chave2']); + $items = [...$cacheEngine->getMultiple(['chave1', 'chave2'], 'default')]; + $this->assertEquals('default', $items['chave1']); + $this->assertEquals('default', $items['chave2']); + + // Set object + $cacheEngine->set('chave1', 'valor1'); + $cacheEngine->set('chave2', 'valor2'); + + // Get Object + if (!($cacheEngine instanceof NoCacheEngine)) { + $item2 = [...$cacheEngine->getMultiple(['chave1', 'chave2'])]; + $this->assertEquals('valor1', $item2['chave1']); + $this->assertEquals('valor2', $item2['chave2']); + } + + // Remove + $cacheEngine->deleteMultiple(['chave1', 'chave2']); + + // Check Removed + $items = [...$cacheEngine->getMultiple(['chave1', 'chave2'])]; + $this->assertNull($items['chave1']); + $this->assertNull($items['chave2']); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param BaseCacheEngine $cacheEngine + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function testTtl(BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $item = $cacheEngine->get('chave'); + $this->assertNull($item); + $this->assertFalse($cacheEngine->has('chave')); + $item2 = $cacheEngine->get('chave2'); + $this->assertNull($item2); + $this->assertFalse($cacheEngine->has('chave2')); + + // Set object + $cacheEngine->set('chave', 'valor', 2); + $cacheEngine->set('chave2', 'valor2', 2); + + // Get Object + if (!($cacheEngine instanceof NoCacheEngine)) { + $item2 = $cacheEngine->get('chave'); + $this->assertEquals('valor', $item2); + $this->assertTrue($cacheEngine->has('chave2')); + sleep(3); + $item2 = $cacheEngine->get('chave'); + $this->assertNull($item2); + $this->assertFalse($cacheEngine->has('chave2')); + } + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param BaseCacheEngine $cacheEngine + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function testCacheObject(BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $item = $cacheEngine->get('chave'); + $this->assertNull($item); + + // Set object + $model = new \Tests\Document(10, 20, 30); + $cacheEngine->set('chave', $model); + + $item2 = $cacheEngine->get('chave'); + $this->assertEquals($model, $item2); + + // Delete + $cacheEngine->delete('chave'); + $item = $cacheEngine->get('chave'); + $this->assertNull($item); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } +} From 5d95b19e3ec4f274634bb7f6857552aa5b884d8d Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 30 Oct 2024 17:28:18 -0500 Subject: [PATCH 2/3] Add Cache implementation for Key Value Store --- tests/CachePSR16Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CachePSR16Test.php b/tests/CachePSR16Test.php index 50cf0b7..41f55ec 100644 --- a/tests/CachePSR16Test.php +++ b/tests/CachePSR16Test.php @@ -167,7 +167,7 @@ public function testCacheObject(BaseCacheEngine $cacheEngine) $this->assertNull($item); // Set object - $model = new \Tests\Document(10, 20, 30); + $model = new \Tests\Document("name", "brand", 30); $cacheEngine->set('chave', $model); $item2 = $cacheEngine->get('chave'); From 7b7823cb4cbe1a5f17b64e32aa650c1bfa495bc6 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 17 Dec 2024 09:56:47 -0600 Subject: [PATCH 3/3] Update Documentation --- README.md | 54 ++++++++------------------------------------------- docs/cache.md | 4 ++++ docs/tests.md | 19 ++++++++++++++++++ 3 files changed, 31 insertions(+), 46 deletions(-) create mode 100644 docs/tests.md diff --git a/README.md b/README.md index 8aa2816..5039d7a 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Anydataset NoSQL standardize the access to non-relational databases/repositories and treat them as Key/Value. The implementation can work with: +- S3-Like Storage - MongoDB - Cloudflare KV -- S3 - DynamoDB Anydataset is an agnostic data source abstraction layer in PHP. See more about Anydataset [here](https://opensource.byjg.com/php/anydataset). @@ -36,16 +36,14 @@ See below the current implemented drivers: | [AWS DynamoDB](docs/AwsDynamoDbKeyValue.md) | dynamodb://accesskey:secretkey@hostname/tablename?params | -## PSR-16 Cache Interface +## Topics -This package provides a PSR-16 cache implementation for the Key-Value store and it is compatible with any class -use the PSR-16 interface. - -For more details see: [Cache Inteface for Key Value](docs/cache.md) - -## Examples - -Check implementation examples on [https://opensource.byjg.com/php/anydataset-nosql](https://opensource.byjg.com/php/anydataset-nosql) +- [S3-Like Storage](docs/AwsS3KeyValue.md) +- [MongoDB](docs/MongoDB.md) +- [Cloudflare KV](docs/CloudFlareKV.md) +- [AWS DynamoDB](docs/AwsDynamoDbKeyValue.md) +- [Cache Store](docs/cache.md) +- [Running Tests](docs/tests.md) ## Install @@ -55,42 +53,6 @@ Just type: composer require "byjg/anydataset-nosql" ``` -## Running Unit tests - -```bash -docker-compose up -d -export MONGODB_CONNECTION="mongodb://127.0.0.1/test" -export S3_CONNECTION="s3://aaa:12345678@us-east-1/mybucket?create=true&endpoint=http://127.0.0.1:4566" -export DYNAMODB_CONNECTION="dynamodb://accesskey:secretkey@us-east-1/tablename?endpoint=http://127.0.0.1:8000" -vendor/bin/phpunit -``` - - -### Setup MongoDB for the unit test - -Set the environment variable: - -- MONGODB_CONNECTION = "mongodb://127.0.0.1/test" - -### Setup AWS DynamoDb for the unit test - -Set the environment variable: - -- DYNAMODB_CONNECTION = "dynamodb://accesskey:secretkey@region/tablename" - -### Setup AWS S3 for the unit test - -Set the environment variable: - -- S3_CONNECTION = "s3://accesskey:secretkey@region/bucketname" - - -### Cloudflare KV - -Set the environment variable: - -- CLOUDFLAREKV_CONNECTION = "kv://email:authkey@accountid/namespaceid" - ## Dependencies ```mermaid diff --git a/docs/cache.md b/docs/cache.md index 4ea0820..9f7e33e 100644 --- a/docs/cache.md +++ b/docs/cache.md @@ -1,5 +1,9 @@ # Cache Interface +The class `KeyValueCacheEngine` adds a cache layer on top of the KeyValueStore. + +It allows you to cache the results locally and avoid unnecessary calls to the KeyValueStore. + This package provides a PSR-16 cache implementation for the Key-Value store. To use as a cache store you just need to: diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 0000000..3118e84 --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,19 @@ +# Running Unit tests + +```bash +docker-compose up -d +export MONGODB_CONNECTION="mongodb://127.0.0.1/test" +export S3_CONNECTION="s3://aaa:12345678@us-east-1/mybucket?create=true&endpoint=http://127.0.0.1:4566" +export DYNAMODB_CONNECTION="dynamodb://accesskey:secretkey@us-east-1/tablename?endpoint=http://127.0.0.1:8000" +vendor/bin/phpunit +``` + +## Setup the environment variables + +| Variable | Description | Example | +|-------------------------|-------------------------------------|-------------------------------------------------| +| MONGODB_CONNECTION | Connection string for MongoDB | mongodb://127.0.0.1/test | +| S3_CONNECTION | Connection string for S3 | s3://accesskey:secretkey@region/bucketname | +| DYNAMODB_CONNECTION | Connection string for DynamoDB | dynamodb://accesskey:secretkey@region/tablename | +| CLOUDFLAREKV_CONNECTION | Connection string for Cloudflare KV | kv://email:authkey@accountid/namespaceid | +