How to Improved Caching with PHP

Introduction

In the last Code Gallery Spotlight, we examined a PHP class that performed very simple data caching. This time around we are going to explore the same topic, but we will add new functionality in order to increase efficiency and reduce the chance of errors. The class we will be examining this time around uses file locking and employs some better coding techniques than the last class we used.
If you missed the last, you can find it at https://techsoftcenter.com/caching-variables-with-php/

Improvements

As I mentioned a moment ago, the class we will be examining today has several improvements over the class we looked at last time. For starters, rather than tracking the creation time by appending it to the file name, our new class uses the filemtimefunction to determine when the cache was last updated. This allows us to use a static filename rather than a dynamic one.

By removing the creation time from the file name, we are also able to achieve another improvement. In the last class we examined, we needed to loop through the filenames in the directory containing the cache files in order to find the proper file. Now that our file name is no longer dynamic, we can eliminate the looping and access the file directly. The more files there are in a particular directory, the more efficient this method will be for that directory.

File Locking

The biggest improvement this new class provides is file locking. File locking eliminates the possibility that data may be read from a cache file while it is being updated. Using file locks also prevents a situation where two processes try and update the cache at the same time.
File locking disables access to a file while some action is being performed on that file. By using file locking you can greatly reduce the chance of file corruption and lost data. In single-user applications, file locking is not a great concern. When multiple users may be accessing the same files, however, file locking is a must.                                               File locks can be either “mandatory” or “advisory”. When mandatory locks are used, reading and writing to files is not possible while a file is locked. Mandatory locks are usually implemented at the system level. Advisory locks, on the other hand, do allow for these operations to take place during a file lock. For advisory locks to work properly, any read or write operations must check for the existence of a file lock. Advisory locks are also known as cooperative locks.
The file locking method used by this class is an advisory lock and very straightforward. Before update to the cache file occurs, a copy of the file is made with a .lock extension. The updates are then made to the original cache file without that extension.
During the update process, any requests for the data in the cache file are retrieved from the file with the .lock extension. If another process attempts to update the cache during this time, the existence of the .lock file does not permit the update. After the update is completed, the file with the extension is removed.

Using this method ensures that the cache file is never updated by two processes simultaneously. If that were to happen, there could be very unpredictable data in the cache file.

New Caching Class

This class is a wonderful example of how classes in PHP should be written. Member functions are designed to carry out very specific tasks, the code is extremely well documented, and great care has been taken to ensure the integrity of the data.
Even if you have no need for caching in your applications, I recommend studying this implementation for its elegance in coding.

Code

<?php 
/* Cache file class 
* 
* This class is used to manage cache files 
* 
* @version 0.0.9 
* @author Jon Bardin <[email protected]> */ 

class Cache { 
    /* Cache object Id. 
     * 
     * @var string cacheObjectId 
     * @access private */ 
    var $cacheObjectId = ""; 

    /* Cache object timeout. 
     * 
     * @var integer [$timeout] timeout 
     * @access private */ 
    var $timeout; 

    /* Constructor 
     * 
     * Sets cache object path 
     * 
     * @param string cacheObjectId 
     * @param integer timeout 
     * @return boolean 
     * @access public */ 
    function Cache ($cacheObjectId, $timeout) { 
        $this->cacheObjectId = '/tmp/'.$cacheObjectId; 
        $this->timeout = $timeout; 
        return true; 
    } 

    /* Create cache object. 
     * 
     * Creates a cache object with passed contents 
     * 
     * @param string contents 
     * @return boolean 
     * @access private */ 
    function createCacheObject ($contents) { 
        $fp = fopen ($this->cacheObjectId, w); 
        if (!empty ($fp)) { 
            if (fputs ($fp, $contents)) { 
                fclose ($fp); 
                return true; 
            } else { 
                return false; 
            } 
        } else { 
            return false; 
        } 
    } 

