diff --git a/lib/Cake/Cache/Cache.php b/lib/Cake/Cache/Cache.php index fb7e45de6..2cba7dadc 100644 --- a/lib/Cake/Cache/Cache.php +++ b/lib/Cake/Cache/Cache.php @@ -578,4 +578,42 @@ class Cache { return $results; } +/** + * Write data for key into a cache engine if it doesn't exist already. + * + * ### Usage: + * + * Writing to the active cache config: + * + * `Cache::add('cached_data', $data);` + * + * Writing to a specific cache config: + * + * `Cache::add('cached_data', $data, 'long_term');` + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached - anything except a resource. + * @param string $config Optional string configuration name to write to. Defaults to 'default'. + * @return bool True if the data was successfully cached, false on failure. + * Or if the key existed already. + */ + public static function add($key, $value, $config = 'default') { + $settings = self::settings($config); + + if (empty($settings)) { + return false; + } + if (!self::isInitialized($config)) { + return false; + } + $key = self::$_engines[$config]->key($key); + + if (!$key || is_resource($value)) { + return false; + } + + $success = self::$_engines[$config]->add($settings['prefix'] . $key, $value, $settings['duration']); + self::set(null, $config); + return $success; + } } diff --git a/lib/Cake/Cache/CacheEngine.php b/lib/Cake/Cache/CacheEngine.php index 6c611d2a9..6bad9f7ad 100644 --- a/lib/Cake/Cache/CacheEngine.php +++ b/lib/Cake/Cache/CacheEngine.php @@ -83,6 +83,17 @@ abstract class CacheEngine { */ abstract public function write($key, $value, $duration); +/** + * Write value for a key into cache if it doesn't already exist + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache for. + * @return bool True if the data was successfully cached, false on failure + */ + public function add($key, $value, $duration) { + } + /** * Read a key from the cache * @@ -176,5 +187,4 @@ abstract class CacheEngine { $key = preg_replace('/[\s]+/', '_', strtolower(trim(str_replace(array(DS, '/', '.'), '_', strval($key))))); return $prefix . $key; } - } diff --git a/lib/Cake/Cache/Engine/ApcEngine.php b/lib/Cake/Cache/Engine/ApcEngine.php index c2f0acfe1..da31651e3 100644 --- a/lib/Cake/Cache/Engine/ApcEngine.php +++ b/lib/Cake/Cache/Engine/ApcEngine.php @@ -188,4 +188,22 @@ class ApcEngine extends CacheEngine { return $success; } +/** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link http://php.net/manual/en/function.apc-add.php + */ + public function add($key, $value, $duration) { + $expires = 0; + if ($duration) { + $expires = time() + $duration; + } + apc_add($key . '_expires', $expires, $duration); + return apc_add($key, $value, $duration); + } } diff --git a/lib/Cake/Cache/Engine/FileEngine.php b/lib/Cake/Cache/Engine/FileEngine.php index 434191558..80f0f2d6b 100644 --- a/lib/Cake/Cache/Engine/FileEngine.php +++ b/lib/Cake/Cache/Engine/FileEngine.php @@ -429,4 +429,21 @@ class FileEngine extends CacheEngine { } return true; } + +/** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value, $duration) { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value, $duration); + } + return false; + } } diff --git a/lib/Cake/Cache/Engine/MemcacheEngine.php b/lib/Cake/Cache/Engine/MemcacheEngine.php index 250f02ad8..eba2ec451 100644 --- a/lib/Cake/Cache/Engine/MemcacheEngine.php +++ b/lib/Cake/Cache/Engine/MemcacheEngine.php @@ -289,4 +289,24 @@ class MemcacheEngine extends CacheEngine { public function clearGroup($group) { return (bool)$this->_Memcache->increment($this->settings['prefix'] . $group); } + +/** + * Write data for key into cache if it doesn't exist already. When using memcached as your cache engine + * remember that the Memcached PECL extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link http://php.net/manual/en/memcache.add.php + */ + public function add($key, $value, $duration) { + if ($duration > 30 * DAY) { + $duration = 0; + } + + return $this->_Memcache->add($key, $value, $this->settings['compress'], $duration); + } } diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index a2611396a..f7e7845cf 100644 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -207,7 +207,7 @@ class MemcachedEngine extends CacheEngine { /** * Write data for key into cache. When using memcached as your cache engine - * remember that the Memcached pecl extension does not support cache expiry times greater + * remember that the Memcached PECL extension does not support cache expiry times greater * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. * * @param string $key Identifier for the data @@ -335,4 +335,24 @@ class MemcachedEngine extends CacheEngine { public function clearGroup($group) { return (bool)$this->_Memcached->increment($this->settings['prefix'] . $group); } + +/** + * Write data for key into cache if it doesn't exist already. When using memcached as your cache engine + * remember that the Memcached pecl extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link http://php.net/manual/en/memcached.add.php + */ + public function add($key, $value, $duration) { + if ($duration > 30 * DAY) { + $duration = 0; + } + + return $this->_Memcached->add($key, $value, $duration); + } } diff --git a/lib/Cake/Cache/Engine/RedisEngine.php b/lib/Cake/Cache/Engine/RedisEngine.php index f471efb51..7e266c50c 100644 --- a/lib/Cake/Cache/Engine/RedisEngine.php +++ b/lib/Cake/Cache/Engine/RedisEngine.php @@ -227,4 +227,23 @@ class RedisEngine extends CacheEngine { $this->_Redis->close(); } } + +/** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link https://github.com/phpredis/phpredis#setnx + */ + public function add($key, $value, $duration) { + $result = $this->_Redis->setnx($key, $value); + // setnx() doesn't have an expiry option, so overwrite the key with one + if ($result) { + return $this->_Redis->setex($key, $value, $duration); + } + return false; + } } diff --git a/lib/Cake/Cache/Engine/WincacheEngine.php b/lib/Cake/Cache/Engine/WincacheEngine.php index 392955718..e5c9d7bd7 100644 --- a/lib/Cake/Cache/Engine/WincacheEngine.php +++ b/lib/Cake/Cache/Engine/WincacheEngine.php @@ -188,4 +188,20 @@ class WincacheEngine extends CacheEngine { return $success; } +/** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value, $duration) { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value, $duration); + } + return false; + } } diff --git a/lib/Cake/Cache/Engine/XcacheEngine.php b/lib/Cake/Cache/Engine/XcacheEngine.php index 97c851818..c46a7b9c5 100644 --- a/lib/Cake/Cache/Engine/XcacheEngine.php +++ b/lib/Cake/Cache/Engine/XcacheEngine.php @@ -207,4 +207,21 @@ class XcacheEngine extends CacheEngine { } } } + +/** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value, $duration) { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value, $duration); + } + return false; + } } diff --git a/lib/Cake/Test/Case/Cache/CacheTest.php b/lib/Cake/Test/Case/Cache/CacheTest.php index 0bdb3f1fa..985adb4f6 100644 --- a/lib/Cake/Test/Case/Cache/CacheTest.php +++ b/lib/Cake/Test/Case/Cache/CacheTest.php @@ -519,4 +519,23 @@ class CacheTest extends CakeTestCase { public function cacher() { return 'This is some data ' . $this->_count; } + +/** + * Test add method. + * + * @return void + */ + public function testAdd() { + Cache::delete('test_add_key', 'default'); + + $result = Cache::add('test_add_key', 'test data', 'default'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'default'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'default'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/ApcEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/ApcEngineTest.php index 9c56241c2..0985e0c21 100644 --- a/lib/Cake/Test/Case/Cache/Engine/ApcEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/ApcEngineTest.php @@ -273,4 +273,23 @@ class ApcEngineTest extends CakeTestCase { $this->assertTrue(Cache::clearGroup('group_b', 'apc_groups')); $this->assertFalse(Cache::read('test_groups', 'apc_groups')); } + +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::delete('test_add_key', 'apc'); + + $result = Cache::add('test_add_key', 'test data', 'apc'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'apc'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'apc'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/FileEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/FileEngineTest.php index cfd4ae916..07cdac310 100644 --- a/lib/Cake/Test/Case/Cache/Engine/FileEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/FileEngineTest.php @@ -550,4 +550,22 @@ class FileEngineTest extends CakeTestCase { $this->assertFalse(Cache::read('key_2', 'file_groups'), 'Did not delete'); } +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::delete('test_add_key', 'file_test'); + + $result = Cache::add('test_add_key', 'test data', 'file_test'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'file_test'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'file_test'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcacheEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcacheEngineTest.php index 36f8f0d8b..2060d1527 100644 --- a/lib/Cake/Test/Case/Cache/Engine/MemcacheEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcacheEngineTest.php @@ -490,4 +490,23 @@ class MemcacheEngineTest extends CakeTestCase { $this->assertTrue(Cache::clearGroup('group_b', 'memcache_groups')); $this->assertFalse(Cache::read('test_groups', 'memcache_groups')); } + +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::delete('test_add_key', 'memcache'); + + $result = Cache::add('test_add_key', 'test data', 'memcache'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'memcache'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'memcache'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index 17f1f9972..072bf66e3 100644 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -797,4 +797,24 @@ class MemcachedEngineTest extends CakeTestCase { $this->assertTrue(Cache::clearGroup('group_b', 'memcached_groups')); $this->assertFalse(Cache::read('test_groups', 'memcached_groups')); } + +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::set(array('duration' => 1), null, 'memcached'); + Cache::delete('test_add_key', 'default'); + + $result = Cache::add('test_add_key', 'test data', 'default'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'default'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'default'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php index c3e19acca..65000385e 100644 --- a/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/RedisEngineTest.php @@ -381,4 +381,22 @@ class RedisEngineTest extends CakeTestCase { $this->assertFalse(Cache::read('test_groups', 'redis_groups')); } +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::delete('test_add_key', 'redis'); + + $result = Cache::add('test_add_key', 'test data', 'redis'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'redis'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'redis'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/WincacheEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/WincacheEngineTest.php index 2dd68c85c..316ee0040 100644 --- a/lib/Cake/Test/Case/Cache/Engine/WincacheEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/WincacheEngineTest.php @@ -259,4 +259,23 @@ class WincacheEngineTest extends CakeTestCase { $this->assertTrue(Cache::clearGroup('group_b', 'wincache_groups')); $this->assertFalse(Cache::read('test_groups', 'wincache_groups')); } + +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::delete('test_add_key', 'wincache'); + + $result = Cache::add('test_add_key', 'test data', 'wincache'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'wincache'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'wincache'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/XcacheEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/XcacheEngineTest.php index fef083a1c..16c8643cb 100644 --- a/lib/Cake/Test/Case/Cache/Engine/XcacheEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/XcacheEngineTest.php @@ -268,4 +268,24 @@ class XcacheEngineTest extends CakeTestCase { $this->assertTrue(Cache::clearGroup('group_b', 'xcache_groups')); $this->assertFalse(Cache::read('test_groups', 'xcache_groups')); } + +/** + * Test that failed add write return false. + * + * @return void + */ + public function testAdd() { + Cache::set(array('duration' => 1), null); + Cache::delete('test_add_key', 'default'); + + $result = Cache::add('test_add_key', 'test data', 'default'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'default'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'default'); + $this->assertFalse($result); + } } diff --git a/lib/Cake/Test/test_app/Lib/Cache/Engine/TestAppCacheEngine.php b/lib/Cake/Test/test_app/Lib/Cache/Engine/TestAppCacheEngine.php index 263592db2..4d89deb43 100644 --- a/lib/Cake/Test/test_app/Lib/Cache/Engine/TestAppCacheEngine.php +++ b/lib/Cake/Test/test_app/Lib/Cache/Engine/TestAppCacheEngine.php @@ -46,4 +46,7 @@ class TestAppCacheEngine extends CacheEngine { public function clearGroup($group) { } + + public function add($key, $value, $duration) { + } } diff --git a/lib/Cake/Test/test_app/Plugin/TestPlugin/Lib/Cache/Engine/TestPluginCacheEngine.php b/lib/Cake/Test/test_app/Plugin/TestPlugin/Lib/Cache/Engine/TestPluginCacheEngine.php index aca459cba..6f2a4c792 100644 --- a/lib/Cake/Test/test_app/Plugin/TestPlugin/Lib/Cache/Engine/TestPluginCacheEngine.php +++ b/lib/Cake/Test/test_app/Plugin/TestPlugin/Lib/Cache/Engine/TestPluginCacheEngine.php @@ -43,4 +43,7 @@ class TestPluginCacheEngine extends CacheEngine { public function clearGroup($group) { } + + public function add($key, $value, $duration) { + } }