diff --git a/README.md b/README.md index 9e7ba2b..9da2703 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ provide alternate implementations. * [get()](#get) * [set()](#set) * [delete()](#delete) + * [getMultiple()](#getmultiple) + * [setMultiple()](#setmultiple) + * [deleteMultiple()](#deletemultiple) + * [clear()](#clear) + * [has()](#has) * [ArrayCache](#arraycache) * [Common usage](#common-usage) * [Fallback get](#fallback-get) @@ -94,6 +99,103 @@ 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. + +#### 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 `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. + ### ArrayCache The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 2b4e1c1..dd940af 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,60 @@ 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); + } + + 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\resolve(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 f31a68e..d3628ed 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -77,4 +77,96 @@ 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); + + /** + * 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. + * + * 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 `false` on error + */ + public function has($key); } diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index 3336012..3b5bd8c 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -193,4 +193,130 @@ 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'))); + } + + 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') + ->then($this->expectCallableOnceWith(false)); + + $this->cache + ->has('bar') + ->then($this->expectCallableOnceWith(true)); + + $this->cache + ->has('baz') + ->then($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') + ->then($this->expectCallableOnceWith(false)); + + $this->cache + ->has('bar') + ->then($this->expectCallableOnceWith(false)); + + $this->cache + ->has('baz') + ->then($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 hasShouldResolvePromiseForNonExistentKey() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', 'bar'); + + $this->cache + ->has('foo') + ->then($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 testHasWillResolveIfItemIsExpired() + { + $this->cache = new ArrayCache(); + $this->cache->set('foo', '1', 0); + + $this->cache + ->has('foo') + ->then($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')->then($this->expectCallableOnceWith(false)); + $this->cache->has('baz')->then($this->expectCallableOnceWith(3)); + } }