<?php
/**
 * TOutputCache class file
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2006 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Revision: $  $Date: $
 * @package System.Web.UI.WebControls
 */

/**
 * TOutputCache class.
 *
 * TOutputCache enables caching a portion of a Web page, also known as
 * partial caching. The content being cached can be either static or
 * dynamic.
 *
 * To use TOutputCache, simply enclose the content to be cached
 * within the TOutputCache component tag on a template, e.g.,
 * <code>
 * <com:TOutputCache>
 *   content to be cached
 * </com:TOutputCache>
 * </code>
 * where content to be cached can be static text and/or component tags.
 *
 * The validity of the cached content is determined based on two factors:
 * the {@link setDuration Duration} and the {@link getCacheDependency CacheDependency}.
 * The former specifies the number of seconds that the data can remain
 * valid in cache (defaults to 60s), while the latter specifies a dependency
 * that the data depends on. If the dependency changes, the cached content
 * is invalidated. By default, TOutputCache doesn't specify a dependency.
 * Derived classes may override {@link getCacheDependency()} method to
 * enforce a dependency (such as system state change, etc.)
 *
 * The content fetched from cache may be variated with respect to
 * some parameters. It supports variation with respect to request parameters,
 * which is specified by {@link setVaryByParam VaryByParam} property.
 * If a specified request parameter is different, a different version of
 * cached content is used. This is extremely useful if a page's content
 * may be variated according to some GET parameters. To variate the cached
 * content by other factors, override {@link calculateCacheKey()} method.
 *
 * Output caches can be nested. An outer cache takes precedence over an
 * inner cache. This means, if the content cached by the inner cache expires
 * or is invalidated, while that by the outer cache not, the outer cached
 * content will be used.
 *
 * Note, TOutputCache is effective only for non-postback page requests
 * and when cache module is enabled.
 *
 * Do not attempt to address child controls of TOutputCache when the cached
 * content is to be used. Use {@link getContentCached ContentCached} property
 * to determine whether the content is cached or not.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Revision: $  $Date: $
 * @package System.Web.UI.WebControls
 * @since 3.1
 */
class TOutputCache extends TControl implements INamingContainer
{
	const CACHE_ID_PREFIX='prado:outputcache';
	private $_dataCached=false;
	private $_cacheAvailable=false;
	private $_cacheChecked=false;
	private $_cacheKey=null;
	private $_duration=60;
	private $_cache=null;
	private $_contents;
	private $_state;
	private $_actions=array();
	private $_varyByParam='';
	private $_keyPrefix='';

	/**
	 * Returns a value indicating whether body contents are allowed for this control.
	 * This method overrides the parent implementation by checking if cached
	 * content is available or not. If yes, it returns false, otherwise true.
	 * @param boolean whether body contents are allowed for this control.
	 */
	public function getAllowChildControls()
	{
		$this->determineCacheability();
		return !$this->_dataCached;
	}

	private function determineCacheability()
	{
		if(!$this->_cacheChecked)
		{
			$this->_cacheChecked=true;
			if(!$this->getPage()->getIsPostBack() && ($this->_cache=$this->getApplication()->getCache())!==null && $this->_duration>0)
			{
				$this->_cacheAvailable=true;
				$data=$this->_cache->get($this->getCacheKey());
				if(($this->_dataCached=($data!==false)))
					list($this->_contents,$this->_state,$this->_actions)=$data;
			}
		}
	}

	/**
	 * Performs the Init step for the control and all its child controls.
	 * This method overrides the parent implementation by setting up
	 * the stack of the output cache in the page.
	 * Only framework developers should use this method.
	 * @param TControl the naming container control
	 */
	protected function initRecursive($namingContainer=null)
	{
		if($this->_cacheAvailable && !$this->_dataCached)
		{
			$stack=$this->getPage()->getCachingStack();
			$stack->push($this);
			parent::initRecursive($namingContainer);
			$stack->pop();
		}
		else
			parent::initRecursive($namingContainer);
	}

	/**
	 * Performs the Load step for the control and all its child controls.
	 * This method overrides the parent implementation by setting up
	 * the stack of the output cache in the page. If the data is restored
	 * from cache, it also recovers the actions associated with the cached data.
	 * Only framework developers should use this method.
	 * @param TControl the naming container control
	 */
	protected function loadRecursive()
	{
		if($this->_cacheAvailable && !$this->_dataCached)
		{
			$stack=$this->getPage()->getCachingStack();
			$stack->push($this);
			parent::loadRecursive();
			$stack->pop();
		}
		else
		{
			if($this->_dataCached)
				$this->performActions();
			parent::loadRecursive();
		}
	}

	private function performActions()
	{
		$page=$this->getPage();
		$cs=$page->getClientScript();
		foreach($this->_actions as $action)
		{
			if($action[0]==='Page.ClientScript')
				call_user_func_array(array($cs,$action[1]),$action[2]);
			else if($action[0]==='Page')
				call_user_func_array(array($page,$action[1]),$action[2]);
			else
				call_user_func_array(array($this->getSubProperty($action[0]),$action[1]),$action[2]);
		}
	}

	/**
	 * Performs the PreRender step for the control and all its child controls.
	 * This method overrides the parent implementation by setting up
	 * the stack of the output cache in the page.
	 * Only framework developers should use this method.
	 * @param TControl the naming container control
	 */
	protected function preRenderRecursive()
	{
		if($this->_cacheAvailable && !$this->_dataCached)
		{
			$stack=$this->getPage()->getCachingStack();
			$stack->push($this);
			parent::preRenderRecursive();
			$stack->pop();
		}
		else
			parent::preRenderRecursive();
	}

