From d37dbb7d5095a97255a97a784bd2141d3fbb3438 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 9 Jan 2016 03:06:18 +0100 Subject: [PATCH] Added a trait to use with adapters to support integration from adapters. --- composer.json | 4 +- src/HierarchicalCachePool.php | 201 ---------------------------- src/HierarchicalCachePoolTrait.php | 124 +++++++++++++++++ src/HierarchicalPoolInterface.php | 10 ++ tests/Helper/CachePool.php | 49 +++++++ tests/HierarchicalCachePoolTest.php | 155 +++++++++++++++++---- tests/IntegrationPoolTest.php | 34 ----- tests/IntegrationTagTest.php | 34 ----- 8 files changed, 316 insertions(+), 295 deletions(-) delete mode 100644 src/HierarchicalCachePool.php create mode 100644 src/HierarchicalCachePoolTrait.php create mode 100644 tests/Helper/CachePool.php delete mode 100644 tests/IntegrationPoolTest.php delete mode 100644 tests/IntegrationTagTest.php diff --git a/composer.json b/composer.json index 3ecfdd5..b3fb2d6 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "cache/hierarchical-cache", - "description": "A PSR-6 cache implementation using hierarchical. This implementation supports tags", + "description": "A helper trait and interface to your PSR-6 cache to support hierarchical keys.", "type": "library", "license": "MIT", "minimum-stability": "stable", @@ -30,7 +30,7 @@ "php": "^5.5|^7.0", "psr/cache": "1.0.0", "cache/adapter-common": "^0.1", - "cache/taggable-cache": "^0.2" + "cache/taggable-cache": "^0.3" }, "require-dev": { diff --git a/src/HierarchicalCachePool.php b/src/HierarchicalCachePool.php deleted file mode 100644 index 8d1c615..0000000 --- a/src/HierarchicalCachePool.php +++ /dev/null @@ -1,201 +0,0 @@ -, Tobias Nyholm - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Cache\Hierarchy; - -use Cache\Adapter\Common\CacheItem; -use Cache\Adapter\Common\Exception\InvalidArgumentException; -use Cache\Taggable\TaggableItemInterface; -use Cache\Taggable\TaggablePoolInterface; -use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; - -/** - * @author Tobias Nyholm - */ -class HierarchicalCachePool implements CacheItemPoolInterface, HierarchicalPoolInterface, TaggablePoolInterface -{ - const SEPARATOR = '|'; - - /** - * @var CacheItemPoolInterface - */ - private $cache; - - /** - * - * @param CacheItemPoolInterface $cache - */ - public function __construct(CacheItemPoolInterface $cache) - { - $this->cache = $cache; - } - - /** - * {@inheritdoc} - */ - public function getItem($key, array $tags = []) - { - $item = $this->cache->getItem($key, $tags); - if (!$this->isHierarchyKey($key) || !$item->isHit()) { - return $item; - } - - if (!$this->validateParents($key, $tags)) { - return $item; - } - - // Invalid item - if ($item instanceof TaggableItemInterface) { - $key = $item->getTaggedKey(); - } else { - $key = $item->getKey(); - } - - return new CacheItem($key); - } - - /** - * {@inheritdoc} - */ - public function getItems(array $keys = [], array $tags = []) - { - $items = []; - foreach ($keys as $key) { - $items[$key] = $this->getItem($key, $tags); - } - - return $items; - } - - /** - * {@inheritdoc} - */ - public function hasItem($key, array $tags = []) - { - $hasItem = $this->cache->hasItem($key, $tags); - if (!$this->isHierarchyKey($key) || $hasItem === false) { - return $hasItem; - } - - return $this->validateParents($key, $tags); - } - - /** - * {@inheritdoc} - */ - public function clear(array $tags = []) - { - return $this->cache->clear($tags); - } - - /** - * {@inheritdoc} - */ - public function deleteItem($key, array $tags = []) - { - return $this->cache->deleteItem($key, $tags); - } - - /** - * {@inheritdoc} - */ - public function deleteItems(array $keys, array $tags = []) - { - return $this->cache->deleteItems($keys, $tags); - } - - /** - * {@inheritdoc} - */ - public function save(CacheItemInterface $item) - { - $parts = $this->explodeKey($item->getKey()); - $parentKey = ''; - foreach ($parts as $part) { - $parentKey .= $part; - $parent = $this->cache->getItem($parentKey); - $parent->set(null); - $this->cache->save($parent); - } - - return $this->cache->save($item); - } - - /** - * {@inheritdoc} - */ - public function saveDeferred(CacheItemInterface $item) - { - return $this->cache->saveDeferred($item); - } - - /** - * {@inheritdoc} - */ - public function commit() - { - return $this->cache->commit(); - } - - /** - * A hierarchy key MUST begin with the separator. - * @param string $key - * - * @return bool - */ - private function isHierarchyKey($key) - { - if (!is_string($key)) { - throw new InvalidArgumentException(sprintf('Key must be string.')); - } - - return substr($key, 0, 1) === self::SEPARATOR; - } - - /** - * @param string $key - * @param array $tags - * - * @return bool true if parents are valid - */ - private function validateParents($key, array $tags) - { - $parts = $this->explodeKey($key); - $parentKey = ''; - foreach ($parts as $part) { - $parentKey .= $part; - if (!$this->cache->hasItem($parentKey, $tags)) { - // Invalid item - return false; - } - } - - return true; - } - - /** - * @param CacheItemInterface $item - * - * @return array - */ - private function explodeKey($key) - { - $parts = explode(self::SEPARATOR, $key); - - unset($parts[0]); - foreach ($parts as &$part) { - $part = self::SEPARATOR.$part; - } - - return $parts; - } -} diff --git a/src/HierarchicalCachePoolTrait.php b/src/HierarchicalCachePoolTrait.php new file mode 100644 index 0000000..a1fd9ed --- /dev/null +++ b/src/HierarchicalCachePoolTrait.php @@ -0,0 +1,124 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\Hierarchy; + +use Cache\Taggable\TaggablePoolInterface; + +/** + * @author Tobias Nyholm + */ +trait HierarchicalCachePoolTrait +{ + /** + * A temporary cache for keys. + * + * @type array + */ + private $keyCache = []; + + /** + * Get a value form the store. This must not be an PoolItemInterface. + * + * @param string $key + * + * @return string|null + */ + abstract protected function getValueFormStore($key); + + /** + * Get a key to use with the hierarchy. If the key does not start with HierarchicalPoolInterface::SEPARATOR + * this will return an unalterered key. This function supports a tagged key. Ie "foo:bar". + * + * @param string $key The original key + * @param string &$pathKey A cache key for the path. If this key is changed everything beyond that path is changed. + * + * @return string + */ + protected function getHierarchyKey($key, &$pathKey = null) + { + if (!$this->isHierarchyKey($key)) { + return $key; + } + + $key = $this->explodeKey($key); + + $keyString = ''; + // The comments below is for a $key = ["foo!tagHash", "bar!tagHash"] + foreach ($key as $name) { + // 1) $keyString = "foo!tagHash" + // 2) $keyString = "foo!tagHash![foo_index]!bar!tagHash" + $keyString .= $name; + $pathKey = 'path'.TaggablePoolInterface::TAG_SEPARATOR.$keyString; + + if (isset($this->keyCache[$pathKey])) { + $index = $this->keyCache[$pathKey]; + } else { + $index = $this->getValueFormStore($pathKey); + $this->keyCache[$pathKey] = $index; + } + + // 1) $keyString = "foo!tagHash![foo_index]!" + // 2) $keyString = "foo!tagHash![foo_index]!bar!tagHash![bar_index]!" + $keyString .= TaggablePoolInterface::TAG_SEPARATOR.$index.TaggablePoolInterface::TAG_SEPARATOR; + } + + // Assert: $pathKey = "path!foo!tagHash![foo_index]!bar!tagHash" + // Assert: $keyString = "foo!tagHash![foo_index]!bar!tagHash![bar_index]!" + + return $keyString; + } + + /** + * Clear the cache for the keys. + */ + protected function clearHierarchyKeyCache() + { + $this->keyCache = []; + } + + /** + * A hierarchy key MUST begin with the separator. + * + * @param string $key + * + * @return bool + */ + private function isHierarchyKey($key) + { + return substr($key, 0, 1) === HierarchicalPoolInterface::HIERARCHY_SEPARATOR; + } + + /** + * This will take a hierarchy key ("|foo|bar") with tags ("|foo|bar!tagHash") and return an array with + * each level in the hierarchy appended with the tags. ["foo!tagHash", "bar!tagHash"]. + * + * @param string $key + * + * @return array + */ + private function explodeKey($string) + { + list($key, $tag) = explode(TaggablePoolInterface::TAG_SEPARATOR, $string.TaggablePoolInterface::TAG_SEPARATOR); + + if ($key === HierarchicalPoolInterface::HIERARCHY_SEPARATOR) { + $parts = ['root']; + } else { + $parts = explode(HierarchicalPoolInterface::HIERARCHY_SEPARATOR, $key); + // remove first element since it is always empty and replace it with 'root' + $parts[0] = 'root'; + } + + return array_map(function ($level) use ($tag) { + return $level.TaggablePoolInterface::TAG_SEPARATOR.$tag; + }, $parts); + } +} diff --git a/src/HierarchicalPoolInterface.php b/src/HierarchicalPoolInterface.php index b8ea020..eba4f17 100644 --- a/src/HierarchicalPoolInterface.php +++ b/src/HierarchicalPoolInterface.php @@ -1,7 +1,17 @@ , Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + namespace Cache\Hierarchy; interface HierarchicalPoolInterface { + const HIERARCHY_SEPARATOR = '|'; } diff --git a/tests/Helper/CachePool.php b/tests/Helper/CachePool.php new file mode 100644 index 0000000..01d5b0c --- /dev/null +++ b/tests/Helper/CachePool.php @@ -0,0 +1,49 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\Hierarchy\Tests\Helper; + +use Cache\Hierarchy\HierarchicalCachePoolTrait; + +/** + * A cache pool used in tests. + * + * @author Tobias Nyholm + */ +class CachePool +{ + use HierarchicalCachePoolTrait; + + private $storeValues = []; + + /** + * @param array $storeValues + */ + public function __construct(array $storeValues = []) + { + $this->storeValues = $storeValues; + } + + public function exposeClearHierarchyKeyCache() + { + return $this->clearHierarchyKeyCache(); + } + + public function exposeGetHierarchyKey($key, &$pathKey = null) + { + return $this->getHierarchyKey($key, $pathKey); + } + + protected function getValueFormStore($key) + { + return array_shift($this->storeValues); + } +} diff --git a/tests/HierarchicalCachePoolTest.php b/tests/HierarchicalCachePoolTest.php index 653c414..3dc2231 100644 --- a/tests/HierarchicalCachePoolTest.php +++ b/tests/HierarchicalCachePoolTest.php @@ -1,7 +1,7 @@ , Tobias Nyholm * @@ -9,42 +9,149 @@ * with this source code in the file LICENSE. */ -namespace Cache\Hierarchy; +namespace Cache\Hierarchy\Tests; -use Cache\Adapter\Apc\ApcCachePool; -use Cache\Adapter\PHPArray\ArrayCachePool; -use Cache\IntegrationTests\CachePoolTest as BaseTest; +use Cache\Hierarchy\Tests\Helper\CachePool; -class HierarchicalCachePoolTest extends \PHPUnit_Framework_TestCase +/** + * We should not use constans on interfaces in the tests. Tests should break if the constant is changed. + * + * @author Tobias Nyholm + */ +class HierarchicalCachePoolTest extends \PHPUnit_Framework_TestCase { - private $cache; + public function testGetHierarchyKey() + { + $path = null; + + $pool = new CachePool(); + $result = $pool->exposeGetHierarchyKey('key', $path); + $this->assertEquals('key', $result); + $this->assertNull($path); + + $pool = new CachePool(['idx_1', 'idx_2', 'idx_3']); + $result = $pool->exposeGetHierarchyKey('|foo|bar', $path); + $this->assertEquals('root!!idx_1!foo!!idx_2!bar!!idx_3!', $result); + $this->assertEquals('path!root!!idx_1!foo!!idx_2!bar!', $path); - public function createCachePool() + $pool = new CachePool(['idx_1', 'idx_2', 'idx_3']); + $result = $pool->exposeGetHierarchyKey('|', $path); + $this->assertEquals('path!root!', $path); + $this->assertEquals('root!!idx_1!', $result); + } + + public function testGetHierarchyKeyWithTags() { - return new HierarchicalCachePool($this->getCache()); + $path = null; + + $pool = new CachePool(); + $result = $pool->exposeGetHierarchyKey('key!tagHash', $path); + $this->assertEquals('key!tagHash', $result); + $this->assertNull($path); + + $pool = new CachePool(['idx_1', 'idx_2', 'idx_3']); + $result = $pool->exposeGetHierarchyKey('|foo|bar!tagHash', $path); + $this->assertEquals('root!tagHash!idx_1!foo!tagHash!idx_2!bar!tagHash!idx_3!', $result); + $this->assertEquals('path!root!tagHash!idx_1!foo!tagHash!idx_2!bar!tagHash', $path); + + $pool = new CachePool(['idx_1', 'idx_2', 'idx_3']); + $result = $pool->exposeGetHierarchyKey('|!tagHash', $path); + $this->assertEquals('path!root!tagHash', $path); + $this->assertEquals('root!tagHash!idx_1!', $result); } - public function getCache() + public function testGetHierarchyKeyEmptyCache() { - if ($this->cache === null) { - $this->cache = new ArrayCachePool(); - } + $pool = new CachePool(); + $path = null; + + $result = $pool->exposeGetHierarchyKey('key', $path); + $this->assertEquals('key', $result); + $this->assertNull($path); + + $result = $pool->exposeGetHierarchyKey('|foo|bar', $path); + $this->assertEquals('root!!!foo!!!bar!!!', $result); + $this->assertEquals('path!root!!!foo!!!bar!', $path); - return $this->cache; + $result = $pool->exposeGetHierarchyKey('|', $path); + $this->assertEquals('path!root!', $path); + $this->assertEquals('root!!!', $result); } - public function testBasicUsage() + public function testKeyCache() { - $pool = $this->createCachePool(); - $user = 4711; - for ($i = 0; $i < 10; $i++) { - $item = $pool->getItem(sprintf('|users|%d|followers|%d|likes', $user, $i)); - $item->set('Justin Bieber'); - $pool->save($item); + $path = null; + + $pool = new CachePool(['idx_1', 'idx_2', 'idx_3']); + $result = $pool->exposeGetHierarchyKey('|foo', $path); + $this->assertEquals('root!!idx_1!foo!!idx_2!', $result); + $this->assertEquals('path!root!!idx_1!foo!', $path); + + // Make sure re reuse the old index value we already looked up for 'root'. + $result = $pool->exposeGetHierarchyKey('|bar', $path); + $this->assertEquals('root!!idx_1!bar!!idx_3!', $result); + $this->assertEquals('path!root!!idx_1!bar!', $path); + } + + public function testClearHierarchyKeyCache() + { + $pool = new CachePool(); + $prop = new \ReflectionProperty('Cache\Hierarchy\Tests\Helper\CachePool', 'keyCache'); + $prop->setAccessible(true); + + // add some values to the prop and make sure they are beeing cleared + $prop->setValue($pool, ['foo' => 'bar', 'baz' => 'biz']); + $pool->exposeClearHierarchyKeyCache(); + $this->assertEmpty($prop->getValue($pool), 'The key cache must be cleared after ::ClearHierarchyKeyCache'); + } + + public function testIsHierarchyKey() + { + $pool = new CachePool(); + $method = new \ReflectionMethod('Cache\Hierarchy\Tests\Helper\CachePool', 'isHierarchyKey'); + $method->setAccessible(true); + + $this->assertFalse($method->invoke($pool, 'key')); + $this->assertFalse($method->invoke($pool, 'key|bar')); + $this->assertFalse($method->invoke($pool, 'key|')); + $this->assertTrue($method->invoke($pool, '|key')); + $this->assertTrue($method->invoke($pool, '|key|bar')); + } + + public function testExplodeKey() + { + $pool = new CachePool(); + $method = new \ReflectionMethod('Cache\Hierarchy\Tests\Helper\CachePool', 'explodeKey'); + $method->setAccessible(true); + + $result = $method->invoke($pool, '|key'); + $this->assertCount(2, $result); + $this->assertEquals('key!', $result[1]); + $this->assertTrue(in_array('key!', $result)); + + $result = $method->invoke($pool, '|key|bar'); + $this->assertCount(3, $result); + $this->assertTrue(in_array('key!', $result)); + $this->assertTrue(in_array('bar!', $result)); + + $result = $method->invoke($pool, '|'); + $this->assertCount(1, $result); + } + + public function testExplodeKeyWithTags() + { + $pool = new CachePool(); + $method = new \ReflectionMethod('Cache\Hierarchy\Tests\Helper\CachePool', 'explodeKey'); + $method->setAccessible(true); + + $result = $method->invoke($pool, '|key|bar!hash'); + $this->assertCount(3, $result); + foreach ($result as $r) { + $this->assertRegExp('|.*!hash|s', $r, 'Tag hash must be on every level in hierarchy key'); } - $this->assertTrue($pool->hasItem('|users|4711|followers|4|likes')); - $pool->deleteItem('|users|4711|followers'); - $this->assertFalse($pool->hasItem('|users|4711|followers|4|likes')); + $result = $method->invoke($pool, '|!hash'); + $this->assertCount(1, $result); + $this->assertRegExp('|.*!hash|s', $result[0], 'Tag hash must on root level in hierarchy key'); } } diff --git a/tests/IntegrationPoolTest.php b/tests/IntegrationPoolTest.php deleted file mode 100644 index 5d50072..0000000 --- a/tests/IntegrationPoolTest.php +++ /dev/null @@ -1,34 +0,0 @@ -, Tobias Nyholm - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Cache\Hierarchy; - -use Cache\Adapter\PHPArray\ArrayCachePool; -use Cache\IntegrationTests\CachePoolTest; - -class IntegrationPoolTest extends CachePoolTest -{ - private $cache; - - public function createCachePool() - { - return new HierarchicalCachePool($this->getCache()); - } - - public function getCache() - { - if ($this->cache === null) { - $this->cache = new ArrayCachePool(); - } - - return $this->cache; - } -} diff --git a/tests/IntegrationTagTest.php b/tests/IntegrationTagTest.php deleted file mode 100644 index e0213c4..0000000 --- a/tests/IntegrationTagTest.php +++ /dev/null @@ -1,34 +0,0 @@ -, Tobias Nyholm - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Cache\Hierarchy; - -use Cache\Adapter\PHPArray\ArrayCachePool; -use Cache\IntegrationTests\TaggableCachePoolTest; - -class IntegrationTagTest extends TaggableCachePoolTest -{ - private $cache; - - public function createCachePool() - { - return new HierarchicalCachePool($this->getCache()); - } - - public function getCache() - { - if ($this->cache === null) { - $this->cache = new ArrayCachePool(); - } - - return $this->cache; - } -}