diff --git a/composer.json b/composer.json index 99850ef99849c..504f2b49b6e75 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "twig/twig": "~1.28|~2.0", "psr/cache": "~1.0", "psr/log": "~1.0", + "psr/simple-cache": "^0.2", "symfony/polyfill-intl-icu": "~1.0", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php56": "~1.0", @@ -97,7 +98,8 @@ "phpdocumentor/type-resolver": "<0.2.0" }, "provide": { - "psr/cache-implementation": "1.0" + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" }, "autoload": { "psr-4": { diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 4a788963ea74b..3c3549f3ea11c 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -15,13 +15,14 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; +use Psr\SimpleCache\CounterInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; /** * @author Nicolas Grekas
*/ -abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface +abstract class AbstractAdapter implements AdapterInterface, CounterInterface, LoggerAwareInterface { use LoggerAwareTrait; @@ -372,6 +373,40 @@ public function commit() return $ok; } + /** + * {@inheritdoc} + * + * This implementation is not atomic unless the "doIncrement()" method provided by the concrete adapter is. + */ + public function increment($key, $step = 1) + { + if (!is_numeric($step)) { + return false; + } + $id = $this->getId($key); + + try { + if (is_numeric($result = $this->doIncrement($id, (int) $step))) { + return $result; + } + CacheItem::log($this->logger, 'Failed to increment key "{key}"', array('key' => $key)); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to increment key "{key}"', array('key' => $key, 'exception' => $e)); + } + + return false; + } + + /** + * {@inheritdoc} + * + * This implementation is not atomic unless the "doIncrement()" method provided by the concrete adapter is. + */ + public function decrement($key, $step = 1) + { + return is_numeric($step) ? $this->increment($key, -$step) : false; + } + public function __destruct() { if ($this->deferred) { @@ -406,6 +441,29 @@ protected static function unserialize($value) } } + /** + * Increments a value in the cache by the given step value and returns the new value. + * + * If the key does not exist, it is initialized to the value of $step. + * + * @param string $key The cache item key + * @param int $step The value to increment by + * + * @return int|bool The new value on success and false on failure + */ + protected function doIncrement($id, $step) + { + foreach ($this->doFetch(array($id)) as $value) { + if (is_numeric($value)) { + $step += $value; + } + } + + $e = $this->doSave(array($id => $step), 0); + + return true === $e || array() === $e ? $step : false; + } + private function getId($key) { CacheItem::validateKey($key); diff --git a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php index 67afd5c72a89e..04174c64a94fa 100644 --- a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php @@ -104,4 +104,12 @@ protected function doSave(array $values, $lifetime) throw $e; } + + /** + * {@inheritdoc} + */ + protected function doIncrement($id, $step) + { + return apcu_inc($id, $step); + } } diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 2898ba50cdc9a..b758bc44cafc8 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -14,12 +14,13 @@ use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Psr\SimpleCache\CounterInterface; use Symfony\Component\Cache\CacheItem; /** * @author Nicolas Grekas
*/ -class ArrayAdapter implements AdapterInterface, LoggerAwareInterface +class ArrayAdapter implements AdapterInterface, CounterInterface, LoggerAwareInterface { use LoggerAwareTrait; @@ -197,6 +198,33 @@ public function commit() return true; } + /** + * {@inheritdoc} + */ + public function increment($key, $step = 1) + { + if (!is_numeric($step)) { + return false; + } + + if ($this->hasItem($key) && is_numeric($this->values[$key])) { + $this->values[$key] += (int) $step; + } else { + $this->values[$key] = (int) $step; + $this->expiries[$key] = PHP_INT_MAX; + } + + return $this->values[$key]; + } + + /** + * {@inheritdoc} + */ + public function decrement($key, $step = 1) + { + return is_numeric($step) ? $this->increment($key, -$step) : false; + } + private function generateItems(array $keys, $now) { $f = $this->createCacheItem; diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php b/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php index faf6e50f0f948..c9722532ab381 100644 --- a/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php +++ b/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php @@ -79,6 +79,25 @@ protected function doDelete(array $ids) return $ok; } + /** + * {@inheritdoc} + */ + protected function doIncrement($id, $step) + { + if (!$lock = @fopen($this->getFile($id, true), 'cb')) { + return false; + } + if (!flock($lock, LOCK_EX)) { + return false; + } + $result = parent::doIncrement($id, $step); + + flock($lock, LOCK_UN); + fclose($lock); + + return $result; + } + private function write($file, $data, $expiresAt = null) { if (false === @file_put_contents($this->tmp, $data)) { diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index 24245fcf27d10..7b7de8a682017 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -335,6 +335,19 @@ protected function doSave(array $values, $lifetime) return $failed; } + /** + * {@inheritdoc} + */ + protected function doIncrement($id, $step) + { + $conn = $this->getConnection(); + $conn->beginTransaction(); + $result = parent::doIncrement($id, $step); + $conn->commit(); + + return $result; + } + /** * @return \PDO|Connection */ diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php index fccfb926317d7..e049993f95377 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php @@ -266,6 +266,14 @@ protected function doSave(array $values, $lifetime) return $failed; } + /** + * {@inheritdoc} + */ + protected function doIncrement($id, $step) + { + return $this->redis->incrBy($id, $step); + } + private function execute($command, $id, array $args, $redis = null) { array_unshift($args, $id); diff --git a/src/Symfony/Component/Cache/SimpleCache.php b/src/Symfony/Component/Cache/SimpleCache.php new file mode 100644 index 0000000000000..976ec8fdf4de3 --- /dev/null +++ b/src/Symfony/Component/Cache/SimpleCache.php @@ -0,0 +1,179 @@ + + * + * 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\CacheItemPoolInterface; +use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\CounterInterface; + +/** + * @author Nicolas Grekas
+ */ +class SimpleCache implements CacheInterface, CounterInterface +{ + private $pool; + private $createCacheItem; + + public function __construct(CacheItemPoolInterface $pool) + { + $this->pool = $pool; + + if ($pool instanceof Adapter\AdapterInterface) { + $this->createCacheItem = \Closure::bind( + function ($key, $value) { + CacheItem::validateKey($key); + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + + return $item; + }, + null, + CacheItem::class + ); + } else { + $this->createCacheItem = function ($key, $value) { + return $this->pool->getItem($key)->set($value); + }; + } + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $item = $this->pool->getItem($key); + + return $item->isHit() ? $item->get() : $default; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $f = $this->createCacheItem; + $item = $f($key, $value); + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + + return $this->pool->save($item); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + return $this->pool->deleteItem($key); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys) + { + if ($key instanceof \Traversable) { + $key = iterator_to_array($key); + } + $values = array(); + + foreach ($this->pool->getItems($keys) as $key => $item) { + $values[$key] = $item->get(); + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($items, $ttl = null) + { + $f = $this->createCacheItem; + $ok = true; + + foreach ($items as $key => $value) { + $item = $f($key, $value); + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + $ok = $this->pool->saveDeferred($item) && $ok; + } + + return $this->pool->commit() && $ok; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + if ($key instanceof \Traversable) { + $key = iterator_to_array($key); + } + + return $this->pool->deleteItems($keys); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return $this->pool->hasItem($key); + } + + /** + * {@inheritdoc} + * + * This implementation is not atomic unless the underlying pool implements PSR-16's CounterInterface. + */ + public function increment($key, $step = 1) + { + if ($this->pool instanceof CounterInterface) { + return $this->pool->increment($key, $step); + } + if (!is_numeric($step)) { + return false; + } + $step = (int) $step; + + $item = $this->pool->getItem($key); + if (is_numeric($value = $item->get())) { + $step += $value; + } + $item->set($step); + + return $this->pool->save($item) ? $step : false; + } + + /** + * {@inheritdoc} + * + * This method is atomic only if the underlying pool implements PSR-16's CounterInterface. + */ + public function decrement($key, $step = 1) + { + return is_numeric($step) ? $this->increment($key, -$step) : false; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 4bc80ee30022e..6362027438605 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Cache\IntegrationTests\CachePoolTest; +use Psr\SimpleCache\CounterInterface; abstract class AdapterTestCase extends CachePoolTest { @@ -28,8 +29,6 @@ public function testDefaultLifeTime() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); - - return; } $cache = $this->createCachePool(2); @@ -51,8 +50,6 @@ public function testNotUnserializable() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); - - return; } $cache = $this->createCachePool(); @@ -71,6 +68,26 @@ public function testNotUnserializable() } $this->assertFalse($item->isHit()); } + + public function testCounterInterface() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + $cache = $this->createCachePool(); + + if (!$cache instanceof CounterInterface) { + $this->markTestSkipped(get_class($cache).' does not implement Psr\SimpleCache\CounterInterface'); + } + + $this->assertSame(1, $cache->increment('foo')); + $this->assertSame(5, $cache->increment('foo', 4)); + $this->assertSame(3, $cache->increment('foo', -2)); + + $this->assertSame(2, $cache->decrement('foo')); + $this->assertSame(-4, $cache->decrement('bar', 4)); + $this->assertSame(-2, $cache->decrement('bar', -2)); + } } class NotUnserializable implements \Serializable diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index 6cb772a80fc93..45a704ee91828 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -16,12 +16,14 @@ } ], "provide": { - "psr/cache-implementation": "1.0" + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" }, "require": { "php": ">=5.5.9", "psr/cache": "~1.0", - "psr/log": "~1.0" + "psr/log": "~1.0", + "psr/simple-cache": "^0.2" }, "require-dev": { "cache/integration-tests": "dev-master",