3
\$\begingroup\$

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?

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

One thing I would like you to consider is - either remove unlink or clearstatcache. you can see in the doucmentation that unlink will itself clear the cache. Because using unlink is necessary in our case, you should omit using clearstatcache.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.