Easy to implement PHP Caching?

Hello,

One of my websites is quite database intensive something which causes my server to overload and become unresponsive in times of high traffic.

I believe that a caching system would solve this issue since the website doesn’t change very often. I am looking for an easy to implement caching solution that will work in both PHP4 and PHP5 (My server is still on PHP4 because one of my websites is running older code, but I am planning to upgrade it in a few months)

I read some tutorials on how to do this with PHP ob_* functions. I am not looking for anything too advanced and it seems that this solution would work fine. But I read that it is unreliable and that there are better solutions. Is this true? If yes, can you suggest another solution that is equally easy to implement over an existing website but which will also be more reliable?

I am not really looking for the fastest solution or one that will offer many features. A solution that will be simple, easy to implement and reliable is what is most important to me at this point.

Thank you!

I have a simple class that allows you to cache chunks of content on the file system. You’ll need to modify it slightly to work on PHP4, but nothing major.

Using it looks like this:


$cache = new Cache('/home/me/cache/');  //store cache path somewhere central!


//name of this cache file
$cfile = 'complex_report_of_stuff';

//Display from cache (if available, and <4 hrs old)
if($data = $cache->getcache($cfile, 4)) {
   echo $data;
}

//Rebuild from database if no cache file, or too old
else {
   $result = mysqli_query('SELECT *, JOIN, huge tables, etc...');
   $data = "<h2>My Data</h2><p>Build your chunk of content here</p>";

   $cache->writecache($cfile, $data);
   echo $data;
}

To explicity remove a cache file you just call:


$cache->removecache('name_of_file');

//remove all files starting with name_of_
$cache->removecache('name_of_*'); 

Here is the class:


<?php
/**
* File system caching wrapper
* @author Mike Healy
* @version 1.0.1
*/

class Cache
{
	/**
	* @var string
	*/
	protected $path;		//includes trailing slash
	
	
	/**
	* CONSTRUCTOR
	* @param string $path file system path to parent cache directory
	*/
	public function __construct($path = null) {
		
		//Default path
		//Remove if you have central config value passed in
		if($path === null) {
			$path = '/home/myusername/cache/';
		}
		
		//enforce trailing slash
		else if(substr($path, -1) != '/') {
			$path .= '/';
		}
			
		if(!file_exists($path) OR !is_dir($path)) {
			mkdir($path);
		}
		$this->path = $path;
	}
	
	
	
	/**
	* return contents of cache file (if available and fresh enough)
	* return false otherwise
	* @return mixed
	*/
	public function getcache($file, $hours = 3) {
		$file = $this->filename($file);
		$path = $this->path . $file;
		if(!file_exists($path) OR is_dir($path)) return false;
		
		//Expired cache file
		if(filemtime($path) < time()-$hours*3600) {
			$this->removecache($file);
			return false;
		}
		
		//Get contents
		if($content = file_get_contents($path)) return $content;
		return false;
	}
	
	
	
	/**
	* Remove specific cache file
	* Wildcard * supported to match any ENDING
	* @return void
	*/
	public function removecache($file) {
		$file = $this->filename($file);
		$path = $this->path . $file;
		
		//Wildcard ending 
		if(strpos($file, '*')) {
			
			$ignore = array('.', '..');
			$file = basename($path);
			$path = substr($path, 0, (strlen($file)+1)*-1);	//remove file from end of path. (+1 extends link to include trailing slash
			$file = str_replace('*', '', $file);
			
			//Open dir, check each file
			$dir = opendir($path);
			while($systemFile = readdir($dir)) {
				$filePattern = '~^' . $file . '~';
				if(!in_array($systemFile, $ignore) AND preg_match($filePattern, $systemFile)) unlink($path.'/'.$systemFile);
			}
		}
		else if(file_exists($path)) unlink($path);	
	}
	
	
	/**
	* Write cache file
	* @return bool
	*/
	public function writecache($file, $content) {
		$file = $this->filename($file);
		$path = $this->path . $file;
		if(file_put_contents($path, $content)) return true;
		return false;
	}
	
	
	/**
	* Safen up file name. All other methods use this, ensures consistent access
	* @return string
	*/
	private function filename($file) {
		$file = strtolower($file);
		$file = preg_replace('~[^a-z0-9/\\\\_\\-]~', '', $file);
		return $file;	
	}
}

Whilst it might not be the most amazing approach, one simple way I have done this is to have a MySQL table with name, value, expiry - and to cache the html directly in there, allowing it to expire after a certain time period and be regenerated.

In my case this was significantly faster than generating the content on every page load.

Thanks Vali! Some questions:

When you say “Pass all your requests through the same script”, how exactly? I was planning to include the caching code in the php scripts that generate the pages that I need cached, so basically I would only need to include this code in 2-3 php scripts max. Would that be OK?

When you say “get the output”, should I do that with the usual ob_ PHP functions, or there is a better way?

Zoom123: if you have a dedicated box, you can use something like APC or Memcached.
If not, you can use file caching.

The easiest way to do this would be this:

  • Pass all your requests through the same script (say: cache.php)
  • In that script, have a list of URLs you want cached.
  • If the request came for one of those URLs, create a KEY based on the incoming url and parameters.
    — See if a file exists for that KEY, if it does not:
    ----- run the script, get the output and cache it (in a file, apc or memcached) linked to that KEY.
    ----- return the output of that script to the user.
    — if the KEY is there, return the contents of the file / apc or memached entry.
  • Clear or timeout the cache from time to time, or when you change the DB content.

That way, if you have lets say 10 db reads for each page, and you get 1,000 requests per second, you will get 10 total db reads, instead of 10,000 reads per second.

Thanks. Does storing the html in the database instead of on a file solve the reliability issues that otherwise would (supposedly) be created by doing simple caching with the PHP ob_* functions?

Regarding object buffering, I usually found it unnecessary if you’re caching chunks of content (e.g. a user list). It could be handy for whole-page caching, but less flexible, as any customisations that are part of the page will get stored in the cache and shown to all users.

You can just build your piece of output into a variable, and cache and echo it. You can then cache only the expensive parts of a page.