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 5305 2011-10-03 18:04:14Z 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 * Remove cache records older than ttl
195 if ($this->type == 'db' && $this->db) {
197 "DELETE FROM ".get_table_name('cache').
198 " WHERE user_id = ?".
199 " AND cache_key LIKE ?".
200 " AND " . $this->db->unixtimestamp('created')." < ?",
203 time() - $this->ttl);
209 * Writes the cache back to the DB.
213 if (!$this->cache_changed) {
217 foreach ($this->cache as $key => $data) {
218 // The key has been used
219 if ($this->cache_changes[$key]) {
220 // Make sure we're not going to write unchanged data
221 // by comparing current md5 sum with the sum calculated on DB read
222 $data = $this->packed ? serialize($data) : $data;
224 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
225 $this->write_record($key, $data);
230 $this->write_index();
237 * @param string $key Cache key name
238 * @param boolean $nostore Enable to skip in-memory store
240 * @return mixed Cached value
242 private function read_record($key, $nostore=false)
248 if ($this->type != 'db') {
249 if ($this->type == 'memcache') {
250 $data = $this->db->get($this->ckey($key));
252 else if ($this->type == 'apc') {
253 $data = apc_fetch($this->ckey($key));
257 $md5sum = md5($data);
258 $data = $this->packed ? unserialize($data) : $data;
264 $this->cache_sums[$key] = $md5sum;
265 $this->cache[$key] = $data;
268 $this->cache[$key] = null;
272 $sql_result = $this->db->limitquery(
273 "SELECT cache_id, data, cache_key".
274 " FROM ".get_table_name('cache').
275 " WHERE user_id = ?".
276 " AND cache_key = ?".
277 // for better performance we allow more records for one key
279 " ORDER BY created DESC",
280 0, 1, $this->userid, $this->prefix.'.'.$key);
282 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
283 $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
284 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
285 if ($sql_arr['data']) {
286 $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
293 $this->cache[$key] = $data;
294 $this->cache_sums[$key] = $md5sum;
295 $this->cache_keys[$key] = $sql_arr['cache_id'];
298 $this->cache[$key] = null;
302 return $this->cache[$key];
307 * Writes single cache record into DB.
309 * @param string $key Cache key name
310 * @param mxied $data Serialized cache data
312 * @param boolean True on success, False on failure
314 private function write_record($key, $data)
320 if ($this->type == 'memcache' || $this->type == 'apc') {
321 return $this->add_record($this->ckey($key), $data);
324 $key_exists = $this->cache_keys[$key];
325 $key = $this->prefix . '.' . $key;
327 // Remove NULL rows (here we don't need to check if the record exist)
330 "DELETE FROM ".get_table_name('cache').
331 " WHERE user_id = ?".
332 " AND cache_key = ?",
333 $this->userid, $key);
338 // update existing cache record
340 $result = $this->db->query(
341 "UPDATE ".get_table_name('cache').
342 " SET created = ". $this->db->now().", data = ?".
343 " WHERE user_id = ?".
344 " AND cache_key = ?",
345 $data, $this->userid, $key);
347 // add new cache record
349 // for better performance we allow more records for one key
350 // so, no need to check if record exist (see rcube_cache::read_record())
351 $result = $this->db->query(
352 "INSERT INTO ".get_table_name('cache').
353 " (created, user_id, cache_key, data)".
354 " VALUES (".$this->db->now().", ?, ?, ?)",
355 $this->userid, $key, $data);
358 return $this->db->affected_rows($result);
363 * Deletes the cache record(s).
365 * @param string $key Cache key name or pattern
366 * @param boolean $prefix_mode Enable it to clear all keys starting
367 * with prefix specified in $key
370 private function remove_record($key=null, $prefix_mode=false)
376 if ($this->type != 'db') {
381 foreach ($this->index as $key) {
382 $this->delete_record($key, false);
384 $this->index = array();
386 // Remove keys by name prefix
387 else if ($prefix_mode) {
388 foreach ($this->index as $k) {
389 if (strpos($k, $key) === 0) {
390 $this->delete_record($k);
394 // Remove one key by name
396 $this->delete_record($key);
402 // Remove all keys (in specified cache)
404 $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
406 // Remove keys by name prefix
407 else if ($prefix_mode) {
408 $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
410 // Remove one key by name
412 $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
416 "DELETE FROM ".get_table_name('cache').
417 " WHERE user_id = ?" . $where,
423 * Adds entry into memcache/apc DB.
425 * @param string $key Cache key name
426 * @param mxied $data Serialized cache data
427 * @param bollean $index Enables immediate index update
429 * @param boolean True on success, False on failure
431 private function add_record($key, $data, $index=false)
433 if ($this->type == 'memcache') {
434 $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
436 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
438 else if ($this->type == 'apc') {
439 if (apc_exists($key))
441 $result = apc_store($key, $data, $this->ttl);
445 if ($index && $result) {
448 if (array_search($key, $this->index) === false) {
449 $this->index[] = $key;
450 $data = serialize($this->index);
451 $this->add_record($this->ikey(), $data);
460 * Deletes entry from memcache/apc DB.
462 private function delete_record($key, $index=true)
464 if ($this->type == 'memcache')
465 $this->db->delete($this->ckey($key));
467 apc_delete($this->ckey($key));
470 if (($idx = array_search($key, $this->index)) !== false) {
471 unset($this->index[$idx]);
478 * Writes the index entry into memcache/apc DB.
480 private function write_index()
486 if ($this->type == 'db') {
492 // Make sure index contains new keys
493 foreach ($this->cache as $key => $value) {
494 if ($value !== null) {
495 if (array_search($key, $this->index) === false) {
496 $this->index[] = $key;
501 $data = serialize($this->index);
502 $this->add_record($this->ikey(), $data);
507 * Gets the index entry from memcache/apc DB.
509 private function load_index()
515 if ($this->index !== null) {
519 $index_key = $this->ikey();
520 if ($this->type == 'memcache') {
521 $data = $this->db->get($index_key);
523 else if ($this->type == 'apc') {
524 $data = apc_fetch($index_key);
527 $this->index = $data ? unserialize($data) : array();
532 * Creates per-user cache key name (for memcache and apc)
534 * @param string $key Cache key name
536 * @return string Cache key
538 private function ckey($key)
540 return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
545 * Creates per-user index cache key name (for memcache and apc)
547 * @return string Cache key
549 private function ikey()
551 // This way each cache will have its own index
552 return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');