diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index c5062411c26..b07764e4507 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -540,4 +540,32 @@ public static function remember($key, $callable, $config = 'default') self::write($key, $results, $config); 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') + { + $engine = static::engine($config); + if (is_resource($value)) { + return false; + } + return $engine->add($key, $value); + } } diff --git a/src/Cache/CacheEngine.php b/src/Cache/CacheEngine.php index fff3da61b6a..ed65f4d8fa8 100644 --- a/src/Cache/CacheEngine.php +++ b/src/Cache/CacheEngine.php @@ -165,6 +165,15 @@ abstract public function decrement($key, $offset = 1); */ abstract public function delete($key); + + /** + * Delete all keys from the cache + * + * @param bool $check if true will check expiration, otherwise delete all + * @return bool True if the cache was successfully cleared, false otherwise + */ + abstract public function clear($check); + /** * Deletes keys from the cache * @@ -182,12 +191,23 @@ public function deleteMany($keys) } /** - * Delete all keys from the cache + * Add a key to the cache if it does not already exist. * - * @param bool $check if true will check expiration, otherwise delete all - * @return bool True if the cache was successfully cleared, false otherwise + * Defaults to a non-atomic implementation. Subclasses should + * prefer atomic implementations. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @return bool True if the data was successfully cached, false on failure. */ - abstract public function clear($check); + public function add($key, $value) + { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value); + } + return false; + } /** * Clears all values belonging to a group. Is up to the implementing engine diff --git a/src/Cache/Engine/ApcEngine.php b/src/Cache/Engine/ApcEngine.php index 88d592301ed..a6172aa2c86 100644 --- a/src/Cache/Engine/ApcEngine.php +++ b/src/Cache/Engine/ApcEngine.php @@ -160,6 +160,28 @@ public function clear($check) 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. + * @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) + { + $key = $this->_key($key); + + $expires = 0; + $duration = $this->_config['duration']; + if ($duration) { + $expires = time() + $duration; + } + apc_add($key . '_expires', $expires, $duration); + return apc_add($key, $value, $duration); + } + /** * Returns the `group value` for each of the configured groups * If the group initial value was not found, then it initializes diff --git a/src/Cache/Engine/MemcachedEngine.php b/src/Cache/Engine/MemcachedEngine.php index 62661e78943..28689cd9c67 100644 --- a/src/Cache/Engine/MemcachedEngine.php +++ b/src/Cache/Engine/MemcachedEngine.php @@ -422,6 +422,9 @@ public function clear($check) } $keys = $this->_Memcached->getAllKeys(); + if ($keys === false) { + return false; + } foreach ($keys as $key) { if (strpos($key, $this->_config['prefix']) === 0) { @@ -432,6 +435,24 @@ public function clear($check) return true; } + /** + * Add a key to the cache if it does not already exist. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value) + { + $duration = $this->_config['duration']; + if ($duration > 30 * DAY) { + $duration = 0; + } + + $key = $this->_key($key); + return $this->_Memcached->add($key, $value, $duration); + } + /** * Returns the `group value` for each of the configured groups * If the group initial value was not found, then it initializes diff --git a/src/Cache/Engine/RedisEngine.php b/src/Cache/Engine/RedisEngine.php index 505a98199f0..872038c4643 100644 --- a/src/Cache/Engine/RedisEngine.php +++ b/src/Cache/Engine/RedisEngine.php @@ -214,6 +214,31 @@ public function clear($check) 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. + * @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 = $this->_config['duration']; + $key = $this->_key($key); + + if (!is_int($value)) { + $value = serialize($value); + } + + // setnx() doesn't have an expiry option, so overwrite the key with one + if ($this->_Redis->setnx($key, $value)) { + return $this->_Redis->setex($key, $duration, $value); + } + return false; + } + /** * Returns the `group value` for each of the configured groups * If the group initial value was not found, then it initializes diff --git a/tests/TestCase/Cache/CacheTest.php b/tests/TestCase/Cache/CacheTest.php index c6c83574118..f729556130b 100644 --- a/tests/TestCase/Cache/CacheTest.php +++ b/tests/TestCase/Cache/CacheTest.php @@ -559,4 +559,25 @@ public function testRemember() $result = Cache::remember('test_key', $cacher, 'tests'); $this->assertEquals($expected, $result); } + + /** + * Test add method. + * + * @return void + */ + public function testAdd() + { + $this->_configCache(); + Cache::delete('test_add_key', 'tests'); + + $result = Cache::add('test_add_key', 'test data', 'tests'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'tests'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'tests'); + $this->assertFalse($result); + } } diff --git a/tests/TestCase/Cache/Engine/ApcEngineTest.php b/tests/TestCase/Cache/Engine/ApcEngineTest.php index d576a6cc092..bea9f9d2047 100644 --- a/tests/TestCase/Cache/Engine/ApcEngineTest.php +++ b/tests/TestCase/Cache/Engine/ApcEngineTest.php @@ -286,4 +286,24 @@ public function testGroupClear() $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/tests/TestCase/Cache/Engine/FileEngineTest.php b/tests/TestCase/Cache/Engine/FileEngineTest.php index 6e20d5aee96..ec80993c6a1 100644 --- a/tests/TestCase/Cache/Engine/FileEngineTest.php +++ b/tests/TestCase/Cache/Engine/FileEngineTest.php @@ -621,4 +621,24 @@ public function testGroupClearNoPrefix() $this->assertFalse(Cache::read('key_1', 'file_groups'), 'Did not delete'); $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/tests/TestCase/Cache/Engine/MemcachedEngineTest.php b/tests/TestCase/Cache/Engine/MemcachedEngineTest.php index 3873462631a..0801e221339 100644 --- a/tests/TestCase/Cache/Engine/MemcachedEngineTest.php +++ b/tests/TestCase/Cache/Engine/MemcachedEngineTest.php @@ -916,4 +916,24 @@ public function testGroupClear() $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::delete('test_add_key', 'memcached'); + + $result = Cache::add('test_add_key', 'test data', 'memcached'); + $this->assertTrue($result); + + $expected = 'test data'; + $result = Cache::read('test_add_key', 'memcached'); + $this->assertEquals($expected, $result); + + $result = Cache::add('test_add_key', 'test data 2', 'memcached'); + $this->assertFalse($result); + } } diff --git a/tests/TestCase/Cache/Engine/RedisEngineTest.php b/tests/TestCase/Cache/Engine/RedisEngineTest.php index 4e65cfae584..35c581ea74a 100644 --- a/tests/TestCase/Cache/Engine/RedisEngineTest.php +++ b/tests/TestCase/Cache/Engine/RedisEngineTest.php @@ -410,4 +410,24 @@ public function testGroupClear() $this->assertTrue(Cache::clearGroup('group_b', 'redis_groups')); $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); + } }