<?php
/**
* Application scope in PHP - implemented with Memcache.
*
* By Chris Dary of Arc90, Inc.
* hide@address.com
* http://www.arc90.com
*
* This work is licensed under a Creative Commons Attribution 3.0 License
* http://creativecommons.org/licenses/by/3.0/
*
* Example code available at http://lab.arc90.com/
*
**/
class Application
{
// private variables
private $key; // The memcache key - the only required parameter.
private $cache; // The array to temporarily store objects in after they are pulled from memcache.
private $memcache_keys; // Associative array of objects that are currently available through memcache. Value is their expire timestamp, or false if never.
// memcached specific variables
private $memcached_obj;
private $memcached_prefix;
private $memcached_servers;
/**
* constructor __construct
* Constructor for the Application class. Sets up our configuration, and connects to the memcache daemon.
* @param array an associative array of initial values.
**/
public function __construct( $configarray )
{
// If configarray is just a string, that means that the
// user wants to use the defaults. Just set the key.
if(is_string($configarray))
{
$this->key = $configarray;
}
else
{
// Parse our configuration options
foreach($configarray as $k=>$v)
{
switch($k)
{
case 'key':
case 'name':
$this->key = $v;
break;
case 'memcached_servers':
// If they passed in a string, make it a single element in an array. Otherwise, use the array they passed.
$this->memcached_servers = is_array($v) ? $v : array($v);
break;
case 'memcached_prefix':
$this->memcached_prefix = $v;
break;
default:
throw new Exception('Unknown parameter "' . $k . '" passed with value "' . $v . '"');
}
}
}
if(!$this->key)
throw new Exception("Application key is a required argument.");
// Set Defaults
if(class_exists("Memcache"))
$this->memcached_obj = new Memcache;
else
throw new Exception("You do not have the PHP extension for memcache installed.");
if(!$this->memcached_servers)
$this->memcached_servers = array("localhost:11211");
if(!$this->memcached_prefix)
$this->memcached_prefix = "appvar_";
// Sanitize the key (only alphanumerics and underscores), and add the prefix to avoid scoping issues with other memcache keys.
$this->key = $this->memcached_prefix . preg_replace("/[^a-zA-Z0-9_]/","",$this->key);
// Set up our connection
$connectSuccess = false;
foreach($this->memcached_servers as $server)
{
if(strpos($server,':'))
{
list($host,$port) = explode(':',$server);
}
else
{
$host = $server;
$port = 11211;
}
$connectSuccess |= $this->memcached_obj->addServer($host, $port);
}
if(!$connectSuccess)
die("Could not connect to a memcache daemon");
// We're done configuring. Initialize our backend.
$this->prepare_cache();
}
/**
* function prepare_cache
* prepare the object in the backend to store our data. If it doesn't exist, create it.
* @return bool true on success, false on failure
**/
private function prepare_cache()
{
// Set up our caching array
$this->cache = array();
$this->memcache_keys = $this->memcached_obj->get($this->key);
// Was our shared memory already initialized before?
if($this->memcache_keys)
{
// Yes, it was already created. Initialize our array of keys associated with this application.
if(!is_array($this->memcache_keys))
$this->memcache_keys = unserialize($this->memcache_keys);
// Remove any expired keys
$mkeys = array_keys($this->memcache_keys);
$keys_modified = false;
foreach($mkeys as $k)
{
if($this->memcache_keys[$k] && $this->memcache_keys[$k] < time())
{
$keymod = true;
unset($this->memcache_keys[$k]);
}
}
// If we removed any keys from expiration, we want to update that on memcached.
if( $keys_modified )
$this->update_keys();
return ($this->memcache_keys != false);
}
else
{
// No, it wasn't yet initialized. So initialize it.
return $this->create_memcache();
}
}
/**
* function create_memcache
* Create a new object in the backend to store our data
* @return bool true on success, false on failure
**/
private function create_memcache()
{
$this->memcache_keys = array();
$this->memcached_obj->set($this->key,array());
return true;
}
/**
* function clear
* Clears the application structure, as well as their references in the memcache backend
* Useful for flushing out all variables from the application.
* @return bool true on success, false on failure
**/
public function clear()
{
foreach(array_keys($this->memcache_keys) as $k)
$this->memcached_obj->delete($k);
$this->cache = array();
$this->memcache_keys = array();
return $this->create_memcache();
}
/**
* function __get
* Get a cached property. If it's in our local cache, return it. Otherwise, get it from memcache.
* @param string Name of property to return
* NOTICE: This uses some ArrayObject trickery to avoid passing array objects erroneously.
* See http://derickrethans.nl/overloaded_properties_get.php
* @return mixed The requested value
**/
public function __get( $name )
{
if(!isset($this->cache[$name]))
$this->cache[$name] = $this->memcached_obj->get($this->keyify($name));
return is_array($this->cache[$name]) ? new ArrayObject($this->cache[$name]) : $this->cache[$name];
}
/**
* function __set
* Overrides __set to implement custom object properties. Updates our local variable, as well as in memcache.
* @param string Name of property to set
* @param mixed Value of property
* @return bool true on success, false on failure
**/
public function __set( $name, $value )
{
$key = $this->keyify($name);
$this->cache[$name] = $value;
if(!isset($this->memcache_keys[$key]))
{
$this->memcache_keys[$key] = false;
$this->update_keys();
}
return $this->memcached_obj->set($key, $value);
}
/**
* function setWithExpire
* Same as __set, except it allows an expiration date to be set.
* Syntax: $app->setWithExpire('key', 'value', [expire]) where [expire] is
* @param string Name of property to set
* @param mixed Value of property
* @param integer Expiration time of the item. If it's equal to zero, the item will never expire. You can also use Unix
* timestamp or a number of seconds starting from current time, but in the latter case the number of seconds
* may not exceed 2592000 (30 days) or it will be parsed as a timestamp.
* @return bool true on success, false on failure
**/
public function setWithExpire( $name, $value, $expire )
{
if(is_string($expire))
$expire = strtotime($expire);
$key = $this->keyify($name);
$this->cache[$name] = $value;
if(!isset($this->memcache_keys[$key]))
{
$this->memcache_keys[$key] = $expire < 2592000 ? (time()+$expire) : $expire;
$this->update_keys();
}
return $this->memcached_obj->set($key, $value, 0, $expire);;
}
/**
* function __isset
* Overrides __isset to implement custom object properties. Checks if the object name was provided in our memcache_keys array.
* @param string Name of property to check
**/
public function __isset( $name )
{
$k = $this->keyify($name);
if( isset($this->memcache_keys[$k]))
{
if( $this->memcache_keys[$k] !== false && $this->memcache_keys[$k] >= time() )
{
// Our key has not yet expired, so cache it for this request and return it.
// This is done to avoid a race condition where the object expires between isset() and __get()
$obj = self::__get($name);
return ($obj !== false);
}
return true;
}
return false;
}
/**
* function __unset
* Overrides __unset to properly delete an item from memcache
* @param string Name of property to unset
**/
public function __unset( $name )
{
unset($this->memcache_keys[$this->keyify($name)]);
unset($this->cache[$name]);
$this->memcached_obj->delete($this->keyify($name));
}
/**
* function getKeys
* Get the list of keys for this application, without the key scoping.
* @return array the keys, with values of expiration timestamp if it exists, or false if it does not.
**/
public function getKeys()
{
$keylist = array();
$find = $this->key . '_';
foreach(array_keys($this->memcache_keys) as $k)
{
$keylist[str_replace($find,'',$k)] = $this->memcache_keys[$k];
}
return $keylist;
}
/**
* function update_keys
* Update the global keys object in memcache
**/
private function update_keys()
{
return $this->memcached_obj->set($this->key, $this->memcache_keys);
}
/**
* function keyify
* Turn a variable name into its corresponding memcache key
* @param string Name of property to keyify
**/
private function keyify( $name )
{
return $this->key . '_' . $name;
}
}
?>