diff --git a/README.md b/README.md index 6770ec1..81808ab 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,13 @@ provide alternate implementations. #### get() -The `get(string $key, mixed $default = null): PromiseInterfae` method can be used to +The `get(string $key, mixed $default = null): PromiseInterface` method can be used to retrieve an item from the cache. This method will resolve with the 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 @@ -55,15 +57,25 @@ This example fetches the value of the key `foo` and passes it to the #### set() +The `set(string $key, mixed $value, ?float $ttl = null): PromiseInterface` method can be used to +store an item in the 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 store +it, it may take a while. + +The optional `$ttl` parameter sets the maximum time-to-live in seconds +for this cache item. If this parameter is omitted (or `null`), the item +will stay in the cache for as long as the underlying implementation +supports. Trying to access an expired cache item results in a cache miss, +see also [`get()`](#get). + ```php -$cache->set('foo', 'bar'); +$cache->set('foo', 'bar', 60); ``` This example eventually sets the value of the key `foo` to `bar`. If it -already exists, it is overridden. To provide guarantees as to when the cache -value is set a promise is returned. The promise will fulfill with `true` on success -or `false` on error. If the cache implementation has to go over the network to store -it, it may take a while. +already exists, it is overridden. #### remove() diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 7d6f75f..8512a1f 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -8,6 +8,7 @@ class ArrayCache implements CacheInterface { private $limit; private $data = array(); + private $expires = array(); /** * The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). @@ -43,6 +44,11 @@ public function __construct($limit = null) public function get($key, $default = null) { + // delete key if it is already expired => below will detect this as a cache miss + 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($default); } @@ -55,16 +61,33 @@ public function get($key, $default = null) return Promise\resolve($value); } - public function set($key, $value) + public function set($key, $value, $ttl = null) { - // unset before setting to ensure this entry will be added to end of array + // unset before setting to ensure this entry will be added to end of array (LRU info) unset($this->data[$key]); $this->data[$key] = $value; + // sort expiration times if TTL is given (first will expire first) + unset($this->expires[$key]); + if ($ttl !== null) { + $this->expires[$key] = microtime(true) + $ttl; + asort($this->expires); + } + // ensure size limit is not exceeded or remove first entry from array if ($this->limit !== null && count($this->data) > $this->limit) { - reset($this->data); - unset($this->data[key($this->data)]); + // first try to check if there's any expired entry + // expiration times are sorted, so we can simply look at the first one + reset($this->expires); + $key = key($this->expires); + + // check to see if the first in the list of expiring keys is already expired + // if the first key is not expired, we have to overwrite by using LRU info + if ($key === null || $this->expires[$key] > microtime(true)) { + reset($this->data); + $key = key($this->data); + } + unset($this->data[$key], $this->expires[$key]); } return Promise\resolve(true); @@ -72,7 +95,8 @@ public function set($key, $value) public function remove($key) { - unset($this->data[$key]); + unset($this->data[$key], $this->expires[$key]); + return Promise\resolve(true); } } diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 3832fac..da46cae 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -11,6 +11,8 @@ interface CacheInterface * * This method will resolve with the 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 @@ -29,14 +31,31 @@ interface CacheInterface public function get($key, $default = null); /** - * Store an item in the cache, returns a promise which resolves to true on success or - * false on error. + * Stores an item in the 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 store + * it, it may take a while. + * + * The optional `$ttl` parameter sets the maximum time-to-live in seconds + * for this cache item. If this parameter is omitted (or `null`), the item + * will stay in the cache for as long as the underlying implementation + * supports. Trying to access an expired cache item results in a cache miss, + * see also [`get()`](#get). + * + * ```php + * $cache->set('foo', 'bar', 60); + * ``` + * + * This example eventually sets the value of the key `foo` to `bar`. If it + * already exists, it is overridden. * * @param string $key - * @param mixed $value - * @return PromiseInterface Returns a promise which resolves to true on success of false on error + * @param mixed $value + * @param ?float $ttl + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error */ - public function set($key, $value); + public function set($key, $value, $ttl = null); /** * Remove an item from the cache, returns a promise which resolves to true on success or diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index bacf448..a5a23df 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -152,4 +152,45 @@ public function testGetWithLimitedSizeWillUpdateLRUInfo() $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); $this->cache->get('baz')->then($this->expectCallableOnceWith('3')); } + + public function testGetWillResolveWithValueIfItemIsNotExpired() + { + $this->cache = new ArrayCache(); + + $this->cache->set('foo', '1', 10); + + $this->cache->get('foo')->then($this->expectCallableOnceWith('1')); + } + + public function testGetWillResolveWithDefaultIfItemIsExpired() + { + $this->cache = new ArrayCache(); + + $this->cache->set('foo', '1', 0); + + $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); + } + + public function testSetWillOverwritOldestItemIfNoEntryIsExpired() + { + $this->cache = new ArrayCache(2); + + $this->cache->set('foo', '1', 10); + $this->cache->set('bar', '2', 20); + $this->cache->set('baz', '3', 30); + + $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); + } + + public function testSetWillOverwriteExpiredItemIfAnyEntryIsExpired() + { + $this->cache = new ArrayCache(2); + + $this->cache->set('foo', '1', 10); + $this->cache->set('bar', '2', 0); + $this->cache->set('baz', '3', 30); + + $this->cache->get('foo')->then($this->expectCallableOnceWith('1')); + $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); + } }