* @link http://www.pradosoft.com/ * @copyright Copyright © 2005-2014 PradoSoft * @license http://www.pradosoft.com/license/ * @package System.Caching */ Prado::using('System.Collections.TList'); /** * TCache class * * TCache is the base class for cache classes with different cache storage implementation. * * TCache implements the interface {@link ICache} with the following methods, * - {@link get} : retrieve the value with a key (if any) from cache * - {@link set} : store the value with a key into cache * - {@link add} : store the value only if cache does not have this key * - {@link delete} : delete the value with the specified key from cache * - {@link flush} : delete all values from cache * * Each value is associated with an expiration time. The {@link get} operation * ensures that any expired value will not be returned. The expiration time by * the number of seconds. A expiration time 0 represents never expire. * * By definition, cache does not ensure the existence of a value * even if it never expires. Cache is not meant to be an persistent storage. * * Child classes must implement the following methods: * - {@link getValue} * - {@link setValue} * - {@link addValue} * - {@link deleteValue} * and optionally {@link flush} * * Since version 3.1.2, TCache implements the ArrayAccess interface such that * the cache acts as an array. * * @author Qiang Xue * @package System.Caching * @since 3.0 */ abstract class TCache extends TModule implements ICache, ArrayAccess { private $_prefix=null; private $_primary=true; /** * Initializes the cache module. * This method initializes the cache key prefix and registers the cache module * with the application if the cache is primary. * @param TXmlElement the module configuration */ public function init($config) { if($this->_prefix===null) $this->_prefix=$this->getApplication()->getUniqueID(); if($this->_primary) { if($this->getApplication()->getCache()===null) $this->getApplication()->setCache($this); else throw new TConfigurationException('cache_primary_duplicated',get_class($this)); } } /** * @return boolean whether this cache module is used as primary/system cache. * A primary cache is used by PRADO core framework to cache data such as * parsed templates, themes, etc. */ public function getPrimaryCache() { return $this->_primary; } /** * @param boolean whether this cache module is used as primary/system cache. Defaults to false. * @see getPrimaryCache */ public function setPrimaryCache($value) { $this->_primary=TPropertyValue::ensureBoolean($value); } /** * @return string a unique prefix for the keys of cached values. * If it is not explicitly set, it will take the value of {@link TApplication::getUniqueID}. */ public function getKeyPrefix() { return $this->_prefix; } /** * @param string a unique prefix for the keys of cached values */ public function setKeyPrefix($value) { $this->_prefix=$value; } /** * @param string a key identifying a value to be cached * @return sring a key generated from the provided key which ensures the uniqueness across applications */ protected function generateUniqueKey($key) { return md5($this->_prefix.$key); } /** * Retrieves a value from cache with a specified key. * @param string a key identifying the cached value * @return mixed the value stored in cache, false if the value is not in the cache or expired. */ public function get($id) { if(($data=$this->getValue($this->generateUniqueKey($id)))!==false) { if(!is_array($data)) return false; if(!($data[1] instanceof ICacheDependency) || !$data[1]->getHasChanged()) return $data[0]; } return false; } /** * Stores a value identified by a key into cache. * If the cache already contains such a key, the existing value and * expiration time will be replaced with the new ones. If the value is * empty, the cache key will be deleted. * * @param string the key identifying the value to be cached * @param mixed the value to be cached * @param integer the number of seconds in which the cached value will expire. 0 means never expire. * @param ICacheDependency dependency of the cached item. If the dependency changes, the item is labeled invalid. * @return boolean true if the value is successfully stored into cache, false otherwise */ public function set($id,$value,$expire=0,$dependency=null) { if(empty($value) && $expire === 0) $this->delete($id); else { $data=array($value,$dependency); return $this->setValue($this->generateUniqueKey($id),$data,$expire); } } /** * Stores a value identified by a key into cache if the cache does not contain this key. * Nothing will be done if the cache already contains the key or if value is empty. * @param string the key identifying the value to be cached * @param mixed the value to be cached * @param integer the number of seconds in which the cached value will expire. 0 means never expire. * @param ICacheDependency dependency of the cached item. If the dependency changes, the item is labeled invalid. * @return boolean true if the value is successfully stored into cache, false otherwise */ public function add($id,$value,$expire=0,$dependency=null) { if(empty($value) && $expire === 0) return false; $data=array($value,$dependency); return $this->addValue($this->generateUniqueKey($id),$data,$expire); } /** * Deletes a value with the specified key from cache * @param string the key of the value to be deleted * @return boolean if no error happens during deletion */ public function delete($id) { return $this->deleteValue($this->generateUniqueKey($id)); } /** * Deletes all values from cache. * Be careful of performing this operation if the cache is shared by multiple applications. * Child classes may implement this method to realize the flush operation. * @throws TNotSupportedException if this method is not overridden by child classes */ public function flush() { throw new TNotSupportedException('cache_flush_unsupported'); } /** * Retrieves a value from cache with a specified key. * This method should be implemented by child classes to store the data * in specific cache storage. The uniqueness and dependency are handled * in {@link get()} already. So only the implementation of data retrieval * is needed. * @param string a unique key identifying the cached value * @return string the value stored in cache, false if the value is not in the cache or expired. */ abstract protected function getValue($key); /** * Stores a value identified by a key in cache. * This method should be implemented by child classes to store the data * in specific cache storage. The uniqueness and dependency are handled * in {@link set()} already. So only the implementation of data storage * is needed. * * @param string the key identifying the value to be cached * @param string the value to be cached * @param integer the number of seconds in which the cached value will expire. 0 means never expire. * @return boolean true if the value is successfully stored into cache, false otherwise */ abstract protected function setValue($key,$value,$expire); /** * Stores a value identified by a key into cache if the cache does not contain this key. * This method should be implemented by child classes to store the data * in specific cache storage. The uniqueness and dependency are handled * in {@link add()} already. So only the implementation of data storage * is needed. * * @param string the key identifying the value to be cached * @param string the value to be cached * @param integer the number of seconds in which the cached value will expire. 0 means never expire. * @return boolean true if the value is successfully stored into cache, false otherwise */ abstract protected function addValue($key,$value,$expire); /** * Deletes a value with the specified key from cache * This method should be implemented by child classes to delete the data from actual cache storage. * @param string the key of the value to be deleted * @return boolean if no error happens during deletion */ abstract protected function deleteValue($key); /** * Returns whether there is a cache entry with a specified key. * This method is required by the interface ArrayAccess. * @param string a key identifying the cached value * @return boolean */ public function offsetExists($id) { return $this->get($id) !== false; } /** * Retrieves the value from cache with a specified key. * This method is required by the interface ArrayAccess. * @param string a key identifying the cached value * @return mixed the value stored in cache, false if the value is not in the cache or expired. */ public function offsetGet($id) { return $this->get($id); } /** * Stores the value identified by a key into cache. * If the cache already contains such a key, the existing value will be * replaced with the new ones. To add expiration and dependencies, use the set() method. * This method is required by the interface ArrayAccess. * @param string the key identifying the value to be cached * @param mixed the value to be cached */ public function offsetSet($id, $value) { $this->set($id, $value); } /** * Deletes the value with the specified key from cache * This method is required by the interface ArrayAccess. * @param string the key of the value to be deleted * @return boolean if no error happens during deletion */ public function offsetUnset($id) { $this->delete($id); } } /** * TCacheDependency class. * * TCacheDependency is the base class implementing {@link ICacheDependency} interface. * Descendant classes must implement {@link getHasChanged()} to provide * actual dependency checking logic. * * The property value of {@link getHasChanged HasChanged} tells whether * the dependency is changed or not. * * You may disable the dependency checking by setting {@link setEnabled Enabled} * to false. * * Note, since the dependency objects often need to be serialized so that * they can persist across requests, you may need to implement __sleep() and * __wakeup() if the dependency objects contain resource handles which are * not serializable. * * Currently, the following dependency classes are provided in the PRADO release: * - {@link TFileCacheDependency}: checks whether a file is changed or not * - {@link TDirectoryCacheDependency}: checks whether a directory is changed or not * - {@link TGlobalStateCacheDependency}: checks whether a global state is changed or not * - {@link TChainedCacheDependency}: checks whether any of a list of dependencies is changed or not * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ abstract class TCacheDependency extends TComponent implements ICacheDependency { } /** * TFileCacheDependency class. * * TFileCacheDependency performs dependency checking based on the * last modification time of the file specified via {@link setFileName FileName}. * The dependency is reported as unchanged if and only if the file's * last modification time remains unchanged. * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ class TFileCacheDependency extends TCacheDependency { private $_fileName; private $_timestamp; /** * Constructor. * @param string name of the file whose change is to be checked. */ public function __construct($fileName) { $this->setFileName($fileName); } /** * @return string the name of the file whose change is to be checked */ public function getFileName() { return $this->_fileName; } /** * @param string the name of the file whose change is to be checked */ public function setFileName($value) { $this->_fileName=$value; $this->_timestamp=@filemtime($value); } /** * @return int the last modification time of the file */ public function getTimestamp() { return $this->_timestamp; } /** * Performs the actual dependency checking. * This method returns true if the last modification time of the file is changed. * @return boolean whether the dependency is changed or not. */ public function getHasChanged() { return @filemtime($this->_fileName)!==$this->_timestamp; } } /** * TDirectoryCacheDependency class. * * TDirectoryCacheDependency performs dependency checking based on the * modification time of the files contained in the specified directory. * The directory being checked is specified via {@link setDirectory Directory}. * * By default, all files under the specified directory and subdirectories * will be checked. If the last modification time of any of them is changed * or if different number of files are contained in a directory, the dependency * is reported as changed. By specifying {@link setRecursiveCheck RecursiveCheck} * and {@link setRecursiveLevel RecursiveLevel}, one can limit the checking * to a certain depth of the subdirectories. * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ class TDirectoryCacheDependency extends TCacheDependency { private $_recursiveCheck=true; private $_recursiveLevel=-1; private $_timestamps; private $_directory; /** * Constructor. * @param string the directory to be checked */ public function __construct($directory) { $this->setDirectory($directory); } /** * @return string the directory to be checked */ public function getDirectory() { return $this->_directory; } /** * @param string the directory to be checked * @throws TInvalidDataValueException if the directory does not exist */ public function setDirectory($directory) { if(($path=realpath($directory))===false || !is_dir($path)) throw new TInvalidDataValueException('directorycachedependency_directory_invalid',$directory); $this->_directory=$path; $this->_timestamps=$this->generateTimestamps($path); } /** * @return boolean whether the subdirectories of the directory will also be checked. * It defaults to true. */ public function getRecursiveCheck() { return $this->_recursiveCheck; } /** * @param boolean whether the subdirectories of the directory will also be checked. */ public function setRecursiveCheck($value) { $this->_recursiveCheck=TPropertyValue::ensureBoolean($value); } /** * @return int the depth of the subdirectories to be checked. * It defaults to -1, meaning unlimited depth. */ public function getRecursiveLevel() { return $this->_recursiveLevel; } /** * Sets a value indicating the depth of the subdirectories to be checked. * This is meaningful only when {@link getRecursiveCheck RecursiveCheck} * is true. * @param int the depth of the subdirectories to be checked. * If the value is less than 0, it means unlimited depth. * If the value is 0, it means checking the files directly under the specified directory. */ public function setRecursiveLevel($value) { $this->_recursiveLevel=TPropertyValue::ensureInteger($value); } /** * Performs the actual dependency checking. * This method returns true if the directory is changed. * @return boolean whether the dependency is changed or not. */ public function getHasChanged() { return $this->generateTimestamps($this->_directory)!=$this->_timestamps; } /** * Checks to see if the file should be checked for dependency. * This method is invoked when dependency of the whole directory is being checked. * By default, it always returns true, meaning the file should be checked. * You may override this method to check only certain files. * @param string the name of the file that may be checked for dependency. * @return boolean whether this file should be checked. */ protected function validateFile($fileName) { return true; } /** * Checks to see if the specified subdirectory should be checked for dependency. * This method is invoked when dependency of the whole directory is being checked. * By default, it always returns true, meaning the subdirectory should be checked. * You may override this method to check only certain subdirectories. * @param string the name of the subdirectory that may be checked for dependency. * @return boolean whether this subdirectory should be checked. */ protected function validateDirectory($directory) { return true; } /** * Determines the last modification time for files under the directory. * This method may go recursively into subdirectories if * {@link setRecursiveCheck RecursiveCheck} is set true. * @param string the directory name * @param int level of the recursion * @return array list of file modification time indexed by the file path */ protected function generateTimestamps($directory,$level=0) { if(($dir=opendir($directory))===false) throw new TIOException('directorycachedependency_directory_invalid',$directory); $timestamps=array(); while(($file=readdir($dir))!==false) { $path=$directory.DIRECTORY_SEPARATOR.$file; if($file==='.' || $file==='..') continue; else if(is_dir($path)) { if(($this->_recursiveLevel<0 || $level<$this->_recursiveLevel) && $this->validateDirectory($path)) $timestamps=array_merge($this->generateTimestamps($path,$level+1)); } else if($this->validateFile($path)) $timestamps[$path]=filemtime($path); } closedir($dir); return $timestamps; } } /** * TGlobalStateCacheDependency class. * * TGlobalStateCacheDependency checks if a global state is changed or not. * If the global state is changed, the dependency is reported as changed. * To specify which global state this dependency should check with, * set {@link setStateName StateName} to the name of the global state. * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ class TGlobalStateCacheDependency extends TCacheDependency { private $_stateName; private $_stateValue; /** * Constructor. * @param string the name of the global state */ public function __construct($name) { $this->setStateName($name); } /** * @return string the name of the global state */ public function getStateName() { return $this->_stateName; } /** * @param string the name of the global state * @see TApplication::setGlobalState */ public function setStateName($value) { $this->_stateName=$value; $this->_stateValue=Prado::getApplication()->getGlobalState($value); } /** * Performs the actual dependency checking. * This method returns true if the specified global state is changed. * @return boolean whether the dependency is changed or not. */ public function getHasChanged() { return $this->_stateValue!==Prado::getApplication()->getGlobalState($this->_stateName); } } /** * TChainedCacheDependency class. * * TChainedCacheDependency represents a list of cache dependency objects * and performs the dependency checking based on the checking results of * these objects. If any of them reports a dependency change, TChainedCacheDependency * will return true for the checking. * * To add dependencies to TChainedCacheDependency, use {@link getDependencies Dependencies} * which gives a {@link TCacheDependencyList} instance and can be used like an array * (see {@link TList} for more details}). * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ class TChainedCacheDependency extends TCacheDependency { private $_dependencies=null; /** * @return TCacheDependencyList list of dependency objects */ public function getDependencies() { if($this->_dependencies===null) $this->_dependencies=new TCacheDependencyList; return $this->_dependencies; } /** * Performs the actual dependency checking. * This method returns true if any of the dependency objects * reports a dependency change. * @return boolean whether the dependency is changed or not. */ public function getHasChanged() { if($this->_dependencies!==null) { foreach($this->_dependencies as $dependency) if($dependency->getHasChanged()) return true; } return false; } } /** * TApplicationStateCacheDependency class. * * TApplicationStateCacheDependency performs dependency checking based on * the mode of the currently running PRADO application. * The dependency is reportedly as unchanged if and only if the application * is running in performance mode. * * You may chain this dependency together with other dependencies * so that only when the application is not in performance mode the other dependencies * will be checked. * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ class TApplicationStateCacheDependency extends TCacheDependency { /** * Performs the actual dependency checking. * This method returns true if the currently running application is not in performance mode. * @return boolean whether the dependency is changed or not. */ public function getHasChanged() { return Prado::getApplication()->getMode()!==TApplicationMode::Performance; } } /** * TCacheDependencyList class. * * TCacheDependencyList represents a list of cache dependency objects. * Only objects implementing {@link ICacheDependency} can be added into this list. * * TCacheDependencyList can be used like an array. See {@link TList} * for more details. * * @author Qiang Xue * @package System.Caching * @since 3.1.0 */ class TCacheDependencyList extends TList { /** * Inserts an item at the specified position. * This overrides the parent implementation by performing additional type checking * for each newly added item. * @param integer the specified position. * @param mixed new item * @throws TInvalidDataTypeException if the item to be inserted is not a dependency instance */ public function insertAt($index,$item) { if($item instanceof ICacheDependency) parent::insertAt($index,$item); else throw new TInvalidDataTypeException('cachedependencylist_cachedependency_required'); } }