<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Redis Cache Store - Main library
*
* @package cachestore_redis
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Redis Cache Store
*
* To allow separation of definitions in Moodle and faster purging, each cache
* is implemented as a Redis hash. That is a trade-off between having functionality of TTL
* and being able to manage many caches in a single redis instance. Given the recommendation
* not to use TTL if at all possible and the benefits of having many stores in Redis using the
* hash configuration, the hash implementation has been used.
*
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_redis extends cache_store implements cache_is_key_aware, cache_is_lockable,
cache_is_configurable, cache_is_searchable {
/**
* Compressor: none.
*/
const COMPRESSOR_NONE = 0;
/**
* Compressor: PHP GZip.
*/
const COMPRESSOR_PHP_GZIP = 1;
/**
* Compressor: PHP Zstandard.
*/
const COMPRESSOR_PHP_ZSTD = 2;
/**
* @var string Suffix used on key name (for hash) to store the TTL sorted list
*/
const TTL_SUFFIX = '_ttl';
/**
* @var int Number of items to delete from cache in one batch when expiring old TTL data.
*/
const TTL_EXPIRE_BATCH = 10000;
/**
* Name of this store.
*
* @var string
*/
protected $name;
/**
* The definition hash, used for hash key
*
* @var string
*/
protected $hash;
/**
* Flag for readiness!
*
* @var boolean
*/
protected $isready = false;
/**
* Cache definition for this store.
*
* @var cache_definition
*/
protected $definition = null;
/**
* Connection to Redis for this store.
*
* @var Redis
*/
protected $redis;
/**
* Serializer for this store.
*
* @var int
*/
protected $serializer = Redis::SERIALIZER_PHP;
/**
* Compressor for this store.
*
* @var int
*/
protected $compressor = self::COMPRESSOR_NONE;
/**
* Bytes read or written by last call to set()/get() or set_many()/get_many().
*
* @var int
*/
protected $lastiobytes = 0;
/** @var int Maximum number of seconds to wait for a lock before giving up. */
protected $lockwait = 60;
/** @var int Timeout before lock is automatically released (in case of crashes) */
protected $locktimeout = 600;
/** @var ?array Array of current locks, or null if we haven't registered shutdown function */
protected $currentlocks = null;
/**
* Determines if the requirements for this type of store are met.
*
* @return bool
*/
public static function are_requirements_met() {
return class_exists('Redis');
}
/**
* Determines if this type of store supports a given mode.
*
* @param int $mode
* @return bool
*/
public static function is_supported_mode($mode) {
return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
}
/**
* Get the features of this type of cache store.
*
* @param array $configuration
* @return int
*/
public static function get_supported_features(array $configuration = array()) {
// Although this plugin now supports TTL I did not add SUPPORTS_NATIVE_TTL here, because
// doing so would cause Moodle to stop adding a 'TTL wrapper' to data items which enforces
// the precise specified TTL. Unless the scheduled task is set to run rather frequently,
// this could cause change in behaviour. Maybe later this should be reconsidered...
return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE;
}
/**
* Get the supported modes of this type of cache store.
*
* @param array $configuration
* @return int
*/
public static function get_supported_modes(array $configuration = array()) {
return self::MODE_APPLICATION + self::MODE_SESSION;
}
/**
* Constructs an instance of this type of store.
*
* @param string $name
* @param array $configuration
*/
public function __construct($name, array $configuration = array()) {
$this->name = $name;
if (!array_key_exists('server', $configuration) || empty($configuration['server'])) {
return;
}
if (array_key_exists('serializer', $configuration)) {
$this->serializer = (int)$configuration['serializer'];
}
if (array_key_exists('compressor', $configuration)) {
$this->compressor = (int)$configuration['compressor'];
}
< $password = !empty($configuration['password']) ? $configuration['password'] : '';
< $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
if (array_key_exists('lockwait', $configuration)) {
$this->lockwait = (int)$configuration['lockwait'];
}
if (array_key_exists('locktimeout', $configuration)) {
$this->locktimeout = (int)$configuration['locktimeout'];
}
< $this->redis = $this->new_redis($configuration['server'], $prefix, $password);
> $this->redis = $this->new_redis($configuration);
}
/**
* Create a new Redis instance and
* connect to the server.
*
< * @param string $server The server connection string
< * @param string $prefix The key prefix
< * @param string $password The server connection password
> * @param array $configuration The server configuration
* @return Redis
*/
< protected function new_redis($server, $prefix = '', $password = '') {
> protected function new_redis(array $configuration): \Redis {
> global $CFG;
>
$redis = new Redis();
< // Check for Unix socket.
>
> $server = $configuration['server'];
> $encrypt = (bool) ($configuration['encryption'] ?? false);
> $password = !empty($configuration['password']) ? $configuration['password'] : '';
> $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
> // Check if it isn't a Unix socket to set default port.
> $port = null;
> $opts = [];
if ($server[0] === '/') {
$port = 0;
} else {
$port = 6379; // No Unix socket so set default port.
if (strpos($server, ':')) { // Check for custom port.
< $serverconf = explode(':', $server);
< $server = $serverconf[0];
< $port = $serverconf[1];
> list($server, $port) = explode(':', $server);
> }
>
> // We can encrypt if we aren't unix socket.
> if ($encrypt) {
> $server = 'tls://' . $server;
> if (empty($configuration['cafile'])) {
> $sslopts = [
> 'verify_peer' => false,
> 'verify_peer_name' => false,
> ];
> } else {
> $sslopts = ['cafile' => $configuration['cafile']];
> }
> $opts['stream'] = $sslopts;
}
}
try {
< if ($redis->connect($server, $port)) {
> if ($redis->connect($server, $port, 1, null, 100, 1, $opts)) {
>
if (!empty($password)) {
$redis->auth($password);
}
// If using compressor, serialisation will be done at cachestore level, not php-redis.
if ($this->compressor == self::COMPRESSOR_NONE) {
$redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
}
if (!empty($prefix)) {
$redis->setOption(Redis::OPT_PREFIX, $prefix);
}
> if ($encrypt && !$redis->ping()) {
$this->isready = true;
> /*
} else {
> * In case of a TLS connection, if phpredis client does not
$this->isready = false;
> * communicate immediately with the server the connection hangs.
}
> * See https://github.com/phpredis/phpredis/issues/2332 .
} catch (\RedisException $e) {
> */
$this->isready = false;
> throw new \RedisException("Ping failed");
}
> }
> debugging("redis $server: $e", DEBUG_NORMAL);
return $redis;
}
/**
* See if we can ping Redis server
*
* @param Redis $redis
* @return bool
*/
protected function ping(Redis $redis) {
try {
if ($redis->ping() === false) {
return false;
}
} catch (Exception $e) {
return false;
}
return true;
}
/**
* Get the name of the store.
*
* @return string
*/
public function my_name() {
return $this->name;
}
/**
* Initialize the store.
*
* @param cache_definition $definition
* @return bool
*/
public function initialise(cache_definition $definition) {
$this->definition = $definition;
$this->hash = $definition->generate_definition_hash();
return true;
}
/**
* Determine if the store is initialized.
*
* @return bool
*/
public function is_initialised() {
return ($this->definition !== null);
}
/**
* Determine if the store is ready for use.
*
* @return bool
*/
public function is_ready() {
return $this->isready;
}
/**
* Get the value associated with a given key.
*
* @param string $key The key to get the value of.
* @return mixed The value of the key, or false if there is no value associated with the key.
*/
public function get($key) {
$value = $this->redis->hGet($this->hash, $key);
if ($this->compressor == self::COMPRESSOR_NONE) {
return $value;
}
// When using compression, values are always strings, so strlen will work.
$this->lastiobytes = strlen($value);
return $this->uncompress($value);
}
/**
* Get the values associated with a list of keys.
*
* @param array $keys The keys to get the values of.
* @return array An array of the values of the given keys.
*/
public function get_many($keys) {
$values = $this->redis->hMGet($this->hash, $keys);
if ($this->compressor == self::COMPRESSOR_NONE) {
return $values;
}
$this->lastiobytes = 0;
foreach ($values as &$value) {
$this->lastiobytes += strlen($value);
$value = $this->uncompress($value);
}
return $values;
}
/**
* Gets the number of bytes read from or written to cache as a result of the last action.
*
* If compression is not enabled, this function always returns IO_BYTES_NOT_SUPPORTED. The reason is that
* when compression is not enabled, data sent to the cache is not serialized, and we would
* need to serialize it to compute the size, which would have a significant performance cost.
*
* @return int Bytes read or written
* @since Moodle 4.0
*/
public function get_last_io_bytes(): int {
if ($this->compressor != self::COMPRESSOR_NONE) {
return $this->lastiobytes;
} else {
// Not supported unless compression is on.
return parent::get_last_io_bytes();
}
}
/**
* Set the value of a key.
*
* @param string $key The key to set the value of.
* @param mixed $value The value.
* @return bool True if the operation succeeded, false otherwise.
*/
public function set($key, $value) {
if ($this->compressor != self::COMPRESSOR_NONE) {
$value = $this->compress($value);
$this->lastiobytes = strlen($value);
}
if ($this->redis->hSet($this->hash, $key, $value) === false) {
return false;
}
if ($this->definition->get_ttl()) {
// When TTL is enabled, we also store the key name in a list sorted by the current time.
$this->redis->zAdd($this->hash . self::TTL_SUFFIX, [], self::get_time(), $key);
// The return value to the zAdd function never indicates whether the operation succeeded
// (it returns zero when there was no error if the item is already in the list) so we
// ignore it.
}
return true;
}
/**
* Set the values of many keys.
*
* @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array
* with two keys, 'key' and 'value'.
* @return int The number of key/value pairs successfuly set.
*/
public function set_many(array $keyvaluearray) {
$pairs = [];
$usettl = false;
if ($this->definition->get_ttl()) {
$usettl = true;
$ttlparams = [];
$now = self::get_time();
}
$this->lastiobytes = 0;
foreach ($keyvaluearray as $pair) {
$key = $pair['key'];
if ($this->compressor != self::COMPRESSOR_NONE) {
$pairs[$key] = $this->compress($pair['value']);
$this->lastiobytes += strlen($pairs[$key]);
} else {
$pairs[$key] = $pair['value'];
}
if ($usettl) {
// When TTL is enabled, we also store the key names in a list sorted by the current
// time.
$ttlparams[] = $now;
$ttlparams[] = $key;
}
}
if ($usettl) {
// Store all the key values with current time.
$this->redis->zAdd($this->hash . self::TTL_SUFFIX, [], ...$ttlparams);
// The return value to the zAdd function never indicates whether the operation succeeded
// (it returns zero when there was no error if the item is already in the list) so we
// ignore it.
}
if ($this->redis->hMSet($this->hash, $pairs)) {
return count($pairs);
}
return 0;
}
/**
* Delete the given key.
*
* @param string $key The key to delete.
* @return bool True if the delete operation succeeds, false otherwise.
*/
public function delete($key) {
$ok = true;
if (!$this->redis->hDel($this->hash, $key)) {
$ok = false;
}
if ($this->definition->get_ttl()) {
// When TTL is enabled, also remove the key from the TTL list.
$this->redis->zRem($this->hash . self::TTL_SUFFIX, $key);
}
return $ok;
}
/**
* Delete many keys.
*
* @param array $keys The keys to delete.
* @return int The number of keys successfully deleted.
*/
public function delete_many(array $keys) {
// If there are no keys to delete, do nothing.
if (!$keys) {
return 0;
}
$count = $this->redis->hDel($this->hash, ...$keys);
if ($this->definition->get_ttl()) {
// When TTL is enabled, also remove the keys from the TTL list.
$this->redis->zRem($this->hash . self::TTL_SUFFIX, ...$keys);
}
return $count;
}
/**
* Purges all keys from the store.
*
* @return bool
*/
public function purge() {
if ($this->definition->get_ttl()) {
// Purge the TTL list as well.
$this->redis->del($this->hash . self::TTL_SUFFIX);
// According to documentation, there is no error return for the 'del' command (it
// only returns the number of keys deleted, which could be 0 or 1 in this case) so we
// do not need to check the return value.
}
return ($this->redis->del($this->hash) !== false);
}
/**
* Cleans up after an instance of the store.
*/
public function instance_deleted() {
$this->redis->close();
unset($this->redis);
}
/**
* Determines if the store has a given key.
*
* @see cache_is_key_aware
* @param string $key The key to check for.
* @return bool True if the key exists, false if it does not.
*/
public function has($key) {
return !empty($this->redis->hExists($this->hash, $key));
}
/**
* Determines if the store has any of the keys in a list.
*
* @see cache_is_key_aware
* @param array $keys The keys to check for.
* @return bool True if any of the keys are found, false none of the keys are found.
*/
public function has_any(array $keys) {
foreach ($keys as $key) {
if ($this->has($key)) {
return true;
}
}
return false;
}
/**
* Determines if the store has all of the keys in a list.
*
* @see cache_is_key_aware
* @param array $keys The keys to check for.
* @return bool True if all of the keys are found, false otherwise.
*/
public function has_all(array $keys) {
foreach ($keys as $key) {
if (!$this->has($key)) {
return false;
}
}
return true;
}
/**
* Tries to acquire a lock with a given name.
*
* @see cache_is_lockable
* @param string $key Name of the lock to acquire.
* @param string $ownerid Information to identify owner of lock if acquired.
* @return bool True if the lock was acquired, false if it was not.
*/
public function acquire_lock($key, $ownerid) {
$timelimit = time() + $this->lockwait;
do {
// If the key doesn't already exist, grab it and return true.
if ($this->redis->setnx($key, $ownerid)) {
// Ensure Redis deletes the key after a bit in case something goes wrong.
$this->redis->expire($key, $this->locktimeout);
// If we haven't got it already, better register a shutdown function.
if ($this->currentlocks === null) {
core_shutdown_manager::register_function([$this, 'shutdown_release_locks']);
$this->currentlocks = [];
}
$this->currentlocks[$key] = $ownerid;
return true;
}
// Wait 1 second then retry.
sleep(1);
} while (time() < $timelimit);
return false;
}
/**
* Releases any locks when the system shuts down, in case there is a crash or somebody forgets
* to use 'try-finally'.
*
* Do not call this function manually (except from unit test).
*/
public function shutdown_release_locks() {
foreach ($this->currentlocks as $key => $ownerid) {
debugging('Automatically releasing Redis cache lock: ' . $key . ' (' . $ownerid .
') - did somebody forget to call release_lock()?', DEBUG_DEVELOPER);
$this->release_lock($key, $ownerid);
}
}
/**
* Checks a lock with a given name and owner information.
*
* @see cache_is_lockable
* @param string $key Name of the lock to check.
* @param string $ownerid Owner information to check existing lock against.
* @return mixed True if the lock exists and the owner information matches, null if the lock does not
* exist, and false otherwise.
*/
public function check_lock_state($key, $ownerid) {
$result = $this->redis->get($key);
if ($result === (string)$ownerid) {
return true;
}
if ($result === false) {
return null;
}
return false;
}
/**
* Finds all of the keys being used by this cache store instance.
*
* @return array of all keys in the hash as a numbered array.
*/
public function find_all() {
return $this->redis->hKeys($this->hash);
}
/**
* Finds all of the keys whose keys start with the given prefix.
*
* @param string $prefix
*
* @return array List of keys that match this prefix.
*/
public function find_by_prefix($prefix) {
$return = [];
foreach ($this->find_all() as $key) {
if (strpos($key, $prefix) === 0) {
$return[] = $key;
}
}
return $return;
}
/**
* Releases a given lock if the owner information matches.
*
* @see cache_is_lockable
* @param string $key Name of the lock to release.
* @param string $ownerid Owner information to use.
* @return bool True if the lock is released, false if it is not.
*/
public function release_lock($key, $ownerid) {
if ($this->check_lock_state($key, $ownerid)) {
unset($this->currentlocks[$key]);
return ($this->redis->del($key) !== false);
}
return false;
}
/**
* Runs TTL expiry process for this cache.
*
* This is not part of the standard cache API and is intended for use by the scheduled task
* \cachestore_redis\ttl.
*
* @return array Various keys with information about how the expiry went
*/
public function expire_ttl(): array {
$ttl = $this->definition->get_ttl();
if (!$ttl) {
throw new \coding_exception('Cache definition ' . $this->definition->get_id() . ' does not use TTL');
}
$limit = self::get_time() - $ttl;
$count = 0;
$batches = 0;
$timebefore = microtime(true);
$memorybefore = $this->store_total_size();
do {
$keys = $this->redis->zRangeByScore($this->hash . self::TTL_SUFFIX, 0, $limit,
['limit' => [0, self::TTL_EXPIRE_BATCH]]);
$this->delete_many($keys);
$count += count($keys);
$batches++;
} while (count($keys) === self::TTL_EXPIRE_BATCH);
$memoryafter = $this->store_total_size();
$timeafter = microtime(true);
$result = ['keys' => $count, 'batches' => $batches, 'time' => $timeafter - $timebefore];
if ($memorybefore !== null) {
$result['memory'] = $memorybefore - $memoryafter;
}
return $result;
}
/**
* Gets the current time for TTL functionality. This wrapper makes it easier to unit-test
* the TTL behaviour.
*
* @return int Current time
*/
protected static function get_time(): int {
global $CFG;
if (PHPUNIT_TEST && !empty($CFG->phpunit_cachestore_redis_time)) {
return $CFG->phpunit_cachestore_redis_time;
}
return time();
}
/**
* Sets the current time (within unit test) for TTL functionality.
*
* This setting is stored in $CFG so will be automatically reset if you use resetAfterTest.
*
* @param int $time Current time (set 0 to start using real time).
*/
public static function set_phpunit_time(int $time = 0): void {
global $CFG;
if (!PHPUNIT_TEST) {
throw new \coding_exception('Function only available during unit test');
}
if ($time) {
$CFG->phpunit_cachestore_redis_time = $time;
} else {
unset($CFG->phpunit_cachestore_redis_time);
}
}
/**
* Estimates the stored size, taking into account whether compression is turned on.
*
* @param mixed $key Key name
* @param mixed $value Value
* @return int Approximate stored size
*/
public function estimate_stored_size($key, $value): int {
if ($this->compressor == self::COMPRESSOR_NONE) {
// If uncompressed, use default estimate.
return parent::estimate_stored_size($key, $value);
} else {
// If compressed, compress value.
return strlen($this->serialize($key)) + strlen($this->compress($value));
}
}
/**
* Gets Redis reported memory usage.
*
* @return int|null Memory used by Redis or null if we don't know
*/
public function store_total_size(): ?int {
try {
$details = $this->redis->info('MEMORY');
} catch (\RedisException $e) {
return null;
}
if (empty($details['used_memory'])) {
return null;
} else {
return (int)$details['used_memory'];
}
}
/**
* Creates a configuration array from given 'add instance' form data.
*
* @see cache_is_configurable
* @param stdClass $data
* @return array
*/
public static function config_get_configuration_array($data) {
return array(
'server' => $data->server,
'prefix' => $data->prefix,
'password' => $data->password,
'serializer' => $data->serializer,
'compressor' => $data->compressor,
> 'encryption' => $data->encryption,
);
> 'cafile' => $data->cafile,
}
/**
* Sets form data from a configuration array.
*
* @see cache_is_configurable
* @param moodleform $editform
* @param array $config
*/
public static function config_set_edit_form_data(moodleform $editform, array $config) {
$data = array();
$data['server'] = $config['server'];
$data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : '';
$data['password'] = !empty($config['password']) ? $config['password'] : '';
if (!empty($config['serializer'])) {
$data['serializer'] = $config['serializer'];
}
if (!empty($config['compressor'])) {
$data['compressor'] = $config['compressor'];
}
> if (!empty($config['encryption'])) {
$editform->set_data($data);
> $data['encryption'] = $config['encryption'];
}
> }
> if (!empty($config['cafile'])) {
> $data['cafile'] = $config['cafile'];
/**
> }
* Creates an instance of the store for testing.
*
* @param cache_definition $definition
* @return mixed An instance of the store, or false if an instance cannot be created.
*/
public static function initialise_test_instance(cache_definition $definition) {
if (!self::are_requirements_met()) {
return false;
}
$config = get_config('cachestore_redis');
if (empty($config->test_server)) {
return false;
}
$configuration = array('server' => $config->test_server);
if (!empty($config->test_serializer)) {
$configuration['serializer'] = $config->test_serializer;
}
if (!empty($config->test_password)) {
$configuration['password'] = $config->test_password;
}
> if (!empty($config->test_encryption)) {
// Make it possible to test TTL performance by hacking a copy of the cache definition.
> $configuration['encryption'] = $config->test_encryption;
if (!empty($config->test_ttl)) {
> }
$definition = clone $definition;
> if (!empty($config->test_cafile)) {
$property = (new ReflectionClass($definition))->getProperty('ttl');
> $configuration['cafile'] = $config->test_cafile;
$property->setAccessible(true);
> }
$property->setValue($definition, 999);
}
$cache = new cachestore_redis('Redis test', $configuration);
$cache->initialise($definition);
return $cache;
}
/**
* Return configuration to use when unit testing.
*
* @return array
*/
public static function unit_test_configuration() {
global $DB;
if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) {
throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration');
}
return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
'prefix' => $DB->get_prefix(),
> 'encryption' => defined('TEST_CACHESTORE_REDIS_ENCRYPT') && TEST_CACHESTORE_REDIS_ENCRYPT,
];
}
/**
* Returns true if this cache store instance is both suitable for testing, and ready for testing.
*
* When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing.
*
* @return bool
*/
public static function ready_to_be_used_for_testing() {
return defined('TEST_CACHESTORE_REDIS_TESTSERVERS');
}
/**
* Gets an array of options to use as the serialiser.
* @return array
*/
public static function config_get_serializer_options() {
$options = array(
Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis')
);
if (defined('Redis::SERIALIZER_IGBINARY')) {
$options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis');
}
return $options;
}
/**
* Gets an array of options to use as the compressor.
*
* @return array
*/
public static function config_get_compressor_options() {
$arr = [
self::COMPRESSOR_NONE => get_string('compressor_none', 'cachestore_redis'),
self::COMPRESSOR_PHP_GZIP => get_string('compressor_php_gzip', 'cachestore_redis'),
];
// Check if the Zstandard PHP extension is installed.
if (extension_loaded('zstd')) {
$arr[self::COMPRESSOR_PHP_ZSTD] = get_string('compressor_php_zstd', 'cachestore_redis');
}
return $arr;
}
/**
* Compress the given value, serializing it first.
*
* @param mixed $value
* @return string
*/
private function compress($value) {
$value = $this->serialize($value);
switch ($this->compressor) {
case self::COMPRESSOR_NONE:
return $value;
case self::COMPRESSOR_PHP_GZIP:
return gzencode($value);
case self::COMPRESSOR_PHP_ZSTD:
return zstd_compress($value);
default:
debugging("Invalid compressor: {$this->compressor}");
return $value;
}
}
/**
* Uncompresses (deflates) the data, unserialising it afterwards.
*
* @param string $value
* @return mixed
*/
private function uncompress($value) {
if ($value === false) {
return false;
}
switch ($this->compressor) {
case self::COMPRESSOR_NONE:
break;
case self::COMPRESSOR_PHP_GZIP:
$value = gzdecode($value);
break;
case self::COMPRESSOR_PHP_ZSTD:
$value = zstd_uncompress($value);
break;
default:
debugging("Invalid compressor: {$this->compressor}");
}
return $this->unserialize($value);
}
/**
* Serializes the data according to the configured serializer.
*
* @param mixed $value
* @return string
*/
private function serialize($value) {
switch ($this->serializer) {
case Redis::SERIALIZER_NONE:
return $value;
case Redis::SERIALIZER_PHP:
return serialize($value);
case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY:
return igbinary_serialize($value);
default:
debugging("Invalid serializer: {$this->serializer}");
return $value;
}
}
/**
* Unserializes the data according to the configured serializer
*
* @param string $value
* @return mixed
*/
private function unserialize($value) {
switch ($this->serializer) {
case Redis::SERIALIZER_NONE:
return $value;
case Redis::SERIALIZER_PHP:
return unserialize($value);
case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY:
return igbinary_unserialize($value);
default:
debugging("Invalid serializer: {$this->serializer}");
return $value;
}
}
}