From 03aad31499a0ade7ef11c4e0f28c5f39ff6a18d6 Mon Sep 17 00:00:00 2001 From: krlv Date: Thu, 25 Oct 2018 21:03:53 -0700 Subject: [PATCH 1/3] Add PSR-16 methods support: getMultiple and setMultiple --- README.md | 43 ++++++++++++++++++++++++++++++++++ src/ArrayCache.php | 21 +++++++++++++++++ src/CacheInterface.php | 50 ++++++++++++++++++++++++++++++++++++++++ tests/ArrayCacheTest.php | 20 ++++++++++++++++ 4 files changed, 134 insertions(+) diff --git a/README.md b/README.md index 9e7ba2b..6843a12 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ provide alternate implementations. * [get()](#get) * [set()](#set) * [delete()](#delete) + * [getMultiple](#getmultiple) + * [setMultiple](#setmultiple) * [ArrayCache](#arraycache) * [Common usage](#common-usage) * [Fallback get](#fallback-get) @@ -94,6 +96,47 @@ This example eventually deletes the key `foo` from the cache. As with `set()`, this may not happen instantly and a promise is returned to provide guarantees whether or not the item has been removed from cache. +#### getMultiple() + +The `getMultiple(iterable $keys, mixed $default = null): PromiseInterface` method can be used to +retrieves multiple cache items by their unique keys. + +This method will resolve with the list of cached value on success or with the +given `$default` value when no item can be found or when an error occurs. +Similarly, an expired cache item (once the time-to-live is expired) is +considered a cache miss. + +```php +$cache + ->getMultiple(array('foo', 'bar')) + ->then('var_dump'); +``` + +This example fetches the list of value for `foo` and `bar` keys and passes it to the +`var_dump` function. You can use any of the composition provided by +[promises](https://github.com/reactphp/promise). + +#### setMultiple() + +Persists a set of key => value pairs in the cache, with an optional TTL. + +This method will resolve with `true` on success or `false` when an error +occurs. If the cache implementation has to go over the network to store +it, it may take a while. + +The optional `$ttl` parameter sets the maximum time-to-live in seconds +for these cache items. If this parameter is omitted (or `null`), these items +will stay in the cache for as long as the underlying implementation +supports. Trying to access an expired cache items results in a cache miss, +see also [`getMultiple()`](#getmultiple). + +```php +$cache->setMultiple(array('foo' => 1, 'bar' => 2), 60); +``` + +This example eventually sets the list of values - the key `foo` to `1` value +and the key `bar` to `2`. If some of the keys already exist, they are overridden. + ### ArrayCache The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 2b4e1c1..7b5a08e 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -3,6 +3,7 @@ namespace React\Cache; use React\Promise; +use React\Promise\PromiseInterface; class ArrayCache implements CacheInterface { @@ -99,4 +100,24 @@ public function delete($key) return Promise\resolve(true); } + + public function getMultiple($keys, $default = null) + { + $values = array(); + + foreach ($keys as $key) { + $values[$key] = $this->get($key, $default); + } + + return Promise\all($values); + } + + public function setMultiple($values, $ttl = null) + { + foreach ($values as $key => $value) { + $this->set($key, $value, $ttl); + } + + return Promise\resolve(true); + } } diff --git a/src/CacheInterface.php b/src/CacheInterface.php index f31a68e..42aca90 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -77,4 +77,54 @@ public function set($key, $value, $ttl = null); * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error */ public function delete($key); + + /** + * Retrieves multiple cache items by their unique keys. + * + * This method will resolve with the list of cached value on success or with the + * given `$default` value when no item can be found or when an error occurs. + * Similarly, an expired cache item (once the time-to-live is expired) is + * considered a cache miss. + * + * ```php + * $cache + * ->getMultiple(array('foo', 'bar')) + * ->then('var_dump'); + * ``` + * + * This example fetches the list of value for `foo` and `bar` keys and passes it to the + * `var_dump` function. You can use any of the composition provided by + * [promises](https://github.com/reactphp/promise). + * + * @param iterable $keys A list of keys that can obtained in a single operation. + * @param mixed $default Default value to return for keys that do not exist. + * @return PromiseInterface + */ + public function getMultiple($keys, $default = null); + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * This method will resolve with `true` on success or `false` when an error + * occurs. If the cache implementation has to go over the network to store + * it, it may take a while. + * + * The optional `$ttl` parameter sets the maximum time-to-live in seconds + * for these cache items. If this parameter is omitted (or `null`), these items + * will stay in the cache for as long as the underlying implementation + * supports. Trying to access an expired cache items results in a cache miss, + * see also [`get()`](#get). + * + * ```php + * $cache->setMultiple(array('foo' => 1, 'bar' => 2), 60); + * ``` + * + * This example eventually sets the list of values - the key `foo` to 1 value + * and the key `bar` to 2. If some of the keys already exist, they are overridden. + * + * @param iterable $values A list of key => value pairs for a multiple-set operation. + * @param ?float $ttl Optional. The TTL value of this item. + * @return bool PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function setMultiple($values, $ttl = null); } diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index 3336012..939424e 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -193,4 +193,24 @@ public function testSetWillOverwriteExpiredItemIfAnyEntryIsExpired() $this->cache->get('foo')->then($this->expectCallableOnceWith('1')); $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); } + + public function testGetMultiple() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', '1'); + + $this->cache + ->getMultiple(array('foo', 'bar'), 'baz') + ->then($this->expectCallableOnceWith(array('foo' => '1', 'bar' => 'baz'))); + } + + public function testSetMultiple() + { + $this->cache = new ArrayCache(); + $this->cache->setMultiple(array('foo' => '1', 'bar' => '2'), 10); + + $this->cache + ->getMultiple(array('foo', 'bar')) + ->then($this->expectCallableOnceWith(array('foo' => '1', 'bar' => '2'))); + } } From 867279e5b568c9f4606a6f96216df4efe0664267 Mon Sep 17 00:00:00 2001 From: krlv Date: Tue, 30 Oct 2018 00:41:39 -0700 Subject: [PATCH 2/3] Add PSR-16 methods: deleteMultiple, clear, has --- README.md | 65 +++++++++++++++++++++++- src/ArrayCache.php | 36 +++++++++++++ src/CacheInterface.php | 28 +++++++++++ tests/ArrayCacheTest.php | 106 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 233 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6843a12..749564e 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,11 @@ provide alternate implementations. * [get()](#get) * [set()](#set) * [delete()](#delete) - * [getMultiple](#getmultiple) - * [setMultiple](#setmultiple) + * [getMultiple()](#getmultiple) + * [setMultiple()](#setmultiple) + * [deleteMultiple()](#deletemultiple) + * [clear()](#clear) + * [has()](#has) * [ArrayCache](#arraycache) * [Common usage](#common-usage) * [Fallback get](#fallback-get) @@ -137,6 +140,64 @@ $cache->setMultiple(array('foo' => 1, 'bar' => 2), 60); This example eventually sets the list of values - the key `foo` to `1` value and the key `bar` to `2`. If some of the keys already exist, they are overridden. +#### deleteMultiple() + +Deletes multiple cache items in a single operation. + +This method will resolve with `true` on success or `false` when an error +occurs. When no items for `$keys` are found in the cache, it also resolves +to `true`. If the cache implementation has to go over the network to +delete it, it may take a while. + +```php +$cache->deleteMultiple(array('foo', 'bar, 'baz')); +``` + +This example eventually deletes keys `foo`, `bar` and `baz` from the cache. +As with `setMultiple()`, this may not happen instantly and a promise is returned to +provide guarantees whether or not the item has been removed from cache. + +#### clear() + +Wipes clean the entire cache. + +This method will resolve with `true` on success or `false` when an error +occurs. If the cache implementation has to go over the network to +delete it, it may take a while. + +```php +$cache->clear(); +``` + +This example eventually deletes all keys from the cache. As with `deleteMultiple()`, +this may not happen instantly and a promise is returned to provide guarantees +whether or not all the items have been removed from cache. + +#### has() + +Determines whether an item is present in the cache. + +This method will resolve with `true` on success or reject with `false` +when no item can be found or when an error occurs. Similarly, an expired cache item +(once the time-to-live is expired) is considered a cache miss. + +```php +$cache + ->has('foo') + ->then('var_dump') + ->otherwise('error_log'); +``` + +This example checks if the value of the key `foo` is set in the cache. If it is set, +`true` will be passed to the `var_dump` function; otherwise, `false` will be passed +to the `error_log` function. You can use any of the composition provided by +[promises](https://github.com/reactphp/promise). + +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. + ### ArrayCache The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 7b5a08e..a31c3ea 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -120,4 +120,40 @@ public function setMultiple($values, $ttl = null) return Promise\resolve(true); } + + public function deleteMultiple($keys) + { + foreach ($keys as $key) { + unset($this->data[$key], $this->expires[$key]); + } + + return Promise\resolve(true); + } + + public function clear() + { + $this->data = array(); + $this->expires = array(); + + return Promise\resolve(true); + } + + public function has($key) + { + // delete key if it is already expired + if (isset($this->expires[$key]) && $this->expires[$key] < \microtime(true)) { + unset($this->data[$key], $this->expires[$key]); + } + + if (!\array_key_exists($key, $this->data)) { + return Promise\reject(false); + } + + // remove and append to end of array to keep track of LRU info + $value = $this->data[$key]; + unset($this->data[$key]); + $this->data[$key] = $value; + + return Promise\resolve(true); + } } diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 42aca90..3309217 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -127,4 +127,32 @@ public function getMultiple($keys, $default = null); * @return bool PromiseInterface Returns a promise which resolves to `true` on success or `false` on error */ public function setMultiple($values, $ttl = null); + + /** + * Deletes multiple cache items in a single operation. + * + * @param iterable $keys A list of string-based keys to be deleted. + * @return bool PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function deleteMultiple($keys); + + /** + * Wipes clean the entire cache. + * + * @return bool PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function clear(); + + /** + * 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 PromiseInterface Returns a promise which resolves to `true` on success or rejects `` on error + */ + public function has($key); } diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index 939424e..0e5c262 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -213,4 +213,110 @@ public function testSetMultiple() ->getMultiple(array('foo', 'bar')) ->then($this->expectCallableOnceWith(array('foo' => '1', 'bar' => '2'))); } + + public function testDeleteMultiple() + { + $this->cache = new ArrayCache(); + $this->cache->setMultiple(array('foo' => 1, 'bar' => 2, 'baz' => 3)); + + $this->cache + ->deleteMultiple(array('foo', 'baz')) + ->then($this->expectCallableOnceWith(true)); + + $this->cache + ->has('foo') + ->otherwise($this->expectCallableOnceWith(false)); + + $this->cache + ->has('bar') + ->then($this->expectCallableOnceWith(true)); + + $this->cache + ->has('baz') + ->otherwise($this->expectCallableOnceWith(false)); + } + + public function testClearShouldClearCache() + { + $this->cache = new ArrayCache(); + $this->cache->setMultiple(array('foo' => 1, 'bar' => 2, 'baz' => 3)); + + $this->cache->clear(); + + $this->cache + ->has('foo') + ->otherwise($this->expectCallableOnceWith(false)); + + $this->cache + ->has('bar') + ->otherwise($this->expectCallableOnceWith(false)); + + $this->cache + ->has('baz') + ->otherwise($this->expectCallableOnceWith(false)); + } + + public function hasShouldResolvePromiseForExistingKey() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', 'bar'); + + $this->cache + ->has('foo') + ->then($this->expectCallableOnceWith(true)); + } + + public function hasShouldRejectPromiseForNonExistentKey() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', 'bar'); + + $this->cache + ->has('foo') + ->otherwise($this->expectCallableOnceWith(false)); + } + + public function testHasWillResolveIfItemIsNotExpired() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', '1', 10); + + $this->cache + ->has('foo') + ->then($this->expectCallableOnceWith(true)); + } + + public function testHasWillRejectIfItemIsExpired() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', '1', 0); + + $this->cache + ->has('foo') + ->otherwise($this->expectCallableOnceWith(false)); + } + + public function testHasWillResolveForExplicitNullValue() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', null); + + $this->cache + ->has('foo') + ->then($this->expectCallableOnceWith(true)); + } + + public function testHasWithLimitedSizeWillUpdateLRUInfo() + { + $this->cache = new ArrayCache(2); + + $this->cache->set('foo', 1); + $this->cache->set('bar', 2); + $this->cache->has('foo')->then($this->expectCallableOnceWith(true)); + $this->cache->set('baz', 3); + + $this->cache->has('foo')->then($this->expectCallableOnceWith(1)); + $this->cache->has('bar')->otherwise($this->expectCallableOnceWith(false)); + $this->cache->has('baz')->then($this->expectCallableOnceWith(3)); + } } From 1ad88361a630aa727e31aff999d216bdf59178df Mon Sep 17 00:00:00 2001 From: krlv Date: Tue, 30 Oct 2018 13:22:00 -0700 Subject: [PATCH 3/3] PSR-16 method has: resolve with false instead of reject on a cache miss --- README.md | 14 ++++++-------- src/ArrayCache.php | 2 +- src/CacheInterface.php | 16 +++++++++++++++- tests/ArrayCacheTest.php | 20 ++++++++++---------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 749564e..9da2703 100644 --- a/README.md +++ b/README.md @@ -177,20 +177,18 @@ whether or not all the items have been removed from cache. Determines whether an item is present in the cache. -This method will resolve with `true` on success or reject with `false` -when no item can be found or when an error occurs. Similarly, an expired cache item -(once the time-to-live is expired) is considered a cache miss. +This method will resolve with `true` on success or `false` when no item can be found +or when an error occurs. Similarly, an expired cache item (once the time-to-live +is expired) is considered a cache miss. ```php $cache ->has('foo') - ->then('var_dump') - ->otherwise('error_log'); + ->then('var_dump'); ``` -This example checks if the value of the key `foo` is set in the cache. If it is set, -`true` will be passed to the `var_dump` function; otherwise, `false` will be passed -to the `error_log` function. You can use any of the composition provided by +This example checks if the value of the key `foo` is set in the cache and passes +the result to the `var_dump` function. You can use any of the composition provided by [promises](https://github.com/reactphp/promise). NOTE: It is recommended that has() is only to be used for cache warming type purposes diff --git a/src/ArrayCache.php b/src/ArrayCache.php index a31c3ea..dd940af 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -146,7 +146,7 @@ public function has($key) } if (!\array_key_exists($key, $this->data)) { - return Promise\reject(false); + return Promise\resolve(false); } // remove and append to end of array to keep track of LRU info diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 3309217..d3628ed 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -146,13 +146,27 @@ public function clear(); /** * Determines whether an item is present in the cache. * + * This method will resolve with `true` on success or `false` when no item can be found + * or when an error occurs. Similarly, an expired cache item (once the time-to-live + * is expired) is considered a cache miss. + * + * ```php + * $cache + * ->has('foo') + * ->then('var_dump'); + * ``` + * + * This example checks if the value of the key `foo` is set in the cache and passes + * the result to the `var_dump` function. You can use any of the composition provided by + * [promises](https://github.com/reactphp/promise). + * * 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 PromiseInterface Returns a promise which resolves to `true` on success or rejects `` on error + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error */ public function has($key); } diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index 0e5c262..3b5bd8c 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -225,7 +225,7 @@ public function testDeleteMultiple() $this->cache ->has('foo') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); $this->cache ->has('bar') @@ -233,7 +233,7 @@ public function testDeleteMultiple() $this->cache ->has('baz') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); } public function testClearShouldClearCache() @@ -245,15 +245,15 @@ public function testClearShouldClearCache() $this->cache ->has('foo') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); $this->cache ->has('bar') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); $this->cache ->has('baz') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); } public function hasShouldResolvePromiseForExistingKey() @@ -266,14 +266,14 @@ public function hasShouldResolvePromiseForExistingKey() ->then($this->expectCallableOnceWith(true)); } - public function hasShouldRejectPromiseForNonExistentKey() + public function hasShouldResolvePromiseForNonExistentKey() { $this->cache = new ArrayCache(); $this->cache->set('foo', 'bar'); $this->cache ->has('foo') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); } public function testHasWillResolveIfItemIsNotExpired() @@ -286,14 +286,14 @@ public function testHasWillResolveIfItemIsNotExpired() ->then($this->expectCallableOnceWith(true)); } - public function testHasWillRejectIfItemIsExpired() + public function testHasWillResolveIfItemIsExpired() { $this->cache = new ArrayCache(); $this->cache->set('foo', '1', 0); $this->cache ->has('foo') - ->otherwise($this->expectCallableOnceWith(false)); + ->then($this->expectCallableOnceWith(false)); } public function testHasWillResolveForExplicitNullValue() @@ -316,7 +316,7 @@ public function testHasWithLimitedSizeWillUpdateLRUInfo() $this->cache->set('baz', 3); $this->cache->has('foo')->then($this->expectCallableOnceWith(1)); - $this->cache->has('bar')->otherwise($this->expectCallableOnceWith(false)); + $this->cache->has('bar')->then($this->expectCallableOnceWith(false)); $this->cache->has('baz')->then($this->expectCallableOnceWith(3)); } }