Creating a Simple, Reusable PHP Cache Class

Recently for a small project, I turned one of my previous WordPress cache experiments into a simple, reusable PHP class that you can throw into any project for quick caching. It’s released under MIT license so feel free to use and modify it however you’d like!

Download

The following zip file contains the PHP class and license information.

The Cache Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class Cache {

    // Pages you do not want to Cache:
    var $doNotCache = array("admin","profile");

    // General Config Vars
    var $cacheDir = "./cache";
    var $cacheTime = 21600;
    var $caching = false;
    var $cacheFile;
    var $cacheFileName;
    var $cacheLogFile;
    var $cacheLog;

    function __construct(){
        $this->cacheFile = base64_encode($_SERVER['REQUEST_URI']);
        $this->cacheFileName = $this->cacheDir.'/'.$this->cacheFile.'.txt';
        $this->cacheLogFile = $this->cacheDir."/log.txt";
        if(!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755);
        if(file_exists($this->cacheLogFile))
            $this->cacheLog = unserialize(file_get_contents($this->cacheLogFile));
        else
            $this->cacheLog = array();
    }
   
    function start(){
        $location = array_slice(explode('/',$_SERVER['REQUEST_URI']), 2);
        if(!in_array($location[0],$this->doNotCache)){
            if(file_exists($this->cacheFileName) && (time() - filemtime($this->cacheFileName)) < $this->cacheTime && $this->cacheLog[$this->cacheFile] == 1){
                $this->caching = false;
                echo file_get_contents($this->cacheFileName);
                exit();
            }else{
                $this->caching = true;
                ob_start();
            }
        }
    }
   
    function end(){
        if($this->caching){
            file_put_contents($this->cacheFileName,ob_get_contents());
            ob_end_flush();
            $this->cacheLog[$this->cacheFile] = 1;
            if(file_put_contents($this->cacheLogFile,serialize($this->cacheLog)))
                return true;
        }
    }
   
    function purge($location){
        $location = base64_encode($location);
        $this->cacheLog[$location] = 0;
        if(file_put_contents($this->cacheLogFile,serialize($this->cacheLog)))
            return true;
        else
            return false;
    }
   
    function purge_all(){
        if(file_exists($this->cacheLogFile)){
            foreach($this->cacheLog as $key=>$value) $this->cacheLog[$key] = 0;
            if(file_put_contents($this->cacheLogFile,serialize($this->cacheLog)))
                return true;
            else
                return false;
        }
    }

}

All of the functions are fairly self-explanatory. The __construct function is called when you instantiate the class – all it does is set the variables and create (or load) the cache log file. I chose to use a log file to deal with cache creation and deletion as it reduces unnecessary calls and is a bit faster, in my opinion. If you’d rather just delete expired files, feel free to change that section to however you see fit.

The start function first checks the current URL to make sure that the base name is not in the “do not cache” list and either a) begins the cache process or b) loads the existing cache. The end function checks to see whether or not the page is being cached and if so, creates the static file and updates the cache log file.

The last two functions are used for deleting cache files. The purge function takes a location (full URL for now) and marks it for deletion, while the purge_all function marks all files for deletion. When a file is marked for deletion, the next time a user visits that page the script will create a fresh cache file (it will not be deleted in advance, simply overwritten).

How to Use the Cache Class

Using the cache class is very easy! Just call the start function at the part of your script where you’d like to start caching and the end function wherever the cache ends. For me personally, I tend to start caching after the header section (where I have dynamic elements, such as username and profile links) and end caching after the footer. I also make use of the doNotCache array to make sure my admin and profile pages are not cached. Lastly, I add the purge_all function to my admin save function, so that whenever I update the site the cache is cleared.

To start the Cache:

1
2
3
4
5
include_once("cache.php");
$cache = new Cache;
$cache->start(); // Start caching (if needed)

// Other PHP that generates your pages goes here

And to end the Cache:

1
$cache->end(); // Stop caching

Anything outside of the start and end functions are left alone, so be sure to cache wisely.

Conclusion

