con = new \Redis(); $this->connect($data); } /** * Connect to cache * * @param null|array{db:int, host:string, port:int} $data Cache data * * @return void * * @since 1.0.0 */ public function connect(array $data = null) : void { $this->dbdata = isset($data) ? $data : $this->dbdata; if (!isset($this->dbdata['host'], $this->dbdata['port'], $this->dbdata['db'])) { $this->status = CacheStatus::FAILURE; throw new InvalidConnectionConfigException((string) \json_encode($this->dbdata)); } $this->con->connect($this->dbdata['host'], $this->dbdata['port']); try { $this->con->ping(); } catch (\Throwable $e) { $this->status = CacheStatus::FAILURE; return; } $this->con->setOption(\Redis::OPT_SERIALIZER, (string) \Redis::SERIALIZER_NONE); $this->con->setOption(\Redis::OPT_SCAN, (string) \Redis::SCAN_NORETRY); $this->con->select($this->dbdata['db']); $this->status = CacheStatus::OK; } /** * {@inheritdoc} */ public function close() : void { if ($this->con !== null) { $this->con->close(); } parent::close(); } /** * {@inheritdoc} */ public function set(int|string $key, mixed $value, int $expire = -1) : void { if ($this->status !== CacheStatus::OK) { return; } if ($expire > 0) { $this->con->setEx($key, $expire, $this->build($value)); return; } $this->con->set($key, $this->build($value)); } /** * {@inheritdoc} */ public function add(int|string $key, mixed $value, int $expire = -1) : bool { if ($this->status !== CacheStatus::OK) { return false; } if ($expire > 0) { return $this->con->setNx($key, $this->build($value), $expire); } return $this->con->setNx($key, $this->build($value)); } /** * {@inheritdoc} */ public function get(int|string $key, int $expire = -1) : mixed { if ($this->status !== CacheStatus::OK || $this->con->exists($key) < 1) { return null; } $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); } return $result; } /** * {@inheritdoc} */ public function delete(int|string $key, int $expire = -1) : bool { if ($this->status !== CacheStatus::OK) { return false; } return $this->con->del($key) > 0; } /** * {@inheritdoc} */ public function flush(int $expire = 0) : bool { return $this->flushAll(); } /** * {@inheritdoc} */ public function flushAll() : bool { if ($this->status !== CacheStatus::OK) { return false; } $this->con->flushDb(); return true; } /** * {@inheritdoc} */ public function replace(int|string $key, mixed $value, int $expire = -1) : bool { if ($this->status !== CacheStatus::OK) { return false; } if ($this->con->exists($key) > 0) { $this->set($key, $value, $expire); return true; } return false; } /** * {@inheritdoc} */ public function stats() : array { if ($this->status !== CacheStatus::OK) { return []; } $info = $this->con->info(); $stats = []; $stats['status'] = $this->status; $stats['count'] = $this->con->dbSize(); $stats['size'] = $info['used_memory']; return $stats; } /** * {@inheritdoc} */ public function getThreshold() : int { return 0; } /** * Destructor. * * @since 1.0.0 */ public function __destruct() { $this->close(); } /** * Removing all cache elements larger or equal to the expiration date. Call flushAll for removing persistent cache elements (expiration is negative) as well. * * @param mixed $value Data to cache * * @return mixed * * @since 1.0.0 */ private function build(mixed $value) { $type = $this->dataType($value); $raw = $this->cachify($value, $type); return \is_string($raw) ? $type . self::DELIM . $raw : $raw; } /** * Create string representation of data for storage * * @param mixed $value Value of the data * @param int $type Type of the cache data * * @return mixed * * @throws InvalidEnumValue This exception is thrown if an unsupported cache value type is used * * @since 1.0.0 */ private function cachify(mixed $value, int $type) { if ($type === CacheValueType::_INT || $type === CacheValueType::_STRING || $type === CacheValueType::_BOOL) { return (string) $value; } elseif ($type === CacheValueType::_FLOAT) { return \rtrim(\rtrim(\number_format($value, 5, '.', ''), '0'), '.'); } elseif ($type === CacheValueType::_ARRAY) { return (string) \json_encode($value); } elseif ($type === CacheValueType::_SERIALIZABLE) { return \get_class($value) . self::DELIM . $value->serialize(); } elseif ($type === CacheValueType::_JSONSERIALIZABLE) { return \get_class($value) . self::DELIM . ((string) \json_encode($value->jsonSerialize())); } elseif ($type === CacheValueType::_NULL) { return ''; } throw new InvalidEnumValue($type); } /** * Parse cached value * * @param int $type Cached value type * @param mixed $raw Cached value * @param int $start Value start position * * @return mixed * * @since 1.0.0 */ private function reverseValue(int $type, mixed $raw, int $start) { switch ($type) { case CacheValueType::_INT: return (int) \substr($raw, $start + 1); case CacheValueType::_FLOAT: return (float) \substr($raw, $start + 1); case CacheValueType::_BOOL: return (bool) \substr($raw, $start + 1); case CacheValueType::_STRING: return \substr($raw, $start + 1); case CacheValueType::_ARRAY: $array = \substr($raw, $start + 1); return \json_decode($array === false ? '[]' : $array, true); case CacheValueType::_NULL: return null; case CacheValueType::_JSONSERIALIZABLE: $namespaceStart = (int) \strpos($raw, self::DELIM, $start); $namespaceEnd = (int) \strpos($raw, self::DELIM, $namespaceStart + 1); $namespace = \substr($raw, $namespaceStart + 1, $namespaceEnd - $namespaceStart - 1); if ($namespace === false) { return null; } return new $namespace(); case CacheValueType::_SERIALIZABLE: $namespaceStart = (int) \strpos($raw, self::DELIM, $start); $namespaceEnd = (int) \strpos($raw, self::DELIM, $namespaceStart + 1); $namespace = \substr($raw, $namespaceStart + 1, $namespaceEnd - $namespaceStart - 1); if ($namespace === false) { return null; } $obj = new $namespace(); $obj->unserialize(\substr($raw, $namespaceEnd + 1)); return $obj; default: return null; } } }