<?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;
 
}
 
 
}
 
 
 |