Page 1 of 1

Basic Page Cache

Posted: Wed Aug 23, 2006 7:42 pm
by Chris Corbyn
This is just a bi-product of something I used to practise TDD with.

Code: Select all

<?php

/**
 * A very basic web page caching system
 * Stores pages in a cache, checks expiry times
 * and dumps the cache contents back.
 * @author Chris Corbyn
 * @license Lesser GNU Public License
 */
class PageCache
{
	/**
	 * The directory where the cache resides
	 * @var string path
	 */
	protected $cacheDirectory = '/tmp';
	/**
	 * The name of the current page as specified by the developer
	 * @var string page name
	 */
	protected $page;
	/**
	 * Open file handle
	 * @var resource file
	 */
	protected $handle;
	/**
	 * The cache timeout in seconds
	 * @var int timeout
	 */
	protected $timeout;
	
	/**
	 * Constructor
	 * @param string page name
	 */
	public function __construct($pagename)
	{
		$this->page = $pagename;
	}
	/**
	 * Specify a maximum age in seconds for cached pages
	 * @param int seconds
	 */
	public function setTimeout($seconds)
	{
		$this->timeout = (int) $seconds;
	}
	/**
	 * Set the directory to use to cache pages
	 * @var string path
	 */
	public function setCacheDirectory($path)
	{
		$this->cacheDirectory = $path;
	}
	/**
	 * Check if the cache is writable
	 * @return boolean
	 */
	public function isWritable()
	{
		return is_writable($this->cacheDirectory);
	}
	/**
	 * Start output buffering to catch the page contents
	 * Open the file ready for writing
	 */
	public function startCaching()
	{
		if ($this->isWritable())
		{
			ob_start();
			$this->handle = fopen($this->getCachedFilePath(), 'w+');
		}
	}
	/**
	 * Commit the contents of the buffer to the cache
	 */
	public function write()
	{
		if ($this->handle)
		{
			$buffer = ob_get_clean();
			fwrite($this->handle, $buffer);
		}
	}
	/**
	 * Check if the page we have open is already cached
	 * @return boolean
	 */
	public function isCached()
	{
		return file_exists($this->getCachedFilePath());
	}
	/**
	 * Get the contents of the already cached page
	 * @return string page data
	 */
	public function getContents()
	{
		if ($this->isCached())
		{
			return file_get_contents($this->getCachedFilePath());
		}
	}
	/**
	 * Print the contents of the cached page to the screen
	 */
	public function dump()
	{
		echo $this->getContents();
	}
	/**
	 * Get the path the file which we're working with
	 * @access private
	 * @return string path
	 */
	private function getCachedFilePath()
	{
		return $this->cacheDirectory.'/'.$this->page;
	}
	/**
	 * Get the age in seconds of the already cached file
	 * @return int age
	 */
	public function getAge()
	{
		if ($this->isCached())
		{
			return time() - filemtime($this->getCachedFilePath());
		}
		else return 0;
	}
	/**
	 * Check if the currently cached file is out of date
	 * @return boolean
	 */
	public function isExpired()
	{
		return ($this->timeout <= $this->getAge());
	}
	/**
	 * Remove old cache data for this page
	 */
	public function cleanUp()
	{
		if ($this->isCached() && $this->isWritable())
		{
			unlink($this->getCachedFilePath());
		}
	}
}

?>
And the tests that drove the code (for anyone who cares :P)....

Code: Select all

Mock::GeneratePartial('PageCache', 'PartialPageCache', array('getAge'));

class TestOfPageCache extends UnitTestCase
{
	public function testCacheIsWritable()
	{
		$cache = new PageCache('mypage');
		$cache->setCacheDirectory('./cache');
		$this->assertTrue($cache->isWritable());
		@fopen('./cache/test.txt', 'w+');
		$this->assertTrue(file_exists('./cache/test.txt'));
		@unlink('./cache/test.txt');
		$this->assertFalse(file_exists('./cache/test.txt'));
	}
	
	public function testPageCacheFileIsCreated()
	{
		$cache = new PageCache('mypage');
		$cache->setCacheDirectory('./cache');
		$this->assertTrue($cache->isWritable());
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		@unlink('./cache/mypage');
		$this->assertFalse(file_exists('./cache/mypage'));
	}
	
