<?php

/**
 * @author Bastiaan Welmers <haasje@welmers.net>
 *
 * cache layer to an object 
 */
class Cache
{
	/**
	 * holds object that needs to be cached
	 * @var object
	 */
	private $object;
	
	/**
	 * the Cache ID of the cache data in Zend_Cache
	 * @var string
	 */
	private $cacheId;
	
	/**
	 * The instance of Zend_Cache
	 * @var Zend_Cache
	 */
	private $cacheInstance;
	
	/**
	 * The cache data
	 * @var array
	 */
	private $cache;
	
	/**
	 * Keeps track of changes in cache data. If true, cache data needs to be saved on destruction
	 * @var boolean
	 */
	private $needSave = false;
	
	/**
	 * Set to false by constructor if data doesnt need to be saved on destruction. default true.
	 * @var boolean
	 */
	private $saveAtDestruct = true;
	
	/**
	 * construct cache object.
	 * @param object Object that needs to be cached
	 * @param object optional object (passed by instance) where to save data in, instead of 
	 *               creating our own. if this variable is set, cache data wil not be saved by us
	 *               @see Cache_Iterator for this kind of use
	 */
	public function __construct($object, &$cache = false)
	{
		$this->object = $object;
		if ($cache === false)
		{
			// cache variable passed, here we save the data in.
			
			// try to find an unique id of the object for saving
			if (isset($object->id))
				$this->cacheId = get_class($object) . $object->id;
			else
				$this->cacheId = get_class($object) . '_' . md5(serialize($object));
			
			// get cache instance from zend_registry. Here we use a Zend_Cache backend object.
			// Here you can set your own if not using Zend_Cache.
			// It must however implement save() and load().
			$this->cacheInstance = Zend_Registry::get('cache');
			
			// load data from cache if available
			$this->cache = $this->cacheInstance->load($this->cacheId);
			if (!is_array($this->cache))
				$this->cache = array();
			
		} else {
			// cache variable passed, here we save the data in.
			$this->cache =& $cache;
			if (!is_array($this->cache))
				$this->cache = array();
			// don't save the data on destruction
			$this->saveAtDestruct = false;
		}
	}
	
	/**
	 * catch all calls to this object and transparently pass them 
	 * to the object with caching in between.
	 * @param string method name
	 * @param array arguments of functions
	 * @return mixed result of call
	 */
	public function __call($method, array $args)
	{
		if (is_callable(array($this->object, $method)))
		{
			// make up an string ID
			$id = "method_{$method}_" . md5(serialize($args));
			if (!isset($this->cache[$id]))
			{ // not found in cache, do the actual call to our object
				
				$return = call_user_func_array(array($this->object, $method), $args);
				
				// if having a Date object, return a dedicated date cache object
				if ($return instanceof Date)
					$return = new Cache_Date($return, $this->cache[$id . '_date']);
				
				// save our result in cache data array
				$this->cache[$id] = $return;
				// we need so save the cache at destruction
				$this->needSave = true;
			} else
				// found in cache, get data from cache
				$return = $this->cache[$id];
			
			return $return;
			
		} else
			throw new Cache_Exception(get_class($this->object) . "::$method() is not callable");
	}
	
	/**
	 * catch all properties of this object and transparently get them 
	 * from the object with caching in between.
	 * @param string property
	 * @return mixed result of underlaying object
	 */
	public function __get($property)
	{
		if (isset($this->object->$property))
		{ 
			$id = "property_{$property}";
			if (!isset($this->cache[$id]))
			{ // not found in cache. request from our object
				$return = $this->object->$property;
				// save in cache data array
				$this->cache[$id] = $return;
				// we need so save the cache at destruction
				$this->needSave = true;
			} else
				// get our result from cache
				$return = $this->cache[$id];
			
			return $return;
			
		} else
			throw new Cache_Exception(get_class($this->object) . "::$name is not set");
	}
	
	/**
	 * Checks if an item is set. We pass it to the 
	 * the object directly.
	 * @param string property
	 * @return boolean true if set false if not
	 */
	public function __isset($property)
	{
		return isset($this->object->$property);
	}
	
	/**
	 * destructor for saving cache data to cache if neccessary, on
	 * end of object lifetime.
	 */
	public function __destruct()
	{
		
		if ($this->saveAtDestruct && $this->needSave && isset($this->cacheInstance))
			$this->cacheInstance->save($this->cache, $this->cacheId);
		
	}
	
	/**
	 * __toString magic function if this object is used as string.
	 * pass call to __call()
	 */
	public function __toString()
	{
		return $this->__call('__toString', array());
	}
	
}

