From f76f11a2cb228b4161bd84d9856c148cb49f1bd7 Mon Sep 17 00:00:00 2001 From: xue <> Date: Sun, 3 Dec 2006 22:57:44 +0000 Subject: added cache dependency classes. --- .gitattributes | 2 + HISTORY | 7 +- framework/Caching/TCache.php | 439 ++++++++++++++++++++- framework/Exceptions/messages.txt | 3 + framework/interfaces.php | 12 +- .../unit/Caching/TDirectoryCacheDependencyTest.php | 84 ++++ tests/unit/Caching/TFileCacheDependencyTest.php | 60 +++ 7 files changed, 598 insertions(+), 9 deletions(-) create mode 100644 tests/unit/Caching/TDirectoryCacheDependencyTest.php create mode 100644 tests/unit/Caching/TFileCacheDependencyTest.php diff --git a/.gitattributes b/.gitattributes index 751393fa..b2dcbe05 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2338,6 +2338,8 @@ tests/test_tools/simpletest/reflection_php5.php -text tests/test_tools/simpletest/selector.php -text tests/test_tools/simpletest/simpletest.php -text tests/test_tools/simpletest/test_case.php -text +tests/unit/Caching/TDirectoryCacheDependencyTest.php -text +tests/unit/Caching/TFileCacheDependencyTest.php -text tests/unit/Collections/TListTest.php -text tests/unit/Collections/TMapTest.php -text tests/unit/Data/TDbCommandTest.php -text diff --git a/HISTORY b/HISTORY index 7c197661..57727471 100644 --- a/HISTORY +++ b/HISTORY @@ -1,9 +1,11 @@ Version 3.1.0 To be released ============================ BUG: Ticket#188 - Postback js caused controls not inheritable (Qiang) -ENH: Ticket#180, #222 - Prado js files are not rendered at the beginning of the form (Qiang) ENH: Ticket#99 - TXmlTransform control (Knut) ENH: Ticket#117 - added support to configure subproperties in a group. (Qiang) +ENH: Ticket#180, #222 - Prado js files are not rendered at the beginning of the form (Qiang) +CHG: All validators ClientSide.OnSuccess becomes ClientSide.OnValidationSuccess, and OnSuccess event becomes OnValidationSuccess. (Wei) +CHG: All validators ClientSide.OnError becomes ClientSide.OnValidationError, and OnError event becomes OnValidationError. (Wei) NEW: Ajax support and active controls (Wei) NEW: Data abstraction layer based on PDO (Qiang) NEW: SQLMap (Wei) @@ -12,8 +14,7 @@ NEW: TQueue (Qiang) NEW: TSessionPageStatePersister (Qiang) NEW: TFeedService, TRssFeedDocument (Knut, Qiang) NEW: TJsonService -CHG: All validators ClientSide.OnSuccess becomes ClientSide.OnValidationSuccess, and OnSuccess event becomes OnValidationSuccess. (Wei) -CHG: All validators ClientSide.OnError becomes ClientSide.OnValidationError, and OnError event becomes OnValidationError. (Wei) +NEW: TCacheDependency, TFileCacheDependency, TDirectoryCacheDependency, TGlobalStateCacheDependency, TChainedCacheDependency Version 3.0.6 December 4, 2006 ============================== diff --git a/framework/Caching/TCache.php b/framework/Caching/TCache.php index d974fe83..ca7348d4 100644 --- a/framework/Caching/TCache.php +++ b/framework/Caching/TCache.php @@ -1,6 +1,6 @@ * @link http://www.pradosoft.com/ @@ -10,6 +10,8 @@ * @package System.Caching */ +Prado::using('System.Collections.TList'); + /** * TCache class * @@ -229,4 +231,439 @@ abstract class TCache extends TModule implements ICache abstract protected function deleteValue($key); } + +/** + * TCacheDependency class. + * + * TCacheDependency is the base class implementing {@link ICacheDependency} interface. + * Descendant classes must implement {@link checkChanges()} 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 + * @version $Id $ + * @package System.Caching + * @since 3.1.0 + */ +abstract class TCacheDependency extends TComponent implements ICacheDependency +{ + private $_enabled=true; + + /** + * @return boolean whether this dependency is enabled. Default value is true. + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * Sets a value indicating whether this cache dependency is enabled or not. + * Cache dependency checking is only performed when it is enabled. + * @param boolean whether this dependency is to be enabled. + */ + public function setEnabled($value) + { + $this->_enabled=TPropertyValue::ensureBoolean($value); + } + + /** + * @return boolean whether the dependency is changed or not. + * If the dependency checking is disabled, it always returns false. + */ + public function getHasChanged() + { + return $this->_enabled ? $this->checkChanges() : false; + } + + /** + * Performs the actual dependency checking. + * This method must be implemented by child classes. + * @return boolean whether the dependency is changed or not. + */ + abstract protected function checkChanges(); +} + + +/** + * 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 + * @version $Id $ + * @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. + */ + protected function checkChanges() + { + 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 + * @version $Id $ + * @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. + */ + protected function checkChanges() + { + 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 recurisvely 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 + * @version $Id $ + * @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. + */ + protected function checkChanges() + { + return $this->_stateValue!==Prado::getApplication()->getGlobalState($value); + } +} + + +/** + * 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 + * @version $Id $ + * @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. + */ + protected function checkChanges() + { + if($this->_dependencies!==null) + { + foreach($this->_dependencies as $dependency) + if($dependency->getHasChanged()) + return true; + } + return false; + } +} + + +/** + * 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 + * @version $Id $ + * @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 speicified 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'); + } +} + ?> \ No newline at end of file diff --git a/framework/Exceptions/messages.txt b/framework/Exceptions/messages.txt index dcd3c749..d176b5f7 100644 --- a/framework/Exceptions/messages.txt +++ b/framework/Exceptions/messages.txt @@ -363,3 +363,6 @@ dbcommand_query_failed = TDbCommand failed to execute the query SQL: {0} dbcommand_column_empty = TDbCommand returned an empty result and could not obtain the scalar. dbdatareader_rewind_invalid = TDbDataReader is a forward-only stream. It can only be traversed once. dbtransaction_transaction_inactive = TDbTransaction is inactive. + +directorycachedependency_directory_invalid = TDirectoryCacheDependency.Directory {0} does not refer to a valid directory. +cachedependencylist_cachedependency_required = Only objects implementing ICacheDependency can be added into TCacheDependencyList. \ No newline at end of file diff --git a/framework/interfaces.php b/framework/interfaces.php index e5da7bcd..d0e5c6aa 100644 --- a/framework/interfaces.php +++ b/framework/interfaces.php @@ -229,6 +229,8 @@ interface ICache * This interface must be implemented by classes meant to be used as * cache dependencies. * + * Classes implementing this interface must support serialization and unserialization. + * * @author Qiang Xue * @version $Id$ * @package System @@ -282,9 +284,9 @@ interface IBindable /** * IActiveControl interface. - * + * * Active controls must implement IActiveControl interface. - * + * * @author Wei Zhuo * @version $Id$ * @package System @@ -295,15 +297,15 @@ interface IActiveControl /** * @return TBaseActiveControl Active control properties. */ - public function getActiveControl(); + public function getActiveControl(); } /** * ICallbackEventHandler interface. - * + * * If a control wants to respond to callback event, it must implement this * interface. - * + * * @author Wei Zhuo * @version $Id$ * @package System diff --git a/tests/unit/Caching/TDirectoryCacheDependencyTest.php b/tests/unit/Caching/TDirectoryCacheDependencyTest.php new file mode 100644 index 00000000..786720da --- /dev/null +++ b/tests/unit/Caching/TDirectoryCacheDependencyTest.php @@ -0,0 +1,84 @@ +assertEquals($dependency->getDirectory(),$directory); + + try + { + $dependency=new TDirectoryCacheDependency(dirname(__FILE__).'/temp2'); + $this->fail("Expected exception is not raised"); + } + catch(TInvalidDataValueException $e) + { + } + } + + public function testRecursiveCheck() + { + $directory=realpath(dirname(__FILE__).'/temp'); + $dependency=new TDirectoryCacheDependency(dirname(__FILE__).'/temp'); + $this->assertTrue($dependency->getRecursiveCheck()); + $dependency->setRecursiveCheck(false); + $this->assertFalse($dependency->getRecursiveCheck()); + } + + public function testRecursiveLevel() + { + $directory=realpath(dirname(__FILE__).'/temp'); + $dependency=new TDirectoryCacheDependency(dirname(__FILE__).'/temp'); + $this->assertEquals($dependency->getRecursiveLevel(),-1); + $dependency->setRecursiveLevel(5); + $this->assertEquals($dependency->getRecursiveLevel(),5); + } + + public function testHasChanged() + { + $tempFile=dirname(__FILE__).'/temp/foo.txt'; + @unlink($tempFile); + $fw=fopen($tempFile,"w"); + fwrite($fw,"test"); + fclose($fw); + clearstatcache(); + + $dependency=new TDirectoryCacheDependency(dirname($tempFile)); + $str=serialize($dependency); + + // test directory not changed + sleep(2); + $dependency=unserialize($str); + $this->assertFalse($dependency->getHasChanged()); + + // change file + $fw=fopen($tempFile,"w"); + fwrite($fw,"test again"); + fclose($fw); + clearstatcache(); + + // test file changed + sleep(2); + $dependency=unserialize($str); + $this->assertTrue($dependency->getHasChanged()); + + @unlink($tempFile); + } +} + +?> \ No newline at end of file diff --git a/tests/unit/Caching/TFileCacheDependencyTest.php b/tests/unit/Caching/TFileCacheDependencyTest.php new file mode 100644 index 00000000..6b61aed6 --- /dev/null +++ b/tests/unit/Caching/TFileCacheDependencyTest.php @@ -0,0 +1,60 @@ +assertEquals($dependency->getFileName(),__FILE__); + $this->assertEquals($dependency->getTimestamp(),filemtime(__FILE__)); + + $dependency=new TFileCacheDependency(dirname(__FILE__).'/foo.txt'); + $this->assertFalse($dependency->getTimestamp()); + } + + public function testHasChanged() + { + $tempFile=dirname(__FILE__).'/temp/foo.txt'; + @unlink($tempFile); + $fw=fopen($tempFile,"w"); + fwrite($fw,"test"); + fclose($fw); + clearstatcache(); + + $dependency=new TFileCacheDependency($tempFile); + $str=serialize($dependency); + + // test file not changed + sleep(2); + $dependency=unserialize($str); + $this->assertFalse($dependency->getHasChanged()); + + // change file + $fw=fopen($tempFile,"w"); + fwrite($fw,"test again"); + fclose($fw); + clearstatcache(); + + // test file changed + sleep(2); + $dependency=unserialize($str); + $this->assertTrue($dependency->getHasChanged()); + + @unlink($tempFile); + } +} + +?> \ No newline at end of file -- cgit v1.2.3