    /* Updates cache object. 
     * 
     * Updates a cache object with passed contents 
     * 
     * @param string contents 
     * @return string contents 
     * @access public */ 
    function updateCacheObject ($contents) { 
        /* check to see if object exists */ 
        if ($this->cacheObjectExists()) { 
            /* check if its locked */ 
            if (!$this->cacheObjectLocked()) { 
                /* if object exists lock it */ 
                $this->lockCacheObject(); 

                /* then create new one */ 
                $this->createCacheObject($contents); 
                $this->unlockCacheObject(); 
                return $contents; 
            } else { 
                return $this->loadLockedObject(); 
            } 
        } else { 
            $this->createCacheObject($contents); 
            return $contents; 
        } 
    } 

    /* Load cache object. 
     * 
     * Loads cache object and returns its contents 
     * 
     * @return string contents 
     * @access public */ 
    function loadCacheObject () { 
            if (!$this->cacheObjectLocked()) { 
                return $this->cacheObjectContents($this->cacheObjectId); 
            } else { 
                return $this->loadLockedObject(); 
            } 
    } 

    /* Delete cache object. 
     * 
     * Deletes a cache object 
     * 
     * @return boolean 
     * @access private */ 
    function deleteCacheObject () { 
        if (!$this->cacheObjectLocked() && is_file ($this->cacheObjectId)) { 
            if (unlink ($this->cacheObjectId)) { 
                return true; 
            } else { 
                return false; 
            } 
        } else { 
            return true; 
        } 
    } 

    /* Lock cache object. 
     * 
     * Locks a cache object for updating 
     * 
     * @return boolean 
     * @access private */ 
    function lockCacheObject () { 
        if (copy ($this->cacheObjectId, $this->cacheObjectId.'.lock')) { 
            return true; 
        } else { 
            return false; 
        } 
    } 

    /* Unlock cache object. 
     * 
     * Unlocks a cache object for updating 
     * 
     * @return boolean 
     * @access private */ 
    function unlockCacheObject () { 
        if (unlink ($this->cacheObjectId.'.lock')) { 
            return true; 
        } else { 
            return false; 
        } 
    } 

    /* Check cache object. 
     * 
     * Checks to see if the cache object exists or not 
     * 
     * @return boolean 
     * @access private */ 
    function cacheObjectExists () { 
        if (is_file ($this->cacheObjectId)) { 
            return true; 
        } else { 
            return false; 
        } 
    } 

    /* Determines if cache object is locked. 
     * 
     * Checks to see if the cache object is locked or not 
     * 
     * @return boolean 
     * @access private */ 
    function cacheObjectLocked () { 
        if (is_file ($this->cacheObjectId.'.lock')) { 
            return true; 
        } else { 
            return false; 
        } 
    } 

    /* Cached object has timed out. 
     * 
     * Checks to see if the cache object has expired or not 
     * 
     * @return boolean 
     * @access public */ 
    function cacheObjectTimedOut () { 
        if ($this->cacheObjectExists()) { 
            if (time () - filemtime ($this->cacheObjectId) > $this->timeout) { 
                return true; 
            } else { 
                return false; 
            } 
        } else { 
            return true; 
        } 
    } 

    /* Load locked cache object. 
     * 
     * Loads a locked cache object and returns its contents 
     * 
     * @return string 
     * @access public */ 
    function loadLockedObject() { 
        return $this->cacheObjectContents($this->cacheObjectId.'.lock'); 
    } 

    /* Cache object contents. 
     * 
     * Loads a file and returns its contents 
     * 
     * @param string filename 
     * @return string 
     * @access private */ 
    function cacheObjectContents ($filename) { 
        $fd = fopen ($filename, "r"); 
        if (!empty ($fd)) { 
            $contents = fread ($fd, filesize ($filename)); 
            fclose ($fd); 
            return $contents; 
        } else { 
            die; 
        } 
    } 
} 

?>

Example Use

<?php 
require_once ("libCache.php"); 
$varcache = New Cache ("someVariable", 60); 
if ($varcache->cacheObjectTimedOut()) { 
    echo "Cache has expired or didn't exist!<BR>\n"; 
    echo "Refreshing the data.<BR>\n"; 
    $someVariable = "Some data in our cache."; 
    $varcache->updateCacheObject (serialize ($someVariable)); 
} else { 
    $someVariable = unserialize ($varcache->loadCacheObject()); 
} 
echo "Cache contains: $someVariable<BR>\n"; 
?>

Conclusion

In that article I detailed why and when you should use caching.