diff --git a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php new file mode 100644 index 000000000000..4dde64e1db88 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php @@ -0,0 +1,217 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface +{ + private $adapter; + private $deferred = array(); + private $createCacheItem; + private $getTagsByKey; + + /** + * Removes tag-invalidated keys and returns the removed ones. + * + * @param array &$keys The keys to filter + * + * @return array The keys removed from $keys + */ + abstract protected function filterInvalidatedKeys(array &$keys); + + /** + * Persists tags for cache keys. + * + * @param array $tags The tags for each cache keys as index + * + * @return bool True on success + */ + abstract protected function doSaveTags(array $tags); + + public function __construct(AdapterInterface $adapter, $defaultLifetime) + { + $this->adapter = $adapter; + $this->createCacheItem = \Closure::bind( + function ($key) use ($defaultLifetime) { + $item = new CacheItem(); + $item->key = $key; + $item->isHit = false; + $item->defaultLifetime = $defaultLifetime; + + return $item; + }, + null, + CacheItem::class + ); + $this->getTagsByKey = \Closure::bind( + function ($deferred) { + $tagsByKey = array(); + foreach ($deferred as $key => $item) { + $tagsByKey[$key] = $item->tags; + } + + return $tagsByKey; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + if ($this->deferred) { + $this->commit(); + } + if (!$this->adapter->hasItem($key)) { + return false; + } + $keys = array($key); + + return !$this->filterInvalidatedKeys($keys); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if ($this->deferred) { + $this->commit(); + } + $keys = array($key); + + if ($keys = $this->filterInvalidatedKeys($keys)) { + foreach ($this->generateItems(array(), $keys) as $item) { + return $item; + } + } + + return $this->adapter->getItem($key); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + if ($this->deferred) { + $this->commit(); + } + $invalids = $this->filterInvalidatedKeys($keys); + $items = $this->adapter->getItems($keys); + + return $this->generateItems($items, $invalids); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->deferred = array(); + + return $this->adapter->clear(); + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + return $this->adapter->deleteItem($key); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + return $this->adapter->deleteItems($keys); + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + if ($this->deferred) { + $this->commit(); + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $ok = true; + + if ($this->deferred) { + foreach ($this->deferred as $key => $item) { + if (!$this->adapter->saveDeferred($item)) { + unset($this->deferred[$key]); + $ok = false; + } + } + $f = $this->getTagsByKey; + $ok = $this->doSaveTags($f($this->deferred)) && $ok; + $this->deferred = array(); + } + + return $this->adapter->commit() && $ok; + } + + public function __destruct() + { + $this->commit(); + } + + private function generateItems($items, $invalids) + { + foreach ($items as $key => $item) { + yield $key => $item; + } + + $f = $this->createCacheItem; + + foreach ($invalids as $key) { + yield $key => $f($key); + } + } +} diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php index 92c065ebf7c7..8f5573b7790d 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Cache\Adapter; use Predis\Connection\Factory; -use Predis\Connection\Aggregate\PredisCluster; -use Predis\Connection\Aggregate\RedisCluster; use Symfony\Component\Cache\Exception\InvalidArgumentException; /** @@ -22,6 +20,8 @@ */ class RedisAdapter extends AbstractAdapter { + use RedisAdapterTrait; + private static $defaultConnectionOptions = array( 'class' => null, 'persistent' => 0, @@ -29,7 +29,6 @@ class RedisAdapter extends AbstractAdapter 'read_timeout' => 0, 'retry_interval' => 0, ); - private $redis; /** * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient @@ -37,14 +36,7 @@ class RedisAdapter extends AbstractAdapter public function __construct($redisClient, $namespace = '', $defaultLifetime = 0) { parent::__construct($namespace, $defaultLifetime); - - if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { - throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); - } - if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) { - throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient))); - } - $this->redis = $redisClient; + $this->setRedis($redisClient, $namespace); } /** @@ -157,51 +149,6 @@ protected function doHave($id) return (bool) $this->redis->exists($id); } - /** - * {@inheritdoc} - */ - protected function doClear($namespace) - { - // When using a native Redis cluster, clearing the cache cannot work and always returns false. - // Clearing the cache should then be done by any other means (e.g. by restarting the cluster). - - $hosts = array($this->redis); - $evalArgs = array(array($namespace), 0); - - if ($this->redis instanceof \Predis\Client) { - $evalArgs = array(0, $namespace); - - $connection = $this->redis->getConnection(); - if ($connection instanceof PredisCluster) { - $hosts = array(); - foreach ($connection as $c) { - $hosts[] = new \Predis\Client($c); - } - } elseif ($connection instanceof RedisCluster) { - return false; - } - } elseif ($this->redis instanceof \RedisArray) { - foreach ($this->redis->_hosts() as $host) { - $hosts[] = $this->redis->_instance($host); - } - } elseif ($this->redis instanceof \RedisCluster) { - return false; - } - foreach ($hosts as $host) { - if (!isset($namespace[0])) { - $host->flushDb(); - } else { - // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS - // can hang your server when it is executed against large databases (millions of items). - // Whenever you hit this scale, it is advised to deploy one Redis database per cache pool - // instead of using namespaces, so that FLUSHDB is used instead. - $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end", $evalArgs[0], $evalArgs[1]); - } - } - - return true; - } - /** * {@inheritdoc} */ @@ -248,47 +195,4 @@ protected function doSave(array $values, $lifetime) return $failed; } - - private function execute($command, $id, array $args, $redis = null) - { - array_unshift($args, $id); - call_user_func_array(array($redis ?: $this->redis, $command), $args); - } - - private function pipeline(\Closure $callback) - { - $redis = $this->redis; - - try { - if ($redis instanceof \Predis\Client) { - $redis->pipeline(function ($pipe) use ($callback) { - $this->redis = $pipe; - $callback(array($this, 'execute')); - }); - } elseif ($redis instanceof \RedisArray) { - $connections = array(); - $callback(function ($command, $id, $args) use (&$connections) { - if (!isset($connections[$h = $this->redis->_target($id)])) { - $connections[$h] = $this->redis->_instance($h); - $connections[$h]->multi(\Redis::PIPELINE); - } - $this->execute($command, $id, $args, $connections[$h]); - }); - foreach ($connections as $c) { - $c->exec(); - } - } else { - $pipe = $redis->multi(\Redis::PIPELINE); - try { - $callback(array($this, 'execute')); - } finally { - if ($pipe) { - $redis->exec(); - } - } - } - } finally { - $this->redis = $redis; - } - } } diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapterTrait.php b/src/Symfony/Component/Cache/Adapter/RedisAdapterTrait.php new file mode 100644 index 000000000000..b906e47067ea --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapterTrait.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Predis\Connection\Aggregate\RedisCluster; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @internal + * + * @author Nicolas Grekas + */ +trait RedisAdapterTrait +{ + private $redis; + private $namespace; + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + // When using a native Redis cluster, clearing the cache cannot work and always returns false. + // Clearing the cache should then be done by any other means (e.g. by restarting the cluster). + + $hosts = array($this->redis); + $evalArgs = array(array($namespace), 0); + + if ($this->redis instanceof \Predis\Client) { + $evalArgs = array(0, $namespace); + + $connection = $this->redis->getConnection(); + if ($connection instanceof PredisCluster) { + $hosts = array(); + foreach ($connection as $c) { + $hosts[] = new \Predis\Client($c); + } + } elseif ($connection instanceof RedisCluster) { + return false; + } + } elseif ($this->redis instanceof \RedisArray) { + foreach ($this->redis->_hosts() as $host) { + $hosts[] = $this->redis->_instance($host); + } + } elseif ($this->redis instanceof \RedisCluster) { + return false; + } + foreach ($hosts as $host) { + if (!isset($namespace[0])) { + $host->flushDb(); + } else { + // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS + // can hang your server when it is executed against large databases (millions of items). + // Whenever you hit this scale, it is advised to deploy one Redis database per cache pool + // instead of using namespaces, so that FLUSHDB is used instead. + $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end", $evalArgs[0], $evalArgs[1]); + } + } + + return true; + } + + private function setRedis($redisClient, $namespace) + { + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) { + throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient))); + } + $this->redis = $redisClient; + $this->namespace = $namespace; + } + + private function execute($command, $id, array $args = array(), $redis = null) + { + array_unshift($args, $id); + call_user_func_array(array($redis ?: $this->redis, $command), $args); + } + + private function pipeline(\Closure $callback) + { + $redis = $this->redis; + + try { + if ($redis instanceof \Predis\Client) { + $redis->pipeline(function ($pipe) use ($callback) { + $this->redis = $pipe; + $callback(array($this, 'execute')); + }); + } elseif ($redis instanceof \RedisArray) { + $connections = array(); + $callback(function ($command, $id, $args = array()) use (&$connections) { + if (!isset($connections[$h = $this->redis->_target($id)])) { + $connections[$h] = $this->redis->_instance($h); + $connections[$h]->multi(\Redis::PIPELINE); + } + $this->execute($command, $id, $args, $connections[$h]); + }); + foreach ($connections as $c) { + $c->exec(); + } + } else { + $pipe = $redis->multi(\Redis::PIPELINE); + try { + $callback(array($this, 'execute')); + } finally { + if ($pipe) { + $redis->exec(); + } + } + } + } finally { + $this->redis = $redis; + } + } +} diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapterInterface.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapterInterface.php new file mode 100644 index 000000000000..6fab3e8546db --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapterInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareAdapterInterface extends AdapterInterface +{ + /** + * Invalidates cached items using tags. + * + * @param string|string[] $tags A tag or an array of tags to invalidate. + * + * @return bool True on success. + * + * @throws InvalidArgumentException When $tags is not valid. + */ + public function invalidateTags($tags); +} diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareRedisAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareRedisAdapter.php new file mode 100644 index 000000000000..d183becc6473 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/TagAwareRedisAdapter.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\CacheItem; + +/** + * @author Nicolas Grekas + */ +class TagAwareRedisAdapter extends AbstractTagAwareAdapter +{ + use RedisAdapterTrait; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient + */ + public function __construct($redisClient, $namespace = '', $defaultLifetime = 0, AdapterInterface $adapter = null) + { + parent::__construct($adapter ?: new RedisAdapter($redisClient, $namespace, $defaultLifetime), $defaultLifetime); + $this->setRedis($redisClient, $namespace); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $ok = $this->doClear($this->namespace); + + return parent::clear() && $ok; + } + + /** + * {@inheritdoc} + */ + public function invalidateTags($tags) + { + if (!is_array($tags)) { + $tags = array($tags); + } + $this->pipeline(function ($pipe) use ($tags) { + foreach ($tags as $tag) { + CacheItem::validateKey($tag); + $pipe('incr', $this->namespace.'tag:'.$tag); + } + }); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function filterInvalidatedKeys(array &$keys) + { + $tags = $invalids = array(); + + foreach ($keys as $i => $key) { + CacheItem::validateKey($key); + + foreach ($this->redis->hGetAll($this->namespace.$key.':tags') as $tag => $version) { + $tags[$this->namespace.'tag:'.$tag][$version][$i] = $key; + } + } + if ($tags) { + $j = 0; + $versions = $this->redis->mGet(array_keys($tags)); + + foreach ($tags as $tag => $version) { + $version = $versions[$j++]; + unset($tags[$tag][(int) $version]); + + foreach ($tags[$tag] as $version) { + foreach ($version as $i => $key) { + $invalids[] = $key; + unset($keys[$i]); + } + } + } + } + + return $invalids; + } + + /** + * {@inheritdoc} + */ + protected function doSaveTags(array $tagsByKey) + { + $tagVersions = array(); + + foreach ($tagsByKey as $key => $tags) { + foreach ($tags as $tag) { + $tagVersions[$tag] = $this->namespace.'tag:'.$tag; + } + } + + if ($tagVersions) { + $tagVersions = array_combine(array_keys($tagVersions), $this->redis->mGet($tagVersions)); + $tagVersions = array_map('intval', $tagVersions); + } + + $this->pipeline(function ($pipe) use ($tagsByKey, $tagVersions) { + foreach ($tagsByKey as $key => $tags) { + $pipe('del', $this->namespace.$key.':tags'); + if ($tags) { + foreach (array_intersect_key($tagVersions, $tags) as $tag => $version) { + $pipe('hSet', $this->namespace.$key.':tags', array($tag, $version)); + } + } + } + }); + + return true; + } +} diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 2678da4a1d58..dec08b054236 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -11,20 +11,20 @@ namespace Symfony\Component\Cache; -use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Cache\Exception\InvalidArgumentException; /** * @author Nicolas Grekas */ -final class CacheItem implements CacheItemInterface +final class CacheItem implements TaggedCacheItemInterface { protected $key; protected $value; protected $isHit; protected $expiry; protected $defaultLifetime; + protected $tags = array(); protected $innerItem; protected $poolHash; @@ -96,6 +96,33 @@ public function expiresAfter($time) return $this; } + /** + * {@inheritdoc} + */ + public function tag($tags) + { + if (!is_array($tags)) { + $tags = array($tags); + } + foreach ($tags as $tag) { + if (!is_string($tag)) { + throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given', is_object($tag) ? get_class($tag) : gettype($tag))); + } + if (isset($this->tags[$tag])) { + continue; + } + if (!isset($tag[0])) { + throw new InvalidArgumentException('Cache tag length must be greater than zero'); + } + if (isset($tag[strcspn($tag, '{}()/\@:')])) { + throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters {}()/\@:', $tag)); + } + $this->tags[$tag] = $tag; + } + + return $this; + } + /** * Validates a cache key according to PSR-6. * diff --git a/src/Symfony/Component/Cache/README.md b/src/Symfony/Component/Cache/README.md index 3ece466aec9a..604fb1d5b410 100644 --- a/src/Symfony/Component/Cache/README.md +++ b/src/Symfony/Component/Cache/README.md @@ -1,7 +1,7 @@ Symfony PSR-6 implementation for caching ======================================== -This component provides a strict [PSR-6](http://www.php-fig.org/psr/psr-6/) +This component provides an extended [PSR-6](http://www.php-fig.org/psr/psr-6/) implementation for adding cache to your applications. It is designed to have a low overhead so that caching is fastest. It ships with a few caching adapters for the most widespread and suited to caching backends. It also provides a diff --git a/src/Symfony/Component/Cache/TaggedCacheItemInterface.php b/src/Symfony/Component/Cache/TaggedCacheItemInterface.php new file mode 100644 index 000000000000..00e0d041d229 --- /dev/null +++ b/src/Symfony/Component/Cache/TaggedCacheItemInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for adding tags to cache items. + * + * @author Nicolas Grekas + */ +interface TaggedCacheItemInterface extends CacheItemInterface +{ + /** + * Adds a tag to a cache item. + * + * @param string|string[] $tags A tag or array of tags. + * + * @return static + * + * @throws InvalidArgumentException When $tag is not valid. + */ + public function tag($tags); +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTestTrait.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTestTrait.php new file mode 100644 index 000000000000..639efd37bd47 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTestTrait.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +trait TagAwareAdapterTestTrait +{ + /** + * @expectedException Psr\Cache\InvalidArgumentException + */ + public function testInvalidTag() + { + $pool = $this->createCachePool(); + $item = $pool->getItem('foo'); + $item->tag(':'); + } + + public function testInvalidateTags() + { + $pool = $this->createCachePool(); + + $i0 = $pool->getItem('i0'); + $i1 = $pool->getItem('i1'); + $i2 = $pool->getItem('i2'); + $i3 = $pool->getItem('i3'); + $foo = $pool->getItem('foo'); + + $pool->save($i0->tag('bar')); + $pool->save($i1->tag('foo')); + $pool->save($i2->tag('foo')->tag('bar')); + $pool->save($i3->tag('foo')->tag('baz')); + $pool->save($foo); + + $pool->invalidateTags('bar'); + + $this->assertFalse($pool->getItem('i0')->isHit()); + $this->assertTrue($pool->getItem('i1')->isHit()); + $this->assertFalse($pool->getItem('i2')->isHit()); + $this->assertTrue($pool->getItem('i3')->isHit()); + $this->assertTrue($pool->getItem('foo')->isHit()); + + $pool->invalidateTags('foo'); + + $this->assertFalse($pool->getItem('i1')->isHit()); + $this->assertFalse($pool->getItem('i3')->isHit()); + $this->assertTrue($pool->getItem('foo')->isHit()); + } + + public function testTagsAreCleanedOnSave() + { + $pool = $this->createCachePool(); + + $i = $pool->getItem('k'); + $pool->save($i->tag('foo')); + + $i = $pool->getItem('k'); + $pool->save($i->tag('bar')); + + $pool->invalidateTags('foo'); + $this->assertTrue($pool->getItem('k')->isHit()); + } + + public function testTagsAreCleanedOnDelete() + { + $pool = $this->createCachePool(); + + $i = $pool->getItem('k'); + $pool->save($i->tag('foo')); + $pool->deleteItem('k'); + + $pool->save($pool->getItem('k')); + $pool->invalidateTags('foo'); + + $this->assertTrue($pool->getItem('k')->isHit()); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareRedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareRedisAdapterTest.php new file mode 100644 index 000000000000..2a61d49bce70 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareRedisAdapterTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\TagAwareRedisAdapter; + +class TagAwareRedisAdapterTest extends AbstractRedisAdapterTest +{ + use TagAwareAdapterTestTrait; + + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + self::$redis = new \Redis(); + self::$redis->connect('127.0.0.1'); + } + + public function createCachePool() + { + if (defined('HHVM_VERSION')) { + $this->skippedTests['testDeferredSaveWithoutCommit'] = 'Fails on HHVM'; + } + + return new TagAwareRedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareRedisArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareRedisArrayAdapterTest.php new file mode 100644 index 000000000000..0a0f212eee28 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareRedisArrayAdapterTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\TagAwareRedisAdapter; + +class TagAwareRedisArrayAdapterTest extends AbstractRedisAdapterTest +{ + use TagAwareAdapterTestTrait; + + protected $skippedTests = array( + 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', + 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', + ); + + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + self::$redis = new \Redis(); + self::$redis->connect('127.0.0.1'); + } + + public function createCachePool() + { + if (defined('HHVM_VERSION')) { + $this->skippedTests['testDeferredSaveWithoutCommit'] = 'Fails on HHVM'; + } + + return new TagAwareRedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), 0, new ArrayAdapter()); + } +} diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php index 7b20a41ba2fe..d9bfeb4cf856 100644 --- a/src/Symfony/Component/Cache/Tests/CacheItemTest.php +++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.php @@ -46,8 +46,31 @@ public function provideInvalidKey() array(null), array(1), array(1.1), - array(array()), + array(array(array())), array(new \Exception('foo')), ); } + + public function testTag() + { + $item = new CacheItem(); + + $this->assertSame($item, $item->tag('foo')); + $this->assertSame($item, $item->tag(array('bar', 'baz'))); + + call_user_func(\Closure::bind(function () use ($item) { + $this->assertSame(array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'), $item->tags); + }, $this, CacheItem::class)); + } + + /** + * @dataProvider provideInvalidKey + * @expectedException Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Cache tag + */ + public function testInvalidTag($tag) + { + $item = new CacheItem(); + $item->tag($tag); + } }