I wrote this class after having the need for really simple caching that didn’t require a bunch of other libraries to use. If this is useful for your project, I’d love to hear about it in the comments (it would make my day), and I really appreciate any feedback you may have! I know my PHP isn’t pro level (probably not even close) but I hope it’s good enough to use in small projects.

Lastly, if you haven’t subscribed to the RSS feed, please consider doing so to get more posts like this, and follow me on Twitter!

  • http://www.phpexperts.pro/ Theodore R. Smith

    The code as written will *not* scale!!@

    At even a handful of users, you **will** run into file concurrency issues: User A views the page at the same second as Users B and C, both attempt to read/write the cache file and logfile at the same time, corruption occurs, B gets an incomplete page, C and everyone after gets nothing.

    • jax

      memcache

    • http://devgrow.com/ Monji

      I’m using the same technique on this blog, and it’s seen over 6000 users in a day before. The cacheLog file is only written to when a new cache file is created, when you purge the cache or if the cache file has expired. It’s being read by multiple users, so I agree that there is a bottleneck, but it will certainly not have any problems with minor traffic (unless you are making every user write to the log file, which defeats the purpose of caching).

      For sites that get a lot of traffic, I would definitely recommend investing more time and finding a more scalable solution that takes advantage of APC or memcache.

    • http://kodlabbet.net/ Robin Grass

      I know it’s a bit late but – you can easily get this script to deal with the issue of several people trying to write cache at the same time as it is being read by using a file lock (flock in php).

  • Muad fromYemen

    I am searching for a class like that for a while .. the most interesting part is log file and the purge function, but i wanna to ask you about the use for output buffering functions here. because in my CMS I am already using it at the beginning of my script(specifically) at the index page or the main controller to handle the problem of headers. the question is .. in this case how I can handle the start of buffering from some point as i already using the ob_start function?
    sorry for bad English

  • http://montanaflynn.me Montan Flynn

    Very nice.. we do something similar at work and have run into a few issues. Will have our dev look into this as an alternative. Thanks!

  • chintan

    hi,
    can i start and stop multiple time caching in same page ?

    if yes than how ?

  • http://www.vidjin.com/ VidJin

    Gorgeous piece of coding… Simple and Sweet.

    I believe this is exactly what I was looking for my Project. :)

    Thanks a lot.

    • http://vladimir-ivanov.net Vladimir Ivanov

      I absolutely agree with VidJin.
      Simple, neat, gorgeous piece of code.
      It saved on my project I wok on from kicking out of shared hosting.

  • Dan

    > “Just call the start function at the part of your script where you’d like to start caching and the end function wherever the cache ends.”

    This gives the impression that you can selectively exempt any part of a script from caching by placing the start and end function calls around only a portion of the code.

    But this looks to me like it’s only true for code that executes *before* caching starts.

    The way I’m reading your code, the script will exit completely after successfully echoing data from the cache within $cache->start(); and will thus never run any code that might appear *after* $cache->end(); (i.e., following the cache buffer).

    It works for your use case (“end caching after the footer”), but still seems like a caveat worth mentioning.

    Or am I missing something?

  • Kingsley

    Hi,
    I found this script incredible useful. It’s quick, simple and I love it.
    I’ve used it in a variety of different websites now.
    I do have one problem though and it’s not really been a problem, up until now anyway.

    Dan, the above user, has already asked my problem.

    I can only place the $cache->end(); at the very end of my pages(after ), otherwise it doesn’t finish loading the un-cached data.

    It’s been fine before by now I have ads, weather information and some other data which I don’t/can’t have cached.

    Is there a way around this?

    Cheers. If there is, I’d use this code forever. and ever.

    • Dovid Abba Goldstein

       Hey, I had the same problem as you (I wonder if you found a solution in the past 4 months). What I did was rather backwords I think, but I’m not that PHP savvy.

      I edited the cache.php and the line before exit() in the start function I ended the php script with ?> then added all my html and javascript code that I wanted after the cache, then I added <?php to let the function continue as if notin happend. :)

      If you have a more elegant solution please let me know.

      • http://twitter.com/AlexMReynolds Alex Reynolds

        I added a solution to thing

    • http://twitter.com/AlexMReynolds Alex Reynolds

      I added a comment on the fix for this issue

  • http://fashion2012-13.com/ Taani

    It worked like a charm. Thank you for quick cache :)

  • http://crickethighlights1.com/ Cricket Highlights

    Any one have tried purge function???
    How to sweetly delete the expired cache file???

  • http://twitter.com/AlexMReynolds Alex Reynolds

    Hi Everyone I had the same issue as most of you. HERE’s the fix! Okay in the Start() method you will see if the cache file exists EXIT. EXIT is bad in PHP. It will stop outputting everything else. Look at the line above. 

    When a cache file is found we set $this->caching = false. So here’s the solution. REMOVE THE EXIT. Now go to your page where you call the cache class. 

    $cache = new Cache;
    $cache->start();

    Now add an if statement around all you want to cache. If $cache->caching is false then that means we found a cache file. So we only want our code to output if there is no cache file so Caching would be true. SO the next line would be

    caching){ ?>
    put all your code you want to cache here
    probably html instead of tons of php echos. Doesn’t matter

    NOW END THE IF
    make sure you close the if bracket here.
    end(); ?>

    This will make sure if you do have a cache file then the code won’t output twice but it will continue to read after the if so the rest of your page will load. This make a single block of cached code easy. You can do the same for multiple blocks you will just have to make sure you change your naming method.

  • Tony

    Hi, I’ve a question. I want to use cache class for my php page but if a cache is created when i logged with my username (in top of page i’ve my username), all visitors see my username on homepage (but don’t have privileges because the cookies are not setting). This is a little security problem. Can i solve this problem?

    • Dams

      function __construct(){
      $this->cacheFile = base64_encode($_SERVER['REQUEST_URI']);
      /! ==> $this->User = ‘_’.base64_encode($_SESSION['MM_Username']);
      ……

  • Imran

    Hi, must say a very fine piece of code. But i have an issue, well the code works fine on my localhost. But surprisingly, when i test the same code on live server, it is not working at all. i feel that there may be an issue with buffer size. pls reply !

  • Dams

    No new version with fix ?

  • João Verissimo Ribeiro

    Here have an alternative class:
    https://github.com/joaoverissimo/SimpleCachePhp

    thanks

  • http://www.nic.com/ KhornePony

    For concurrency problem, i did some changes.

    1) i add a lock when it is written.

    2) if the file exists but it is too small (less than 64 bytes) then it ignores the cache completely.

    function start(){
    $location = array_slice(explode(‘/’,$_SERVER['REQUEST_URI']), 2);
    if(!in_array($location[0],$this->doNotCache)){
    if(@file_exists($this->cacheFileName) && (time() – filemtime($this->cacheFileName)) cacheTime && @$this->cacheLog[$this->cacheFile] == 1){
    if (@filesize($this->cacheFileName)>64) {
    $this->caching = false;
    echo file_get_contents($this->cacheFileName);
    exit();
    } else {
    $this->caching = false;
    ob_start();
    }
    }else{
    $this->caching = true;
    ob_start();
    }
    }
    }

    function end(){
    if($this->caching){
    file_put_contents($this->cacheFileName,ob_get_contents(),LOCK_EX);
    ob_end_flush();
    $this->cacheLog[$this->cacheFile] = 1;
    if(file_put_contents($this->cacheLogFile,serialize($this->cacheLog),LOCK_EX)) {
    return true;
    }
    }
    }

  • http://www.nic.com/ KhornePony

    i tried (windows 64bits & apache 32 bits) writing a webpage of 3mb and doing 30 concurrent calls to the webpage and it works perfectly.

  • Tobi

    Just great! Thank you very much!

  • http://www.fijianwarrior.co.uk/ Anil Dave

    Thanks for posting this. I had a problem with query string. base64 encoding converts ‘?’ to ‘/’. I added a method to the class to fix this:

    function mybase64_encode($s) {
    return str_replace(array(‘+’, ‘/’), array(‘,’, ‘-’), base64_encode($s));
    }