* @link http://www.pradosoft.com/
* @copyright Copyright © 2005-2014 PradoSoft
* @license http://www.pradosoft.com/license/
* @package Prado\Web\UI\WebControls
*/
namespace Prado\Web\UI\WebControls;
use Prado\TPropertyValue;
use Prado\Exceptions\TInvalidDataValueException;
/**
* 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.,
*
*
* content to be cached
*
*
* 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 cache dependency.
* The former specifies the number of seconds that the data can remain
* valid in cache (defaults to 60s), while the latter specifies conditions
* that the cached data depends on. If a dependency changes,
* (e.g. relevant data in DB are updated), the cached data will be invalidated.
*
* There are two ways to specify cache dependency. One may write event handlers
* to respond to the {@link onCheckDependency OnCheckDependency} event and set
* the event parameter's {@link TOutputCacheCheckDependencyEventParameter::getIsValid IsValid}
* property to indicate whether the cached data remains valid or not.
* One can also extend TOutputCache and override its {@link getCacheDependency}
* function. While the former is easier to use, the latter offers more extensibility.
*
* 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.
* The content being cached may also be variated with user sessions if
* {@link setVaryBySession VaryBySession} is set true.
* 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
* @package Prado\Web\UI\WebControls
* @since 3.1
*/
class TOutputCache extends \Prado\Web\UI\TControl implements \Prado\Web\UI\INamingContainer
{
const CACHE_ID_PREFIX='prado:outputcache';
private $_cacheModuleID='';
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='';
private $_varyBySession=false;
private $_cachePostBack=false;
private $_cacheTime=0;
/**
* 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->_duration>0 && ($this->_cachePostBack || !$this->getPage()->getIsPostBack()))
{
if($this->_cacheModuleID!=='')
{
$this->_cache=$this->getApplication()->getModule($this->_cacheModuleID);
if(!($this->_cache instanceof ICache))
throw new TConfigurationException('outputcache_cachemoduleid_invalid',$this->_cacheModuleID);
}
else
$this->_cache=$this->getApplication()->getCache();
if($this->_cache!==null)
{
$this->_cacheAvailable=true;
$data=$this->_cache->get($this->getCacheKey());
if(is_array($data))
{
$param=new TOutputCacheCheckDependencyEventParameter;
$param->setCacheTime(isset($data[3])?$data[3]:0);
$this->onCheckDependency($param);
$this->_dataCached=$param->getIsValid();
}
else
$this->_dataCached=false;
if($this->_dataCached)
list($this->_contents,$this->_state,$this->_actions,$this->_cacheTime)=$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);
}
public 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}.
* If {@link getVaryBySession VaryBySession} is true, the session ID
* will also participate in the key calculation.
* This method may be overriden to support other variations in
* the calculated cache key.
* @return string cache key
*/
protected function calculateCacheKey()
{
$key=$this->getBaseCacheKey();
if($this->_varyBySession)
$key.=$this->getSession()->getSessionID();
if($this->_varyByParam!=='')
{
$params=array();
$request=$this->getRequest();
foreach(explode(',',$this->_varyByParam) as $name)
{
$name=trim($name);
$params[$name]=$request->itemAt($name);
}
$key.=serialize($params);
}
$param=new TOutputCacheCalculateKeyEventParameter;
$this->onCalculateKey($param);
$key.=$param->getCacheKey();
return $key;
}
/**
* @return string basic cache key without variations
*/
protected function getBaseCacheKey()
{
return self::CACHE_ID_PREFIX.$this->_keyPrefix.$this->getPage()->getPagePath().$this->getUniqueID();
}
/**
* @return string the ID of the cache module. Defaults to '', meaning the primary cache module is used.
*/
public function getCacheModuleID()
{
return $this->_cacheModuleID;
}
/**
* @param string the ID of the cache module. If empty, the primary cache module will be used.
*/
public function setCacheModuleID($value)
{
$this->_cacheModuleID=$value;
}
/**
* 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;
}
/**
* @return integer the timestamp of the cached content. This is only valid if the content is being cached.
* @since 3.1.1
*/
public function getCacheTime()
{
return $this->_cacheTime;
}
/**
* 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);
}
/**
* @return boolean whether the content being cached should be differentiated according to user sessions. Defaults to false.
*/
public function getVaryBySession()
{
return $this->_varyBySession;
}
/**
* @param boolean whether the content being cached should be differentiated according to user sessions.
*/
public function setVaryBySession($value)
{
$this->_varyBySession=TPropertyValue::ensureBoolean($value);
}
/**
* @return boolean whether cached output will be used on postback requests. Defaults to false.
*/
public function getCachingPostBack()
{
return $this->_cachePostBack;
}
/**
* Sets a value indicating whether cached output will be used on postback requests.
* By default, this is disabled. Be very cautious when enabling it.
* If the cached content including interactive user controls such as
* TTextBox, TDropDownList, your page may fail to render on postbacks.
* @param boolean whether cached output will be used on postback requests.
*/
public function setCachingPostBack($value)
{
$this->_cachePostBack=TPropertyValue::ensureBoolean($value);
}
/**
* This event is raised when the output cache is checking cache dependency.
* An event handler may be written to check customized dependency conditions.
* The checking result should be saved by setting {@link TOutputCacheCheckDependencyEventParameter::setIsValid IsValid}
* property of the event parameter (which defaults to true).
* @param TOutputCacheCheckDependencyEventParameter event parameter
*/
public function onCheckDependency($param)
{
$this->raiseEvent('OnCheckDependency',$this,$param);
}
/**
* This event is raised when the output cache is calculating cache key.
* By varying cache keys, one can obtain different versions of cached content.
* An event handler may be written to add variety of the key calculation.
* The value set in {@link TOutputCacheCalculateKeyEventParameter::setCacheKey CacheKey} of
* this event parameter will be appended to the default key calculation scheme.
* @param TOutputCacheCalculateKeyEventParameter event parameter
*/
public function onCalculateKey($param)
{
$this->raiseEvent('OnCalculateKey',$this,$param);
}
/**
* 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();
$multiwriter = new TOutputCacheTextWriterMulti(array($writer->getWriter(),$textwriter));
$htmlWriter = Prado::createComponent($this->GetResponse()->getHtmlWriterType(), $multiwriter);
$stack=$this->getPage()->getCachingStack();
$stack->push($this);
parent::render($htmlWriter);
$stack->pop();
$content=$textwriter->flush();
$data=array($content,$this->_state,$this->_actions,time());
$this->_cache->set($this->getCacheKey(),$data,$this->getDuration(),$this->getCacheDependency());
}
else
parent::render($writer);
}
}