Basic Page Cache

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Basic Page Cache

Post 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 )
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post 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;
        }
Post Reply