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