<?php
  
/**
 * Application scope in PHP - implemented with Memcache.
 * 
 * By Chris Dary of Arc90, Inc.
 * chrisd@arc90.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$value0$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;
    }

}
?>