	/**
	 * Loads state (viewstate and controlstate) into a control and its children.
	 * This method overrides the parent implementation by loading
	 * cached state if available.
	 * This method should only be used by framework developers.
	 * @param array the collection of the state
	 * @param boolean whether the viewstate should be loaded
	 */
	protected function loadStateRecursive(&$state,$needViewState=true)
	{
		$st=unserialize($state);
		parent::loadStateRecursive($st,$needViewState);
	}

	/**
	 * Saves all control state (viewstate and controlstate) as a collection.
	 * This method overrides the parent implementation by saving state
	 * into cache if needed.
	 * This method should only be used by framework developers.
	 * @param boolean whether the viewstate should be saved
	 * @return array the collection of the control state (including its children's state).
	 */
	protected function &saveStateRecursive($needViewState=true)
	{
		if($this->_dataCached)
			return $this->_state;
		else
		{
			$st=parent::saveStateRecursive($needViewState);
			// serialization is needed to avoid undefined classes when loading state
			$this->_state=serialize($st);
			return $this->_state;
		}
	}

	/**
	 * Registers an action associated with the content being cached.
	 * The registered action will be replayed if the content stored
	 * in the cache is served to end-users.
	 * @param string context of the action method. This is a property-path
	 * referring to the context object (e.g. Page, Page.ClientScript)
	 * @param string method name of the context object
	 * @param array list of parameters to be passed to the action method
	 */
	public function registerAction($context,$funcName,$funcParams)
	{
		$this->_actions[]=array($context,$funcName,$funcParams);
	}

	private function getCacheKey()
	{
		if($this->_cacheKey===null)
			$this->_cacheKey=$this->calculateCacheKey();
		return $this->_cacheKey;
	}

	/**
	 * Calculates the cache key.
	 * The key is calculated based on the unique ID of this control
	 * and the request parameters specified via {@link setVaryByParam VaryByParam}.
	 * This method may be overriden to support other variations in
	 * the calculated cache key.
	 * @return string cache key
	 */
	protected function calculateCacheKey()
	{
		if($this->_varyByParam!=='')
		{
			$params=array();
			$request=$this->getRequest();
			foreach(explode(',',$this->_varyByParam) as $name)
			{
				$name=trim($name);
				$params[$name]=$request->itemAt($name);
			}
			return $this->getBaseCacheKey().serialize($params);
		}
		else
			return $this->getBaseCacheKey();
	}

	/**
	 * @return string basic cache key without variations
	 */
	protected function getBaseCacheKey()
	{
		return self::CACHE_ID_PREFIX.$this->_keyPrefix.$this->getPage()->getPagePath().$this->getUniqueID();
	}

	/**
	 * Sets the prefix of the cache key.
	 * This method is used internally by {@link TTemplate}.
	 * @param string key prefix
	 */
	public function setCacheKeyPrefix($value)
	{
		$this->_keyPrefix=$value;
	}

	/**
	 * Returns the dependency of the data to be cached.
	 * The default implementation simply returns null, meaning no specific dependency.
	 * This method may be overriden to associate the data to be cached
	 * with additional dependencies.
	 * @return ICacheDependency
	 */
	protected function getCacheDependency()
	{
		return null;
	}

	/**
	 * @return boolean whether content enclosed is cached or not
	 */
	public function getContentCached()
	{
		return $this->_dataCached;
	}

	/**
	 * @return integer number of seconds that the data can remain in cache. Defaults to 60 seconds.
	 * Note, if cache dependency changes or cache space is limited,
	 * the data may be purged out of cache earlier.
	 */
	public function getDuration()
	{
		return $this->_duration;
	}

	/**
	 * @param integer number of seconds that the data can remain in cache. If 0, it means data is not cached.
	 * @throws TInvalidDataValueException if the value is smaller than 0.
	 */
	public function setDuration($value)
	{
		if(($value=TPropertyValue::ensureInteger($value))<0)
			throw new TInvalidDataValueException('outputcache_duration_invalid',get_class($this));
		$this->_duration=$value;
	}

	/**
	 * @return string a semicolon-separated list of strings used to vary the output cache. Defaults to ''.
	 */
	public function getVaryByParam()
	{
		return $this->_varyByParam;
	}

	/**
	 * Sets the names of the request parameters that should be used in calculating the cache key.
	 * The names should be concatenated by semicolons.
	 * By setting this value, the output cache will use different cached data
	 * for each different set of request parameter values.
	 * @return string a semicolon-separated list of strings used to vary the output cache.
	 */
	public function setVaryByParam($value)
	{
		$this->_varyByParam=trim($value);
	}

	/**
	 * Renders the output cache control.
	 * This method overrides the parent implementation by capturing the output
	 * from its child controls and saving it into cache, if output cache is needed.
	 * @param THtmlWriter
	 */
	public function render($writer)
	{
		if($this->_dataCached)
			$writer->write($this->_contents);
		else if($this->_cacheAvailable)
		{
			$textWriter=new TTextWriter;

			$stack=$this->getPage()->getCachingStack();
			$stack->push($this);
			parent::render(new THtmlWriter($textWriter));
			$stack->pop();

			$content=$textWriter->flush();
			$data=array($content,$this->_state,$this->_actions);
			$this->_cache->set($this->getCacheKey(),$data,$this->getDuration(),$this->getCacheDependency());
			$writer->write($content);
		}
		else
			parent::render($writer);
	}
}

?>