diff --git a/README.md b/README.md index 2545cda..297c9de 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ or not the item has been removed from cache. ### ArrayCache -The `ArrayCache` provides an in-memory implementation of the -[`CacheInterface`](#cacheinterface). +The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). ```php $cache = new ArrayCache(); @@ -83,6 +82,22 @@ $cache = new ArrayCache(); $cache->set('foo', 'bar'); ``` +Its constructor accepts an optional `?int $limit` parameter to limit the +maximum number of entries to store in the LRU cache. If you add more +entries to this instance, it will automatically take care of removing +the one that was least recently used (LRU). + +For example, this snippet will overwrite the first value and only store +the last two entries: + +```php +$cache = new ArrayCache(2); + +$cache->set('foo', '1'); +$cache->set('bar', '2'); +$cache->set('baz', '3'); +``` + ## Common usage ### Fallback get diff --git a/src/ArrayCache.php b/src/ArrayCache.php index f4bf9e8..14a4057 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -6,20 +6,67 @@ class ArrayCache implements CacheInterface { + private $limit; private $data = array(); + /** + * The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). + * + * ```php + * $cache = new ArrayCache(); + * + * $cache->set('foo', 'bar'); + * ``` + * + * Its constructor accepts an optional `?int $limit` parameter to limit the + * maximum number of entries to store in the LRU cache. If you add more + * entries to this instance, it will automatically take care of removing + * the one that was least recently used (LRU). + * + * For example, this snippet will overwrite the first value and only store + * the last two entries: + * + * ```php + * $cache = new ArrayCache(2); + * + * $cache->set('foo', '1'); + * $cache->set('bar', '2'); + * $cache->set('baz', '3'); + * ``` + * + * @param int|null $limit maximum number of entries to store in the LRU cache + */ + public function __construct($limit = null) + { + $this->limit = $limit; + } + public function get($key) { if (!isset($this->data[$key])) { return Promise\resolve(); } - return Promise\resolve($this->data[$key]); + // 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($value); } public function set($key, $value) { + // unset before setting to ensure this entry will be added to end of array + unset($this->data[$key]); $this->data[$key] = $value; + + // 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)]); + } + return Promise\resolve(true); } diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index f98fc0e..eb570a5 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -82,4 +82,52 @@ public function removeShouldRemoveKey() $this->expectCallableNever() ); } + + public function testLimitSizeToZeroDoesNotStoreAnyData() + { + $this->cache = new ArrayCache(0); + + $this->cache->set('foo', 'bar'); + + $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); + } + + public function testLimitSizeToOneWillOnlyReturnLastWrite() + { + $this->cache = new ArrayCache(1); + + $this->cache->set('foo', '1'); + $this->cache->set('bar', '2'); + + $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); + $this->cache->get('bar')->then($this->expectCallableOnceWith('2')); + } + + public function testOverwriteWithLimitedSizeWillUpdateLRUInfo() + { + $this->cache = new ArrayCache(2); + + $this->cache->set('foo', '1'); + $this->cache->set('bar', '2'); + $this->cache->set('foo', '3'); + $this->cache->set('baz', '4'); + + $this->cache->get('foo')->then($this->expectCallableOnceWith('3')); + $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); + $this->cache->get('baz')->then($this->expectCallableOnceWith('4')); + } + + public function testGetWithLimitedSizeWillUpdateLRUInfo() + { + $this->cache = new ArrayCache(2); + + $this->cache->set('foo', '1'); + $this->cache->set('bar', '2'); + $this->cache->get('foo')->then($this->expectCallableOnceWith('1')); + $this->cache->set('baz', '3'); + + $this->cache->get('foo')->then($this->expectCallableOnceWith('1')); + $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); + $this->cache->get('baz')->then($this->expectCallableOnceWith('3')); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index aa449f2..5d31a0e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,6 +26,17 @@ protected function expectCallableOnce() return $mock; } + protected function expectCallableOnceWith($param) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($param); + + return $mock; + } + protected function expectCallableNever() { $mock = $this->createCallableMock();