Rayhan’s blog (raynux.com)

Rayhan’s Personal Web Blog Site

Entries Comments


RayCache – An Easy, Multiple Configurable PHP Caching Class

8 August, 2010 (03:11) | class, PHP, programming, Reference

Tags: , , , ,


RayCache, A Simple and Easy PHP Caching Class with multiple configurations and plug and play features.

Ever needed a quick caching class for your PHP application? This is another caching class for PHP which is able to solve your quick caching needs. Now a days all major frameworks comes with a standard caching library. But, in some cases you may need only a caching class. It is also helpful when you are not using framework or if you don’t like the cache class provided by the framework. In my case, I am using CodeIgniter for last few months and I am not happy with the caching solution provided by CodeIgniter.

Since, I am a CakePHP lover, I have looked at CakePHP cache library and tried to make something similar. So, you may find similarity with CakePHP.

Features:

- Simple & Easy to use from anywhere in your application with a single line of code.
- Easily configurable & can work without any configuration.
- Support multiple cache configurations
- Support static method for caching
- Support file caching engine for now, other caching engines can be added to the class
- Support singleton pattern
- Light Weight

Class:

File: raycache.php

<?php 
/*	SVN FILE: $Id: raycache.php 85 2008-05-13 07:36:10Z rayhan $	 */
/**	Cache Object Class.
*
*	Caching classes for easy and plug and play installation.
* 
*	PHP version 5+
*	
*	
*	@copyright		Copyright 2006-2010, Md. Rayhan Chowdhury
*	@package		raynux
*	@subpackage		raynux.labs.cache
*	@version		$Revision: 85 $
* 	@modifiedby		$LastChangedBy: rayhan $
*	@lastModified           $Date: 2008-05-13 13:36:10 +0600 (Tue, 13 May 2008) $
*	@author			$Author: rayhan $
*	@website		www.raynux.com
*       @license                MIT License http://www.opensource.org/licenses/mit-license.php
*/


/**
 * Cache Engine Interface
 * 
 */
Interface RayCacheEngineInterface{
    function write($key, $data, $options = array());
    function read($key, $options = array());
    function delete($key, $options = array());
    function clear($expired = true);
    function gc();
    public static function &getInstance($configs = array());
}

/**
 * Cache Class
 *
 * Provide cache functionality for multiple configuration and engine.
 * Static read-write method are helpful to call from anywhere of the script.
 *
 * @package raynux
 * @subpackage raynux.labs.cache
 */
class RayCache{
    /**
     * Class Instances
     *
     * @var array
     */
    private static $__instances = array();

    /**
     * Get Instance of a cache engine
     *
     * Factory Interface for Cache Engine.
     *
     * @param config $configName
     * @param string $engine default file
     * @param array $configs
     * @return object
     */
    public static function &getInstance($configName = null, $engine = null, $configs = array()){
        if (empty($configName)) {
            $configName = 'default';
        }

        if (empty($engine)) {
            $engine = 'file';
        }

        if (isset(self::$__instances[$configName])) {
            return self::$__instances[$configName];
        }

        if (empty(self::$__instances)) {
            $default = true;
        }

        $engine = strtolower($engine);

        switch ($engine){
            case 'file':
            default:
                self::$__instances[$configName] = new RayFileCache($configs);
                break;
        }

        return self::$__instances[$configName];
    }

    /**
     * Static wrapper to cache write method
     *
     * @param string $key
     * @param mixed $data
     * @param array $options, array('expire' => 10), expire in seconds
     * @param string $configName 
     * @return boolean
     */
    public static function write($key, $data, $options = array(), $configName = 'default') {
        $_this = self::getInstance($configName);
        return $_this->write($key, $data, $options);
    }

    /**
     * Static Wrapper to cache read method
     *
     * @param string $key
     * @param array $options
     * @param string $configName 
     * @return mixed
     */
    public static function read($key, $options = array(), $configName = 'default') {
        $_this = self::getInstance($configName);
        return $_this->read($key, $options);
    }

    /**
     * Static wrapper to cache delete mathod
     *
     * @param string $key
     * @param array $options
     * @param string $configName
     * @return boolean
     */
    public static function delete($key, $options = array(), $configName = 'default') {
        $_this = self::getInstance($configName);
        return $_this->delete($key, $options);
    }
}


