| 
<?php
/**
 * @author Colin McKinnon
 * @licence LGPL version 3
 * @package item cache
 *
 * The class implements a key store cache for PHP code
 * By default it will discard items based on least-recently-used but
 * can be configured to pass the discard entries to a callback function.
 * Further, it can evict chunks of entities at a time - so if the discarded
 * items are written to storage, this should reduce I/O
 *
 * The flush method evicts all entries (invoking the callback with chunks
 * of entries where appropriate)
 *
 * It is also possible (via a callback mechanism) to let an external function
 * (or method) find data not currently in the cache. The external function is passed
 * a reference to $this and should call $this->add() where it finds the item
 *
 * method sigs:
 *
 * __construct($max_entries=100, $overflow=false, $overflowChunk=1, $underflow=false)
 * add($key, $val)
 * get($key, $refresh_cache=true)
 * expire($key)
 * flush($sorted=false)
 */
 
 define('ICACHE_NO_CHANGE', 0);
 define('ICACHE_UPDATED', 1);
 define('ICACHE_INSERTED', 2);
 
 class itemCache {
 private $curr_size;
 private $max_count;
 private $data;
 private $serial;
 private $overflow;
 private $overflowChunk;
 private $hits;
 private $misses;
 private $evicts;
 /**
 * Constructor
 *
 * @param int max_entries - size of cache in entries
 * @param callback overflow - callback to handle items evicted from cache or false if none
 * @param int overflowChunk - number of entries to evict at a time
 * @param callback underflow - callack to invoke when item not found in cache
 *
 * A size of more than about 300 items is likely to have an adverse effect on performance
 * if it's possible to fetch items from a database using the $underflow callback
 */
 public function __construct($max_entries=100, $overflow=false, $overflowChunk=1, $underflow=false)
 {
 $this->max_count=$max_entries;
 $this->serial=0;
 $this->curr_size=0;
 $this->data=array();
 if ($overflow && $overflowChunk>0) {
 $this->overflow=$overflow;
 $this->overflowChunk=($overflowChunk > $max_entries/3) ? $max_entries/3 : $overflowChunk;
 }
 $this->underflow=$underflow;
 }
 /**
 * report on usage
 */
 public function stats()
 {
 return array('hits'=>$this->hits, 'misses'=>$this->misses
 , 'updates'=>$this->serial, 'evicts'=>$this->evicts);
 }
 /**
 * Add an item to the cache, if cache full, oldest $overflowChunk items will be evicted
 *
 * @param mixed $key
 * @param mixed $val
 * @param bool $nowriteback - don't pass this entry to the overflow handler
 * @return int
 *
 * returned integer will be ICACHE_NO_CHANGE if the key is already in the cache with the same value
 * ICACHE_UPDATED if the key is present but held a different value
 * ICACHE_INSERTED if the key was added
 */
 public function add($key, $val, $writeback=true)
 {
 $already=array_key_exists($key, $this->data);
 if ($already) { $this->hits++; } else { $this->misses++; }
 if (count($this->data)>$this->max_count && !$already) {
 $this->removeOldest();
 }
 if ($already) {
 if (serialize($val)===serialize($this->data[$key]['v'])) {
 $this->data[$key]['s']=$this->serial++;
 $this->data[$key]['o']=$writeback;
 return ICACHE_NO_CHANGE;
 }
 $this->data[$key]=array(
 's'=>$this->serial++,
 'v'=>$val,
 'o'=>$writeback);
 return ICACHE_UPDATED;
 }
 $this->data[$key]=array(
 's'=>$this->serial++,
 'v'=>$val,
 'o'=>$writeback);
 return ICACHE_INSERTED;
 }
 /**
 * attempt to retrieve the item from the cache
 *
 * @param mixed $key
 * @param bool $refresh_cache - if set to false the item will not be marked as freshly accessed
 * @return mixed - the value set for the key
 */
 public function get($key, $refresh_cache=true)
 {
 $in_array=array_key_exists($key, $this->data);
 if ($in_array) { $this->hits++; } else { $this->misses++; };
 if ($refresh_cache && $in_array) {
 $this->data[$key]['s']++;
 return $this->data[$key]['v'];
 }
 if (!$in_array && $this->underflow) {
 call_user_func($this->underflow, $key, $this);
 }
 return $this->data[$key][$v];
 }
 public function expire($key)
 {
 unset($this_data[$key]);
 }
 /**
 * removes the oldest N items from the cache
 * where N is the overflow chunk size
 *
 * if overflow callback is defined, this will be invoked with
 * callback(array( $key[1]=>$value[1],....));
 */
 protected function removeOldest()
 {
 // move oldest to start
 uasort($this->data, array($this, 'sortAge'));
 // get the value (array_unshift will change numeric keys!)
 $chunk=array();
 $count=0;
 foreach ($this->data as $key=>$entry) {
 if ($entry['o']) {
 $chunk[$key]=$entry['v'];
 }
 unset($this->data[$key]);
 if ($count++>=$this->overflowChunk) break;
 }
 $this->evicts+=$count;
 if ($this->overflow && count($chunk)) {
 return call_user_func($this->overflow, $chunk);
 } else {
 return true;
 }
 }
 /**
 * removes all items from the cache
 *
 * @param bool $sorted - if true then the data is sorted by age (oldest first) before being presented to the overflow callback
 *
 * If overflow callback is defined this will be called with chunks of data
 * callback(array( $key[1]=>$value[1],....));
 *
 * sorting will impact performance
 */
 public function flush($sorted=false)
 {
 $this->evicts+=count($this->data);
 if ($this->overflow===false) {
 $this->data=array();
 return true;
 }
 if ($sorted) {
 uasort($this->data, array($this, 'sortAge'));
 }
 while (count($this->data)) {
 $chunk=array();
 $count=0;
 foreach ($this->data as $key=>$entry) {
 if ($entry['o']) {
 $chunk[$key]=$entry['v'];
 }
 unset($this->data[$key]);
 if ($count++>=$this->overflowChunk) break;
 }
 if (count($chunk)) {
 call_user_func($this->overflow, $chunk);
 }
 }
 return true;
 }
 
 /**
 * internal callback for sorting by age
 */
 protected function sortAge($a, $b)
 {
 if ($a['s']==$b['s']) return 0;
 return ($a['s'] < $b['s']) ? -1 : 1;
 }
 
 }
 
 |