* @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved * @license LICENSE MIT */ class MemoryStore implements KeyValueStore { /** * @var array */ protected $items = array(); /** * @var int */ protected $limit = 0; /** * @var int */ protected $size = 0; /** * @param int|string $limit Memory limit in bytes (defaults to 10% of memory_limit) */ public function __construct($limit = null) { if (null === $limit) { $phpLimit = ini_get('memory_limit'); if ($phpLimit <= 0) { $this->limit = PHP_INT_MAX; } else { $this->limit = (int) ($this->shorthandToBytes($phpLimit) / 10); } } else { $this->limit = $this->shorthandToBytes($limit); } } /** * {@inheritdoc} */ public function get($key, &$token = null) { if (!$this->exists($key)) { $token = null; return false; } $value = $this->items[$key][0]; // use serialized version of stored value as CAS token $token = $value; return unserialize($value); } /** * {@inheritdoc} */ public function getMulti(array $keys, array &$tokens = null) { $items = array(); $tokens = array(); foreach ($keys as $key) { if (!$this->exists($key)) { // omit missing keys from return array continue; } $items[$key] = $this->get($key, $token); $tokens[$key] = $token; } return $items; } /** * {@inheritdoc} */ public function set($key, $value, $expire = 0) { $this->size -= isset($this->items[$key]) ? strlen($this->items[$key][0]) : 0; $value = serialize($value); $expire = $this->normalizeTime($expire); $this->items[$key] = array($value, $expire); $this->size += strlen($value); $this->lru($key); $this->evict(); return true; } /** * {@inheritdoc} */ public function setMulti(array $items, $expire = 0) { $success = array(); foreach ($items as $key => $value) { $success[$key] = $this->set($key, $value, $expire); } return $success; } /** * {@inheritdoc} */ public function delete($key) { $exists = $this->exists($key); if ($exists) { $this->size -= strlen($this->items[$key][0]); unset($this->items[$key]); } return $exists; } /** * {@inheritdoc} */ public function deleteMulti(array $keys) { $success = array(); foreach ($keys as $key) { $success[$key] = $this->delete($key); } return $success; } /** * {@inheritdoc} */ public function add($key, $value, $expire = 0) { if ($this->exists($key)) { return false; } return $this->set($key, $value, $expire); } /** * {@inheritdoc} */ public function replace($key, $value, $expire = 0) { if (!$this->exists($key)) { return false; } return $this->set($key, $value, $expire); } /** * {@inheritdoc} */ public function cas($token, $key, $value, $expire = 0) { if (!$this->exists($key)) { return false; } $this->get($key, $comparison); if ($comparison !== $token) { return false; } return $this->set($key, $value, $expire); } /** * {@inheritdoc} */ public function increment($key, $offset = 1, $initial = 0, $expire = 0) { if ($offset <= 0 || $initial < 0) { return false; } return $this->doIncrement($key, $offset, $initial, $expire); } /** * {@inheritdoc} */ public function decrement($key, $offset = 1, $initial = 0, $expire = 0) { if ($offset <= 0 || $initial < 0) { return false; } return $this->doIncrement($key, -$offset, $initial, $expire); } /** * {@inheritdoc} */ public function touch($key, $expire) { $expire = $this->normalizeTime($expire); // get current value & re-save it, with new expiration $value = $this->get($key, $token); return $this->cas($token, $key, $value, $expire); } /** * {@inheritdoc} */ public function flush() { $this->items = array(); $this->size = 0; return true; } /** * {@inheritdoc} */ public function getCollection($name) { return new Collection($this, $name); } /** * Checks if a value exists in cache and is not yet expired. * * @param string $key * * @return bool */ protected function exists($key) { if (!array_key_exists($key, $this->items)) { // key not in cache return false; } $expire = $this->items[$key][1]; if (0 !== $expire && $expire < time()) { // not permanent & already expired $this->size -= strlen($this->items[$key][0]); unset($this->items[$key]); return false; } $this->lru($key); return true; } /** * Shared between increment/decrement: both have mostly the same logic * (decrement just increments a negative value), but need their validation * split up (increment won't accept negative values). * * @param string $key * @param int $offset * @param int $initial * @param int $expire * * @return int|bool */ protected function doIncrement($key, $offset, $initial, $expire) { if (!$this->exists($key)) { $this->set($key, $initial, $expire); return $initial; } $value = $this->get($key); if (!is_numeric($value) || $value < 0) { return false; } $value += $offset; // value can never be lower than 0 $value = max(0, $value); $this->set($key, $value, $expire); return $value; } /** * Times can be: * * relative (in seconds) to current time, within 30 days * * absolute unix timestamp * * 0, for infinity. * * The first case (relative time) will be normalized into a fixed absolute * timestamp. * * @param int $time * * @return int */ protected function normalizeTime($time) { // 0 = infinity if (!$time) { return 0; } // relative time in seconds, <30 days if ($time < 30 * 24 * 60 * 60) { $time += time(); } return $time; } /** * This cache uses least recently used algorithm. This is to be called * with the key to be marked as just used. */ protected function lru($key) { // move key that has just been used to last position in the array $value = $this->items[$key]; unset($this->items[$key]); $this->items[$key] = $value; } /** * Least recently used cache values will be evicted from cache should * it fill up too much. */ protected function evict() { while ($this->size > $this->limit && !empty($this->items)) { $item = array_shift($this->items); $this->size -= strlen($item[0]); } } /** * Understands shorthand byte values (as used in e.g. memory_limit ini * setting) and converts them into bytes. * * @see http://php.net/manual/en/faq.using.php#faq.using.shorthandbytes * * @param string|int $shorthand Amount of bytes (int) or shorthand value (e.g. 512M) * * @return int */ protected function shorthandToBytes($shorthand) { if (is_numeric($shorthand)) { // make sure that when float(1.234E17) is passed in, it doesn't get // cast to string('1.234E17'), then to int(1) return $shorthand; } $units = array('B' => 1024, 'M' => pow(1024, 2), 'G' => pow(1024, 3)); return (int) preg_replace_callback('/^([0-9]+)(' . implode('|', array_keys($units)) . ')$/', function ($match) use ($units) { return $match[1] * $units[$match[2]]; }, $shorthand); } }