/**
 * File Cache Engine
 *
 * @package raynux
 * @subpackage raynux.labs.cache
 */
class RayFileCache implements RayCacheEngineInterface{
    /**
     * Class Instances
     * 
     * @var array
     */
    private static $__instance;

    /**
     * Runtime Configuration Data
     * 
     * @var array
     */
    protected $_configs = array();

    /**
     * Class Constructor
     * 
     * @param array $configs
     */
    function  __construct($configs = array()) {
        $this->config($configs);

        // run garbage collection
        if (rand(1, $this->_configs['gc']) === 1) {
            $this->gc();
        }
    }

    /**
     * Get Instance of Class
     *
     * @param string $name
     * @param array $configs
     * @return object
     * @static
     */
    public static function &getInstance($configs = array()){
        if (is_null(self::$__instance)) {
            self::$__instance = new self($configs);
        }
        return self::$__instance;
    }

    /**
     * Set Configuration
     *
     * default: array('path' => './cache/', 'prefix' => 'raycache_', 'expire' => 10, 'gc' => 100)
     *
     * @param array $configs
     * @return object self instance
     */
    function &config($configs = array()) {
    	// default path modified to work with ci cache
        $default = array('path' => './cache/', 'prefix' => 'raycache_', 'expire' => 10, 'gc' => 100);
        $this->_configs = array_merge($default, $configs);
        return $this;
    }

