4 +-----------------------------------------------------------------------+
5 | program/include/rcube_cache.php |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2011, The Roundcube Dev Team |
9 | Copyright (C) 2011, Kolab Systems AG |
10 | Licensed under the GNU GPL |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com> |
17 | Author: Aleksander Machniak <alec@alec.pl> |
18 +-----------------------------------------------------------------------+
20 $Id: rcube_cache.php 4909 2011-07-05 17:09:25Z alec $
26 * Interface class for accessing Roundcube cache
29 * @author Thomas Bruederli <roundcube@gmail.com>
30 * @author Aleksander Machniak <alec@alec.pl>
36 * Instance of rcube_mdb2 or Memcache class
38 * @var rcube_mdb2/Memcache
47 private $cache = array();
48 private $cache_keys = array();
49 private $cache_changes = array();
50 private $cache_sums = array();
56 * @param string $type Engine type ('db' or 'memcache' or 'apc')
57 * @param int $userid User identifier
58 * @param string $prefix Key name prefix
59 * @param int $ttl Expiration time of memcache/apc items in seconds (max.2592000)
60 * @param bool $packed Enables/disabled data serialization.
61 * It's possible to disable data serialization if you're sure
62 * stored data will be always a safe string
64 function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
66 $rcmail = rcmail::get_instance();
67 $type = strtolower($type);
69 if ($type == 'memcache') {
70 $this->type = 'memcache';
71 $this->db = $rcmail->get_memcache();
73 else if ($type == 'apc') {
75 $this->db = function_exists('apc_exists'); // APC 3.1.4 required
79 $this->db = $rcmail->get_dbh();
82 $this->userid = (int) $userid;
83 $this->ttl = (int) $ttl;
84 $this->packed = $packed;
85 $this->prefix = $prefix;
90 * Returns cached value.
92 * @param string $key Cache key name
94 * @return mixed Cached value
98 if (!array_key_exists($key, $this->cache)) {
99 return $this->read_record($key);
102 return $this->cache[$key];
107 * Sets (add/update) value in cache.
109 * @param string $key Cache key name
110 * @param mixed $data Cache data
112 function set($key, $data)
114 $this->cache[$key] = $data;
115 $this->cache_changed = true;
116 $this->cache_changes[$key] = true;
121 * Returns cached value without storing it in internal memory.
123 * @param string $key Cache key name
125 * @return mixed Cached value
129 if (array_key_exists($key, $this->cache)) {
130 return $this->cache[$key];
133 return $this->read_record($key, true);
138 * Sets (add/update) value in cache and immediately saves
139 * it in the backend, no internal memory will be used.
141 * @param string $key Cache key name
142 * @param mixed $data Cache data
144 * @param boolean True on success, False on failure
146 function write($key, $data)
148 return $this->write_record($key, $this->packed ? serialize($data) : $data);
155 * @param string $key Cache key name or pattern
156 * @param boolean $prefix_mode Enable it to clear all keys starting
157 * with prefix specified in $key
159 function remove($key=null, $prefix_mode=false)
163 $this->cache = array();
164 $this->cache_changed = false;
165 $this->cache_changes = array();
166 $this->cache_keys = array();
168 // Remove keys by name prefix
169 else if ($prefix_mode) {
170 foreach (array_keys($this->cache) as $k) {
171 if (strpos($k, $key) === 0) {
172 $this->cache[$k] = null;
173 $this->cache_changes[$k] = false;
174 unset($this->cache_keys[$k]);
178 // Remove one key by name
180 $this->cache[$key] = null;
181 $this->cache_changes[$key] = false;
182 unset($this->cache_keys[$key]);
185 // Remove record(s) from the backend
186 $this->remove_record($key, $prefix_mode);
191 * Writes the cache back to the DB.
195 if (!$this->cache_changed) {
199 foreach ($this->cache as $key => $data) {
200 // The key has been used
201 if ($this->cache_changes[$key]) {
202 // Make sure we're not going to write unchanged data
203 // by comparing current md5 sum with the sum calculated on DB read
204 $data = $this->packed ? serialize($data) : $data;
206 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
207 $this->write_record($key, $data);
212 $this->write_index();
219 * @param string $key Cache key name
220 * @param boolean $nostore Enable to skip in-memory store
222 * @return mixed Cached value
224 private function read_record($key, $nostore=false)
230 if ($this->type == 'memcache') {
231 $data = $this->db->get($this->ckey($key));
233 else if ($this->type == 'apc') {
234 $data = apc_fetch($this->ckey($key));
238 $md5sum = md5($data);
239 $data = $this->packed ? unserialize($data) : $data;
245 $this->cache_sums[$key] = $md5sum;
246 $this->cache[$key] = $data;
249 if ($this->type == 'db') {
250 $sql_result = $this->db->limitquery(
251 "SELECT cache_id, data, cache_key".
252 " FROM ".get_table_name('cache').
253 " WHERE user_id = ?".
254 " AND cache_key = ?".
255 // for better performance we allow more records for one key
257 " ORDER BY created DESC",
258 0, 1, $this->userid, $this->prefix.'.'.$key);
260 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
261 $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
262 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
263 if ($sql_arr['data']) {
264 $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
271 $this->cache[$key] = $data;
272 $this->cache_sums[$key] = $md5sum;
273 $this->cache_keys[$key] = $sql_arr['cache_id'];
277 return $this->cache[$key];
282 * Writes single cache record into DB.
284 * @param string $key Cache key name
285 * @param mxied $data Serialized cache data
287 * @param boolean True on success, False on failure
289 private function write_record($key, $data)
295 if ($this->type == 'memcache' || $this->type == 'apc') {
296 return $this->add_record($this->ckey($key), $data);
299 $key_exists = $this->cache_keys[$key];
300 $key = $this->prefix . '.' . $key;
302 // Remove NULL rows (here we don't need to check if the record exist)
305 "DELETE FROM ".get_table_name('cache').
306 " WHERE user_id = ?".
307 " AND cache_key = ?",
308 $this->userid, $key);
313 // update existing cache record
315 $result = $this->db->query(
316 "UPDATE ".get_table_name('cache').
317 " SET created = ". $this->db->now().", data = ?".
318 " WHERE user_id = ?".
319 " AND cache_key = ?",
320 $data, $this->userid, $key);
322 // add new cache record
324 // for better performance we allow more records for one key
325 // so, no need to check if record exist (see rcube_cache::read_record())
326 $result = $this->db->query(
327 "INSERT INTO ".get_table_name('cache').
328 " (created, user_id, cache_key, data)".
329 " VALUES (".$this->db->now().", ?, ?, ?)",
330 $this->userid, $key, $data);
333 return $this->db->affected_rows($result);
338 * Deletes the cache record(s).
340 * @param string $key Cache key name or pattern
341 * @param boolean $prefix_mode Enable it to clear all keys starting
342 * with prefix specified in $key
345 private function remove_record($key=null, $prefix_mode=false)
351 if ($this->type != 'db') {
356 foreach ($this->index as $key) {
357 $this->delete_record($key, false);
359 $this->index = array();
361 // Remove keys by name prefix
362 else if ($prefix_mode) {
363 foreach ($this->index as $k) {
364 if (strpos($k, $key) === 0) {
365 $this->delete_record($k);
369 // Remove one key by name
371 $this->delete_record($key);
377 // Remove all keys (in specified cache)
379 $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
381 // Remove keys by name prefix
382 else if ($prefix_mode) {
383 $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
385 // Remove one key by name
387 $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
391 "DELETE FROM ".get_table_name('cache').
392 " WHERE user_id = ?" . $where,
398 * Adds entry into memcache/apc DB.
400 * @param string $key Cache key name
401 * @param mxied $data Serialized cache data
402 * @param bollean $index Enables immediate index update
404 * @param boolean True on success, False on failure
406 private function add_record($key, $data, $index=false)
408 if ($this->type == 'memcache') {
409 $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
411 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
413 else if ($this->type == 'apc') {
414 if (apc_exists($key))
416 $result = apc_store($key, $data, $this->ttl);
420 if ($index && $result) {
423 if (array_search($key, $this->index) === false) {
424 $this->index[] = $key;
425 $data = serialize($this->index);
426 $this->add_record($this->ikey(), $data);
435 * Deletes entry from memcache/apc DB.
437 private function delete_record($key, $index=true)
439 if ($this->type == 'memcache')
440 $this->db->delete($this->ckey($key));
442 apc_delete($this->ckey($key));
445 if (($idx = array_search($key, $this->index)) !== false) {
446 unset($this->index[$idx]);
453 * Writes the index entry into memcache/apc DB.
455 private function write_index()
461 if ($this->type == 'db') {
467 // Make sure index contains new keys
468 foreach ($this->cache as $key => $value) {
469 if ($value !== null) {
470 if (array_search($key, $this->index) === false) {
471 $this->index[] = $key;
476 $data = serialize($this->index);
477 $this->add_record($this->ikey(), $data);
482 * Gets the index entry from memcache/apc DB.
484 private function load_index()
490 if ($this->index !== null) {
494 $index_key = $this->ikey();
495 if ($this->type == 'memcache') {
496 $data = $this->db->get($index_key);
498 else if ($this->type == 'apc') {
499 $data = apc_fetch($index_key);
502 $this->index = $data ? unserialize($data) : array();
507 * Creates per-user cache key name (for memcache and apc)
509 * @param string $key Cache key name
511 * @return string Cache key
513 private function ckey($key)
515 return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
520 * Creates per-user index cache key name (for memcache and apc)
522 * @return string Cache key
524 private function ikey()
526 // This way each cache will have its own index
527 return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');