Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I wrote a simple class to cache data on a filesystem. The class also provides an internal cache using a static variable to avoid reading from disk for each request.

<?php
/**
 * object caching system
 */
class cache {

    /**
     * The *Singleton* instances
     * @staticvar cache
     */
    private static $instance = null;

    /**
     * The instances cache
     * @staticvar array
     */
    private static $cache = null;

    /**
     * cache path
     * @var string
     */
    protected $tempdir;


    /**
     * Create a cache instance
     * @return object A cache object
     */
    protected function __construct(){

        $tempdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . __CLASS__ . DIRECTORY_SEPARATOR;

        if( !is_dir($tempdir) ){
            mkdir($tempdir, 0700);
        }

        $this->tempdir = $tempdir;
    }


    /**
     * Returns the *Singleton* instance
     * @return Singleton The *Singleton* instance.
     */
    public static function getInstance(){

        if (null === self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }


    /**
     * Private clone method to prevent cloning of the instance of the*Singleton* instance.
     * @return void
     */
    private function __clone()
    {
    }


    /**
     * Private unserialize method to prevent unserializing of the *Singleton* instance.
     * @return void
     */
    private function __wakeup()
    {
    }


    /**
     * Store an item
     * @param string $key The key under which to store the value. 
     * @param mixed $value The value to store. 
     * @param integer $lifetime The expiration time, defaults to 3600
     * @return boolean
     */
    public function set($key, $value, $lifetime = 3600){

        if( !$this->isKey($key) )
            return false;

        if( $data = json_encode(array('lifetime' => time() + $lifetime,  'data' => $value)) ){
            if (file_put_contents($this->tempdir.$key, $data) !== false){
                self::$cache[$key] = $data;
                return true;
            }
        }

        return false;
    }


    /**
     * set a new expiration on an item
     * @param string $key The key under which to store the value. 
     * @param integer $lifetime The expiration time, defaults to 3600
     * @return boolean
     */
    public function touch($key, $lifetime = 3600){

        if( $data = $this->get($key) ){
            return $this->set($key, $data, $lifetime);
        }

        return false;
    }


    /**
     * returns the item that was previously stored under the key
     * @param string $key The key of the item to retrieve.
     * @param  mixed $default The default value (see @return)
     * @return mixed Returns the value stored in the cache or $default otherwise
     */
    public function get($key, $default = null){

        if( !$this->isKey($key) )
            return false;

        $file  = $this->tempdir . $key;
        $fdata = false;

        if( isset(self::$cache[$key]) ){
            $fdata = self::$cache[$key];
        } else {
            $fdata = @file_get_contents($file);
            self::$cache[$key] = $fdata;
        }

        if( $fdata !== false ){

            // check if empty (file with failed write/unlink)
            if( !empty($fdata) ){

                $fdata = json_decode($fdata, true);

                if( isset($fdata['lifetime'], $fdata['data']) ) {
                    if( $fdata['lifetime'] >= time() ){
                        return $fdata['data'];
                    } else {
                        $this->deleteFile($file);
                    }
                }
            } else {
                $this->deleteFile($file);
            }
        }

        return $default;
    }


    /**
     * Invalidate all items in the cache
     * @return Returns TRUE on success or FALSE on failure
     */
    public function flush(){
        return $this->deleteByPattern('*');
    }


    /**
     * Delete an item
     * @param string $key The key to be deleted. 
     * @return boolean Returns TRUE on success or FALSE on failure
     */
    public function delete($key){

        if( !$this->isKey($key) )
            return false;

        return $this->deleteFile($this->tempdir . $key);
    }


    /**
     * Delete item matching pattern sintax 
     * @param string $pattern The pattern (@see glob())
     * @return Returns TRUE on success or FALSE on failure
     */
    public function deleteByPattern($pattern = '*'){
        $return = true;

        foreach ( glob($this->tempdir.$pattern, GLOB_NOSORT | GLOB_BRACE) as $cacheFile){
            if( !$this->deleteFile($cacheFile) ){
                $return = false;
            }
        }

        return $return;
    }


    /**
     * check if $key is valid key name
     * @param string $key The key to validate
     * @return boolean Returns TRUE if valid key or FALSE otherwise
     */
    protected function isKey($key){
        return !preg_match('/[^a-z_\-0-9]/i', $key);
    }


    /**
     * delete a file
     * @param string $cacheFile
     */
    private function deleteFile($cacheFile){

        unset(self::$cache[basename($cacheFile)]);

        clearstatcache(true, $cacheFile);

        if( file_exists($cacheFile) ) {
            if( !@unlink($cacheFile) ){
                return (file_put_contents($cacheFile, '') !== false);
            }

            clearstatcache(true, $cacheFile);
        }

        return true;
    }
}

Utilization:

<?php
require_once 'cache.php';

$cache = cache::getInstance();

$cache->set('foo', 'bar');

// ***************************************************
// test performance from disk and from instance cache
// ***************************************************

echo 'read from disk<br>';
echo 'expected: bar';
$start = microtime(true);
var_dump($cache->get('foo') );
echo 'time:' . ( microtime(true)-$start ) .'<br><br>';

echo 'read from instance cache<br>';
echo 'expected: bar';
$start = microtime(true);
var_dump($cache->get('foo') );
echo 'time:' .( microtime(true)-$start ) .'<br><br>';

// ***************************************************
// test basic features
// ***************************************************

echo 'test get(foo)<br>';
echo 'expected: bar';
var_dump( $cache->get('foo') );

echo 'test set(foo, baz)<br>';
echo 'expected: true';
var_dump( $cache->set('foo', 'baz') );

echo 'test get(foo)<br>';
echo 'expected: baz';
var_dump( $cache->get('foo') );

echo 'test delete(foo)<br>';
echo 'expected: true';
var_dump( $cache->delete('foo') );

echo 'test get(foo)<br>';
echo 'expected: null';
var_dump( $cache->get('foo') );

echo 'test flush()<br>';
echo 'expected: true';
var_dump( $cache->flush('foo') );

// ***************************************************
// test lifetime of cache
// ***************************************************

echo 'set a key with low lifetime (1 second)<br>';
echo 'expected: true';
var_dump( $cache->set('foo', 'baz', 1) );

echo 'wait 2 second for cache expire<br>';
sleep(2);

echo 'expected: null (cache expire)';
var_dump( $cache->get('foo') );

echo 'set a key with low lifetime (1 second)<br>';
echo 'expected: true';
var_dump( $cache->set('foo', 'baz', 1) );

echo 'increase lifetime with touch()<br>';
echo 'expected: true';
var_dump( $cache->touch('foo', 5) );

echo 'wait 2 second for cache expire<br>';
sleep(2);

echo 'expected: baz';
var_dump( $cache->get('foo') );

// ***************************************************
// test flush/deleteByPattern
// ***************************************************

echo 'flush all<br>';
echo 'expected: true';
var_dump( $cache->flush() );

echo 'get(foo) after flush<br>';
echo 'expected: null';
var_dump( $cache->get('foo') );

echo 'set new key -foo-bar-baz-<br>';
echo 'expected: true';
var_dump($cache->set('-foo-bar-baz-','foo'));

echo 'deleteByPattern(*-foo-*)<br>';
echo 'expected: true';
var_dump($cache->deleteByPattern('*-foo-*'));

echo 'get(-foo-bar-baz-) after deleteByPattern<br>';
echo 'expected: null';
var_dump( $cache->get('-foo-bar-baz-') );

What do you think of this code? Is it safe and fast?

share|improve this question
1  
What will happen if I set the key ../../../../../../../../../../../ect/? – Ismael Miguel Feb 8 '15 at 1:22
1  
@IsmaelMiguel thanks, I have added a control – Simone Nigro Feb 8 '15 at 10:31
    
Why would I use this over, for instance, memcache, APC, ... ? – Pinoniq Feb 8 '15 at 15:51
    
@Pinoniq do not have memcache, apc .. – Simone Nigro Feb 8 '15 at 19:57
    
@SimoneNigro why are you using @ – braunbaer Feb 11 '15 at 16:57

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.