	public function testCacheContent()
	{
		$cache = new PageCache('mypage');
		$cache->setCacheDirectory('./cache');
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		
		$test_output = "Some <strong>test</strong> output";
		echo $test_output;
		
		$cache->write();
		
		$this->assertEqual($test_output, $cache->getContents());
		
		@unlink('./cache/mypage');
		$this->assertFalse(file_exists('./cache/mypage'));
	}
	
	public function testCacheTimeout()
	{
		$cache = new PartialPageCache($this);
		$cache->__construct('mypage');
		$cache->setCacheDirectory('./cache');
		$cache->setTimeout(60 * 60); //1 hour in seconds
		
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		$test_output = "Dummy";
		echo $test_output;
		$cache->write();
		$cache->setReturnValue('getAge', 60 * 60 + 10); //10 seconds expired
		$this->assertTrue($cache->isExpired());
		@unlink('./cache/mypage');
		$this->assertFalse(file_exists('./cache/mypage'));
		unset($cache);
		
		$cache = new PartialPageCache($this);
		$cache->__construct('mypage');
		$cache->setCacheDirectory('./cache');
		$cache->setTimeout(60 * 60); //1 hour in seconds
		
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		$test_output = "Dummy";
		echo $test_output;
		$cache->write();
		$cache->setReturnValue('getAge', 60 * 60 - 10); //10 seconds remaining
		$this->assertFalse($cache->isExpired());
		@unlink('./cache/mypage');
		$this->assertFalse(file_exists('./cache/mypage'));
		
		$cache = new PageCache('mypage');
		$cache->setCacheDirectory('./cache');
		$cache->setTimeout(60 * 60); //1 hour in seconds
		
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		$test_output = "Dummy";
		echo $test_output;
		$cache->write();
		$this->assertFalse($cache->isExpired());
		@unlink('./cache/mypage');
		$this->assertFalse(file_exists('./cache/mypage'));
	}
	
	public function testDumpOfCacheData()
	{
		$cache = new PageCache('mypage');
		$cache->setCacheDirectory('./cache');
		$cache->setTimeout(60 * 60); //1 hour in seconds
		
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		$test_output = "Dummy";
		echo $test_output;
		$cache->write();
		$this->assertFalse($cache->isExpired());
		
		ob_start();
		if (!$cache->isExpired())
		{
			$cache->dump();
		}
		$cache_data = ob_get_clean();
		
		$this->assertEqual($cache_data, $test_output);
		
		@unlink('./cache/mypage');
		$this->assertFalse(file_exists('./cache/mypage'));
	}
	
	public function testOfCleaning()
	{
		$cache = new PartialPageCache($this);
		$cache->__construct('mypage');
		$cache->setCacheDirectory('./cache');
		$cache->setTimeout(60 * 60); //1 hour in seconds
		
		$cache->startCaching();
		$this->assertTrue(file_exists('./cache/mypage'));
		$test_output = "Dummy";
		echo $test_output;
		$cache->write();
		$cache->setReturnValue('getAge', 60 * 60 + 10); //10 seconds expired
		$this->assertTrue($cache->isExpired());
		
		if ($cache->isExpired())
		{
			$cache->cleanUp();
		}
		$this->assertFalse(file_exists('./cache/mypage'));
	}
}
A basic example (maybe in a controller):

Code: Select all

$cache = new PageCache($this->page);
$cache->setTimeout($this->cacheTimeout);
if ($cache->isCached() && !$cache->isExpired())
{
    $cache->dump();
}
else
{
    $cache->startCaching();
    
    $this->pseudoSendYourPageOutput();
   
    $cache->write(); 
}
I guess you could use register_shutdown_function() if it's not feasible to call write() manually.

(Ok, I really need some sleep 8O )

Posted: Thu Aug 24, 2006 4:15 am
by Jenk
For pure semantics, you could use the realpath() function on the setter for path; and a quick check that is does exist.

Code: Select all

/** 
         * Set the directory to use to cache pages 
         * @var string path 
         * @return bool directory exists
         */ 
        public function setCacheDirectory($path) 
        { 
                if (!$path = realpath($path)) return false;

                $this->cacheDirectory = $path;
                return true;
        }