diff --git a/DataStorage/Cache/Connection/ConnectionInterface.php b/DataStorage/Cache/Connection/ConnectionInterface.php index 18a14e7fe..c402bb3b7 100644 --- a/DataStorage/Cache/Connection/ConnectionInterface.php +++ b/DataStorage/Cache/Connection/ConnectionInterface.php @@ -39,6 +39,43 @@ interface ConnectionInterface extends DataStorageConnectionInterface */ public function set(int|string $key, mixed $value, int $expire = -1) : void; + /** + * Increment value. + * + * @param int|string $key Unique cache key + * @param int $value By value + * + * @return void + * + * @since 1.0.0 + */ + public function increment(int|string $key, int $value = 1) : void; + + /** + * Decrement value. + * + * @param int|string $key Unique cache key + * @param int $value By value + * + * @return void + * + * @since 1.0.0 + */ + public function decrement(int|string $key, int $value = 1) : void; + + /** + * Rename cache key. + * + * @param int|string $old Unique cache key + * @param int|string $new Unique cache key + * @param int $expire Valid duration (in s). Negative expiration means no expiration. + * + * @return void + * + * @since 1.0.0 + */ + public function rename(int|string $old, int|string $new, int $expire = -1) : void; + /** * Adding new data if it doesn't exist. * @@ -64,6 +101,30 @@ interface ConnectionInterface extends DataStorageConnectionInterface */ public function get(int|string $key, int $expire = -1) : mixed; + /** + * Get cache by pattern. + * + * @param string $key Unique cache key + * @param int $expire Valid duration (in s). In case the data needs to be newer than the defined expiration time. If the expiration date is larger than the defined expiration time and supposed to be expired it will not remove the outdated cache. + * + * @return array Cache values + * + * @since 1.0.0 + */ + public function getLike(string $pattern, int $expire = -1) : array; + + /** + * Exists cache by key. + * + * @param int|string $key Unique cache key + * @param int $expire Valid duration (in s). In case the data needs to be newer than the defined expiration time. If the expiration date is larger than the defined expiration time and supposed to be expired it will not remove the outdated cache. + * + * @return bool + * + * @since 1.0.0 + */ + public function exists(int|string $key, int $expire = -1) : bool; + /** * Remove value by key. * @@ -76,6 +137,18 @@ interface ConnectionInterface extends DataStorageConnectionInterface */ public function delete(int|string $key, int $expire = -1) : bool; + /** + * Remove value by pattern. + * + * @param string $key Unique cache key + * @param int $expire Valid duration (in s) + * + * @return bool + * + * @since 1.0.0 + */ + public function deleteLike(string $pattern, int $expire = -1) : bool; + /** * Removing all cache elements larger or equal to the expiration date. Call flushAll for removing persistent cache elements (expiration is negative) as well. * @@ -109,6 +182,18 @@ interface ConnectionInterface extends DataStorageConnectionInterface */ public function replace(int|string $key, mixed $value, int $expire = -1) : bool; + /** + * Updating expire. + * + * @param int|string $key Unique cache key + * @param int $expire Valid duration (in s) + * + * @return bool + * + * @since 1.0.0 + */ + public function updateExpire(int|string $key, int $expire = -1) : bool; + /** * Requesting cache stats. * diff --git a/DataStorage/Cache/Connection/FileCache.php b/DataStorage/Cache/Connection/FileCache.php index ac27e0656..4b5b22582 100644 --- a/DataStorage/Cache/Connection/FileCache.php +++ b/DataStorage/Cache/Connection/FileCache.php @@ -155,7 +155,14 @@ final class FileCache extends ConnectionAbstract $path = Directory::sanitize($key, self::SANITIZE); - File::put($this->con . '/' . \trim($path, '/') . '.cache', $this->build($value, $expire)); + $fp = \fopen($this->con . '/' . \trim($path, '/') . '.cache', 'w+'); + if (\flock($fp, \LOCK_EX)) { + \ftruncate($fp, 0); + \fwrite($fp, $this->build($value, $expire)); + \fflush($fp); + \flock($fp, \LOCK_UN); + } + \fclose($fp); } /** @@ -170,7 +177,14 @@ final class FileCache extends ConnectionAbstract $path = $this->getPath($key); if (!File::exists($path)) { - File::put($path, $this->build($value, $expire)); + $fp = \fopen($path, 'w+'); + if (\flock($fp, \LOCK_EX)) { + \ftruncate($fp, 0); + \fwrite($fp, $this->build($value, $expire)); + \fflush($fp); + \flock($fp, \LOCK_UN); + } + \fclose($fp); return true; } @@ -377,18 +391,254 @@ final class FileCache extends ConnectionAbstract $cacheExpire = $this->getExpire($raw); $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; - if ($cacheExpire >= 0 && $created + $cacheExpire < $now) { - return $this->delete($key); - } + if (($cacheExpire >= 0 && $created + $cacheExpire < $now) + || ($cacheExpire >= 0 && \abs($now - $created) > $expire) + ) { + File::delete($path); - if ($cacheExpire >= 0 && \abs($now - $created) > $expire) { - return $this->delete($key); + return true; } } return false; } + /** + * {@inheritdoc} + */ + public function exists(int|string $key, int $expire = -1) : bool + { + if ($this->status !== CacheStatus::OK) { + return false; + } + + $path = $this->getPath($key); + if (!File::exists($path)) { + return false; + } + + $created = File::created($path)->getTimestamp(); + $now = \time(); + + if ($expire >= 0 && $created + $expire < $now) { + return false; + } + + $raw = \file_get_contents($path); + if ($raw === false) { + return false; + } + + $type = (int) $raw[0]; + $expireStart = (int) \strpos($raw, self::DELIM); + $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); + + if ($expireStart < 0 || $expireEnd < 0) { + return false; + } + + $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); + $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; + + if ($cacheExpire >= 0 && $created + $cacheExpire + ($expire > 0 ? $expire : 0) < $now) { + File::delete($path); + + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function increment(int|string $key, int $value = 1) : void + { + $path = $this->getPath($key); + if (!File::exists($path)) { + return; + } + + $created = File::created($path)->getTimestamp(); + $now = \time(); + + if ($expire >= 0 && $created + $expire < $now) { + return; + } + + $raw = \file_get_contents($path); + if ($raw === false) { + return; + } + + $type = (int) $raw[0]; + $expireStart = (int) \strpos($raw, self::DELIM); + $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); + + if ($expireStart < 0 || $expireEnd < 0) { + return; + } + + $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); + $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; + + $val = $this->reverseValue($type, $raw, $expireEnd); + $this->set($key, $val + $value, $cacheExpire); + } + + /** + * {@inheritdoc} + */ + public function decrement(int|string $key, int $value = 1) : void + { + $path = $this->getPath($key); + if (!File::exists($path)) { + return; + } + + $created = File::created($path)->getTimestamp(); + $now = \time(); + + if ($expire >= 0 && $created + $expire < $now) { + return; + } + + $raw = \file_get_contents($path); + if ($raw === false) { + return; + } + + $type = (int) $raw[0]; + $expireStart = (int) \strpos($raw, self::DELIM); + $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); + + if ($expireStart < 0 || $expireEnd < 0) { + return; + } + + $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); + $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; + + $val = $this->reverseValue($type, $raw, $expireEnd); + $this->set($key, $val - $value, $cacheExpire); + } + + /** + * {@inheritdoc} + */ + public function rename(int|string $old, int|string $new, int $expire = -1) : void + { + $value = $this->get($old); + $this->set($new, $value, $expire); + $this->delete($old); + } + + /** + * {@inheritdoc} + */ + public function getLike(string $pattern, int $expire = -1) : array + { + if ($this->status !== CacheStatus::OK) { + return []; + } + + $files = Directory::list($this->con . '/', $pattern . '\.cache', true); + $values = []; + + foreach ($files as $path) { + $path = $this->con . '/' . $path; + $created = File::created($path)->getTimestamp(); + $now = \time(); + + if ($expire >= 0 && $created + $expire < $now) { + continue; + } + + $raw = \file_get_contents($path); + if ($raw === false) { + continue; + } + + $type = (int) $raw[0]; + $expireStart = (int) \strpos($raw, self::DELIM); + $expireEnd = (int) \strpos($raw, self::DELIM, $expireStart + 1); + + if ($expireStart < 0 || $expireEnd < 0) { + continue; + } + + $cacheExpire = \substr($raw, $expireStart + 1, $expireEnd - ($expireStart + 1)); + $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; + + if ($cacheExpire >= 0 && $created + $cacheExpire + ($expire > 0 ? $expire : 0) < $now) { + File::delete($path); + + continue; + } + + $values[] = $this->reverseValue($type, $raw, $expireEnd); + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function deleteLike(string $pattern, int $expire = -1) : bool + { + if ($this->status !== CacheStatus::OK) { + return false; + } + + $files = Directory::list($this->con . '/', $pattern . '\.cache', true); + + foreach ($files as $path) { + $path = $this->con . '/' . $path; + + if ($expire < 0) { + File::delete($path); + + continue; + } + + if ($expire >= 0) { + $created = File::created($path)->getTimestamp(); + $now = \time(); + $raw = \file_get_contents($path); + + if ($raw === false) { + continue; + } + + $cacheExpire = $this->getExpire($raw); + $cacheExpire = ($cacheExpire === -1) ? $created : (int) $cacheExpire; + + if (($cacheExpire >= 0 && $created + $cacheExpire < $now) + || ($cacheExpire >= 0 && \abs($now - $created) > $expire) + ) { + File::delete($path); + + continue; + } + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function updateExpire(int|string $key, int $expire = -1) : bool + { + $value = $this->get($key); + $this->delete($key); + $this->set($key, $value, $expire); + + return true; + } + /** * {@inheritdoc} */ @@ -427,7 +677,14 @@ final class FileCache extends ConnectionAbstract $path = $this->getPath($key); if (File::exists($path)) { - File::put($path, $this->build($value, $expire)); + $fp = \fopen($path, 'w+'); + if (\flock($fp, \LOCK_EX)) { + \ftruncate($fp, 0); + \fwrite($fp, $this->build($value, $expire)); + \fflush($fp); + \flock($fp, \LOCK_UN); + } + \fclose($fp); return true; } diff --git a/DataStorage/Cache/Connection/MemCached.php b/DataStorage/Cache/Connection/MemCached.php index 56c561719..7d3a54e33 100644 --- a/DataStorage/Cache/Connection/MemCached.php +++ b/DataStorage/Cache/Connection/MemCached.php @@ -139,6 +139,103 @@ final class MemCached extends ConnectionAbstract return $this->con->delete($key); } + /** + * {@inheritdoc} + */ + public function exists(int|string $key, int $expire = -1) : bool + { + if ($this->status !== CacheStatus::OK) { + return false; + } + + return $this->con->get($key) !== false; + } + + /** + * {@inheritdoc} + */ + public function increment(int|string $key, int $value = 1) : void + { + $this->con->increment($key, $value); + } + + /** + * {@inheritdoc} + */ + public function decrement(int|string $key, int $value = 1) : void + { + $this->con->decrement($key, $value); + } + + /** + * {@inheritdoc} + */ + public function rename(int|string $old, int|string $new, int $expire = -1) : void + { + $value = $this->get($old); + $this->set($new, $value, $expire); + $this->delete($old); + } + + /** + * {@inheritdoc} + */ + public function getLike(string $pattern, int $expire = -1) : array + { + if ($this->status !== CacheStatus::OK) { + return []; + } + + $keys = $this->con->getAllKeys(); + $values = []; + + foreach ($keys as $key) { + if (\preg_match($key, $key) === 1) { + $result = $this->con->get($key); + if (\is_string($result)) { + $type = (int) $result[0]; + $start = (int) \strpos($result, self::DELIM); + $result = $this->reverseValue($type, $result, $start); + } + + $values[] = $result; + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function deleteLike(string $pattern, int $expire = -1) : bool + { + if ($this->status !== CacheStatus::OK) { + return false; + } + + $keys = $this->con->getAllKeys(); + foreach ($keys as $key) { + if (\preg_match($key, $key) === 1) { + $this->con->delete($key); + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function updateExpire(int|string $key, int $expire = -1) : bool + { + if ($expire > 0) { + $this->con->touch($key, $expire); + } + + return true; + } + /** * {@inheritdoc} */ diff --git a/DataStorage/Cache/Connection/NullCache.php b/DataStorage/Cache/Connection/NullCache.php index ef2df277c..24ddd1e6a 100644 --- a/DataStorage/Cache/Connection/NullCache.php +++ b/DataStorage/Cache/Connection/NullCache.php @@ -101,4 +101,43 @@ final class NullCache extends ConnectionAbstract { return 0; } + + /** + * {@inheritdoc} + */ + public function exists(int|string $key, int $expire = -1) : bool + { + return true; + } + + /** + * {@inheritdoc} + */ + public function rename(int|string $old, int|string $new, int $expire = -1) : void + { + } + + /** + * {@inheritdoc} + */ + public function getLike(string $pattern, int $expire = -1) : array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function deleteLike(string $pattern, int $expire = -1) : bool + { + return true; + } + + /** + * {@inheritdoc} + */ + public function updateExpire(int|string $key, int $expire = -1) : bool + { + return true; + } } diff --git a/DataStorage/Cache/Connection/RedisCache.php b/DataStorage/Cache/Connection/RedisCache.php index 4d08980c9..fe0a35be3 100644 --- a/DataStorage/Cache/Connection/RedisCache.php +++ b/DataStorage/Cache/Connection/RedisCache.php @@ -167,6 +167,105 @@ final class RedisCache extends ConnectionAbstract return $this->con->del($key) > 0; } + /** + * {@inheritdoc} + */ + public function exists(int|string $key, int $expire = -1) : bool + { + if ($this->status !== CacheStatus::OK) { + return false; + } + + return $this->con->exists($key); + } + + /** + * {@inheritdoc} + */ + public function increment(int|string $key, int $value = 1) : void + { + $this->con->incrBy($key, $value); + } + + /** + * {@inheritdoc} + */ + public function decrement(int|string $key, int $value = 1) : void + { + $this->con->decrBy($key, $value); + } + + /** + * {@inheritdoc} + */ + public function rename(int|string $old, int|string $new, int $expire = -1) : void + { + $this->con->rename($old, $new); + + if ($expire > 0) { + $this->con->expire($new, $expire); + } + } + + /** + * {@inheritdoc} + */ + public function getLike(string $pattern, int $expire = -1) : array + { + if ($this->status !== CacheStatus::OK) { + return []; + } + + $keys = $this->con->keys('*'); + $values = []; + + foreach ($keys as $key) { + if (\preg_match($key, $key) === 1) { + $result = $this->con->get($key); + if (\is_string($result)) { + $type = (int) $result[0]; + $start = (int) \strpos($result, self::DELIM); + $result = $this->reverseValue($type, $result, $start); + } + + $values[] = $result; + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function deleteLike(string $pattern, int $expire = -1) : bool + { + if ($this->status !== CacheStatus::OK) { + return false; + } + + $keys = $this->con->keys('*'); + foreach ($keys as $key) { + if (\preg_match($key, $key) === 1) { + $this->con->del($key); + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function updateExpire(int|string $key, int $expire = -1) : bool + { + if ($expire > 0) { + $this->con->expire($key, $expire); + } + + return true; + } + /** * {@inheritdoc} */