    /**
     * Write data to cache
     *
     * @param string $key
     * @param mixed $data
     * @param array $options
     * @return boolean
     */
    public function write($key, $data, $options = array()){
        // check is writable
        if (!is_writable($this->_configs['path'])) {
            echo $this->_configs['path'];
            return false;
        }

        // Prepare data for writing
        if (!empty($options['expire'])) {
            $expire = $options['expire'];
        } else {
            $expire = $this->_configs['expire'];
        }

        if (is_string($expire)) {
            $expire = strtotime($expire);
        } else {
            $expire = time() + $expire;
        }
        
        $data = serialize(array('expire' => $expire, 'data' => $data));

        $fileName = $this->_configs['path'] . $this->_configs['prefix'] . $key;
        
        // Write data to files
        if (file_put_contents($fileName, $data, LOCK_EX)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Read Data from cache 
     * 
     * @param string $key
     * @param array $options
     * @return mixed
     */
    public function read($key, $options = array()) {
        $fileName = $this->_configs['path'] . $this->_configs['prefix'] . $key;

        if (!file_exists($fileName)) {
            return false;
        }

        if (!is_readable($fileName)) {
            return false;
        }

        $data = file_get_contents($fileName);
        if ($data === false) {
            return false;
        }
        
        $data = unserialize($data);
        
        if ($data['expire'] < time()) {
            $this->delete($key);
            return false;
        }

        return $data['data'];
    }

    /**
     * Delete a cache data
     * 
     * @param string $key
     * @param arrayt $options
     * @return boolean
     */
    function delete($key, $options = array()) {
        $fileName = $this->_configs['path'] . $this->_configs['prefix'] . $key;
        if (!file_exists($fileName) || !is_writable($fileName)) {
            return false;
        }
        return unlink($fileName);
    }

    /**
     * Clear cache data
     *
     * @param boolean $expired if true then only delete expired cache
     * @return booelan
     */
    public function clear($expired = true) {
        $entries = glob($this->_configs['path'] . $this->_configs['prefix'] . "*");

        if (!is_array($entries)) {
            return false;
        }

        foreach ($entries as $item) {
            if (!is_file($item) || !is_writable($item)) {
                continue;
            }

            if ($expired) {
                $expire = file_get_contents($item, null, null, 20, 11);

                $strpos = strpos($expire, ';');
                if ($strpos !== false) {
                    $expire = substr($expire, 0, $strpos);
                }

                if ($expire > time()) {
                    continue;
                }
            }
            
            if (!unlink($item)) {
                return false;
            }
        }
        
        return true;
    }

    /**
     * Garbage collection
     * 
     * @return boolean
     */
    public function gc() {
        return $this->clear(true);
    }
}

?>

Usage Example: some quick examples are given below to introduce you with the class and it’s methods.

<?php
    /**
     * Load the cache library
     * 
     */
    include_once('raycache.php');

    /**
     * Class Example
     * 
     * Once loaded you can use this class in two ways:
     * - Using Static Methods (My Favorite)
     * - Using Class Instance
     * 
     * or, even you can mix them both
     * In both ways it support multiple configuration and uses
     */

    /**
     * Static Method
     */

    //Get default CacheInstance with one of the following methods
    RayCache::getInstance();

    // OR
    RayCache::getInstance(null, null, array('path' => 'my_cache_path/', 'prefix' => 'my_cache_', 'expire' => '+10 seconds'));

    // You can configure or reconfigure your instance anytime you like.
    RayCache::getInstance()->config(array('path' => 'my_cache_path/', 'prefix' => 'my_cache_', 'expire' => '+10 seconds'));

    // store data
    RayCache::write('test_key', 'This data will expire in 10 seconds
'); RayCache::write('test_key2', 'This data will expire in 10 seconds
', array('expire' => '+10 seconds')); // expre value can be integer in seconds or time string supported by php strtotime method RayCache::write('test_key3', 'This data will expire in 20 seconds
', array('expire' => '+20 seconds')); // read data echo RayCache::read('test_key'); echo RayCache::read('test_key2'); echo RayCache::read('test_key3'); /** * Class Instance Method */ // get calss class instance $cache = RayCache::getInstance(); // default configure $cache2 = RayCache::getInstance('short', null, array('prefix' => 'short_', 'path' => 'my_cache_path/', 'expire' => '+20 seconds')); $cache3 = RayCache::getInstance('long', null, array('prefix' => 'long_', 'path' => 'my_cache_path/', 'expire' => '+1 hour')); // store data $cache->write('test_key', 'This data will expire in 10 seconds
'); $cache2->write('test_key2', 'This data will expire in 20 seconds
'); $cache3->write('test_key3', 'This data will expire in 1 hour
'); // read data echo $cache->read('test_key'); echo $cache2->read('test_key2'); echo $cache3->read('test_key3'); ?>

Please let me know if you find this class helpful for you..

«

  »

Comments

Comment from rayhan
Time: September 20, 2010, 11:40 am

One more thing, this cache class can be extended with memcache and other caching engine.

Comment from Emran Hasan
Time: September 28, 2010, 2:50 pm

Hi Rayhan Bhai,

The implementation is clean and can be built upon. Your code is also very well-structured, with a few exceptions:

- use of singletone
- no visibility at “function __construct”
- unnecessary header comment for the simple functions (config is the only exception where comment is required)

However, I believe using Zend_Cache instead of a new implementation makes more sense as it has all these functionalities, stable drivers, tested code, etc. The objective of which is to save other developers time in re-writing it.

Just my thoughts.

Comment from rayhan
Time: September 28, 2010, 5:11 pm

Dear Emran Bhai,

Thank you very much for your valuable comment!

I agree with you on Zend_Cache library, and when using Zend Framework, Zend_Cache should be the only option. This class is handy when you are not using Zend Framework or CakePHP or any other framework which comes with a default Caching system.

Last few months, I worked on few CodeIgniter projects and I was not happy with the caching system provided by the framework. Zend_Cache was an option but I had to load the entire zend library to use the Zend_Cache as it has some dependency. That is why I have come with this class with only a single file to be loaded easily to any projects without dependency.

- Regarding your thoughts:

- no visibility at “function __construct”
I agree and I will upgrade it with construct

- use of Singletone
It support singleton, but not restricted on only one instance of cache, you can have multiple instance easily.

- Header comments are not always unnecessary
I know that, coding with beauty doesn’t require comments that much. But, It helps in documentation very much, like if you use doxygen your class documentation will be done instantly. Also when we are using IDE like netbeans, a code completion window with function signature along with parameters and description shows automatically.

Thanks again for the suggestions.

Comment from M. A. Islam
Time: October 12, 2012, 12:42 am

Glad to see that a Bangladeshi has written a library for php caching. The cricketers of Bangladesh has made good branding of our country. If the Bangladeshi talents like you make good libraries (under MIT/BSD licensing), I hope the whole IT world will know Bangladesh as an IT power house.

By the way, can you write small and fast ORM library for mysql and Mongodb? I will use that will slim framework.

Write a comment