* @author Camille Moyon * @license GPLv2 * @package generis * */ class common_persistence_KeyValuePersistence extends common_persistence_Persistence { /** * Ability to set the key only if it does not already exist */ const FEATURE_NX = 'nx'; const MAX_VALUE_SIZE = 'max_value_size'; const MAP_IDENTIFIER = 'map_identifier'; const START_MAP_DELIMITER = 'start_map_delimiter'; const END_MAP_DELIMITER = 'end_map_delimiter'; const MAPPED_KEY_SEPARATOR = '###'; const LEVEL_SEPARATOR = '-'; const DEFAULT_MAP_IDENTIFIER = '<<<>>>'; const DEFAULT_START_MAP_DELIMITER = '<<<>>>'; const DEFAULT_END_MAP_DELIMITER = '<<<>>>'; /** * @var int The maximum size allowed for the value */ protected $size = false; /** * Set a $key with a $value * If $value is too large, it is split into multiple $mappedKey. * These new keys are serialized and stored into actual $key * * @param string $key * @param string $value * @param string $ttl * @param bool $nx * @return bool * @throws common_Exception If size is misconfigured */ public function set($key, $value, $ttl = null, $nx = false) { if ($this->hasMaxSize()) { if ($this->isLarge($value)) { $value = $this->setLargeValue($key, $value, 0, true, true, $ttl, $nx); } } return $this->getDriver()->set($key, $value, $ttl, $nx); } /** * Get $key from driver. If $key is split, all mapped values are retrieved and join to restore original value * * @param string $key * @return bool|int|null|string */ public function get($key) { $value = $this->getDriver()->get($key); if ($this->hasMaxSize()) { if ($this->isSplit($value)) { $value = $this->join($key, $value); } } return $value; } /** * Check if a key exists * Return false if $key is a mappedKey * * @param $key * @return bool */ public function exists($key) { if ($this->isMappedKey($key)) { return false; } else { return $this->getDriver()->exists($key); } } /** * Delete a key. If key is split, all associated mapped key are deleted too * * @param $key * @return bool */ public function del($key) { if ($this->isMappedKey($key)) { return false; } else { $success = true; if ($this->hasMaxSize()) { $success = $this->deleteMappedKey($key); } return $success && $this->getDriver()->del($key); } } /** * Increment $key, only for numeric * Mapped key will be ignored * * @param $key * @return bool|int */ public function incr($key) { if ($this->isMappedKey($key)) { return false; } return $this->getDriver()->incr($key); } /** * Decrement $key, only for numeric * Mapped key will be ignored * * @param $key * @return bool|int */ public function decr($key) { if ($this->isMappedKey($key)) { return false; } return $this->getDriver()->decr($key); } /** * Delete a key and if the value is a map, delete all mapped key recursively * * @param $key * @param null $value * @param int $level * @return bool */ protected function deleteMappedKey($key, $value = null, $level = 0) { if (is_null($value)) { $value = $this->getDriver()->get($key); } if ($level > 0) { $key = $key . self::LEVEL_SEPARATOR . $level; } $success = true; if ($this->isSplit($value)) { $valueParts = []; foreach ($this->unSerializeMap($value) as $mappedKey) { $mappedKey = $this->transformReferenceToMappedKey($mappedKey); $valueParts[$this->getMappedKeyIndex($mappedKey, $key)] = $this->getDriver()->get($mappedKey); $success = $success && $this->getDriver()->del($mappedKey); } uksort($valueParts, 'strnatcmp'); $value = implode('', $valueParts); if ($this->isSplit($value)) { $success = $success && $this->deleteMappedKey($key, $value, $level + 1); } } return $success; } /** * Purge the Driver if it implements common_persistence_Purgable * Otherwise throws common_exception_NotImplemented * * @return mixed * @throws common_exception_NotImplemented */ public function purge() { if ($this->getDriver() instanceof common_persistence_Purgable) { return $this->getDriver()->purge(); } else { throw new common_exception_NotImplemented("purge not implemented "); } } /** * Set a large value recursively. * Create a map of value (split by size range) and store the serialize map as current value * * @param $key * @param $value * @param int $level * @param bool $flush * @param bool $toTransform * @param null $ttl * @param bool $nx * @return mixed * @throws common_Exception */ protected function setLargeValue($key, $value, $level = 0, $flush = true, $toTransform = true, $ttl = null, $nx = false) { if (!$this->isLarge($value)) { if ($flush) { $this->set($key, $value, $ttl, $nx); } return $value; } if ($nx) { throw new common_exception_NotImplemented("NX not implemented for large values"); } if ($level > 0) { $key = $key . self::LEVEL_SEPARATOR . $level; } $map = $this->createMap($key, $value); foreach ($map as $mappedKey => $valuePart) { if ($toTransform) { $transformedKey = $this->transformReferenceToMappedKey($mappedKey); } else { $transformedKey = $mappedKey; } if (!is_null($ttl)) { $this->set($transformedKey, $valuePart, $ttl); } else { $this->set($transformedKey, $valuePart); } } return $this->setLargeValue($key, $this->serializeMap($map), $level + 1, $flush, $toTransform, $ttl); } /** * Check if the given $value is larger than $this max size * * @param $value * @return bool * @throws common_Exception If size is misconfigured */ protected function isLarge($value) { $size = $this->getSize(); if (!$size) { return false; } return strlen($value) > $size; } /** * Cut a string into an array with $size option * * @param $value * @return array * @throws common_Exception If size is misconfigured */ protected function split($value) { return str_split($value, $this->getSize()); } /** * Join different values referenced into a map recursively * * @param $key * @param $value * @param int $level * @return string */ protected function join($key, $value, $level = 0) { if ($level > 0) { $key = $key . self::LEVEL_SEPARATOR . $level; } $valueParts = []; foreach ($this->unSerializeMap($value) as $mappedKey) { $mappedKey = $this->transformReferenceToMappedKey($mappedKey); $valueParts[$this->getMappedKeyIndex($mappedKey, $key)] = $this->getDriver()->get($mappedKey); } uksort($valueParts, 'strnatcmp'); $value = implode('', $valueParts); if ($this->isSplit($value)) { $value = $this->join($key, $value, $level + 1); } return $value; } /** * Split a large value to an array with value size lesser than required max size * Construct the array with index of value * * @param $key * @param $value * @return array * @throws common_Exception If size is misconfigured */ protected function createMap($key, $value) { $splitValue = $this->split($value); $map = []; foreach ($splitValue as $index => $part) { $map[$key . self::MAPPED_KEY_SEPARATOR . $index] = $part; } return $map; } /** * Transform a map reference to an identifiable $key * * @param $key * @return string */ protected function transformReferenceToMappedKey($key) { return $this->getStartMapDelimiter() . $key . $this->getEndMapDelimiter(); } /** * Check if current $key is part of a map * * @param $key * @return bool */ protected function isMappedKey($key) { return substr($key, 0, strlen($this->getStartMapDelimiter())) == $this->getStartMapDelimiter() && substr($key, -strlen($this->getEndMapDelimiter())) == $this->getEndMapDelimiter(); } /** * Get the mapped key index of a mappedKey * * @param $mappedKey * @param $key * @return bool|string */ protected function getMappedKeyIndex($mappedKey, $key) { $startSize = strlen($this->getStartMapDelimiter()) - 1; $key = substr($key, $startSize, strrpos($key, $this->getEndMapDelimiter()) - $startSize); return substr($mappedKey, strlen($key . self::MAPPED_KEY_SEPARATOR)); } /** * Serialize a map to set it as a value * * @param array $map * @return string */ protected function serializeMap(array $map) { return $this->getMapIdentifier() . json_encode(array_keys($map)); } /** * Unserialize a map that contains references to mapped keys * * @param $map * @return mixed */ protected function unSerializeMap($map) { return json_decode(substr_replace($map, '', 0, strlen($this->getMapIdentifier())), true); } /** * Check if the value was split into couple of values. * Identifiable by the map identifier at beginning * * @param $value * @return bool */ protected function isSplit($value) { if (!is_string($value)) { return false; } return strpos($value, $this->getMapIdentifier()) === 0; } /** * Get the current maximum allowed size for a value * * @return int * @throws common_Exception If size is set */ protected function getSize() { if (! $this->size) { $size = $this->getParam(self::MAX_VALUE_SIZE); if ($size !== false) { if (!is_int($size)) { throw new common_Exception('Persistence max value size has to be an integer'); } $this->size = $size - strlen($this->getMapIdentifier()); } } return $this->size; } /** * Check if the current persistence has a max size parameter * * @return boolean */ protected function hasMaxSize() { return $this->getParam(self::MAX_VALUE_SIZE) !== false; } /** * Get the identifier to identify a value as split. Should be no used into beginning of none mapped key * * @return string */ protected function getMapIdentifier() { return $this->getParam(self::MAP_IDENTIFIER) ?: self::DEFAULT_MAP_IDENTIFIER; } /** * Get the start-map delimiter from config, otherwise fallback to default value * * @return string */ protected function getStartMapDelimiter() { return $this->getParam(self::START_MAP_DELIMITER) ?: self::DEFAULT_START_MAP_DELIMITER; } /** * Get the end-map delimiter from config, otherwise fallback to default value * * @return string */ protected function getEndMapDelimiter() { return $this->getParam(self::END_MAP_DELIMITER) ?: self::DEFAULT_END_MAP_DELIMITER; } /** * Get the requested param from current parameters, otherwise throws exception * * @param $param * @return mixed */ protected function getParam($param) { $params = $this->getParams(); if (! isset($params[$param])) { return false; } return $params[$param]; } /** * Test wheever or not a feature is supported * @param string $feature * @throws common_exception_Error if feature is unkown * @return boolean */ public function supportsFeature($feature) { switch ($feature) { case self::FEATURE_NX: return ($this->getDriver() instanceof common_persistence_KeyValue_Nx); default: throw new common_exception_Error('Unknown feature ' . $feature); } return false; } }