Location: PHPKode > scripts > MultiCache library > MultiCache.class.php
<?php
/**
 * MultiCache class library is a PHP solution for work with caches.
 * It can use local file system, memcache or other external storage.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * @author    Vadym Timofeyev <hide@address.com> http://weblancer.net/users/tvv/
 * @copyright 2007 Vadym Timofeyev
 * @license   http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version   1.00
 * @since     PHP 5.0
 */
abstract class MultiCache {
    /**
     * Cache max size, bytes.
     * If maxSize == -1 use cache driver specific value for cache max size.
     * @var integer
     */
    public $maxSize = 0;

    /**
     * Cache max items count
     * @var integer
     */
    public $maxItemsCount = 0;

    /**
     * Clean cache frequensy factor.
     * Clean cache operation will start randomically with random factor N if cache overflow.
     * @var integer
     */
    public $cleanCacheFactor = 10;

    /**
     * Class constructor. Setup primary parameters.
     * @param array $params Primary properties
     */
    public function __construct($params = array()) {
        foreach ($params as $key => $value) {
            $this->$key = $value;
        }
    }

    /**
     * Get data
     * @param mixed $key The key that will be associated with the item
     * @param mixed $default Default value
     * @return mixed Stored data
     */
    public abstract function get($key, $default = null);

    /**
     * Store data
     * @param string $key The key that will be associated with the item
     * @param mixed $value The variable to store
     * @param integer $expire Expiration time of the item. Unix timestamp or number of seconds.
     */
    public function set($key, $value, $expire = null) {
        // Check cache limits
        $err = null;
        if (($m = $this->getMaxItemsCount()) > 0 && $this->getItemsCount() >= $m) {
            $err = "Maximum items count attained!";
        }
        if (($m = $this->getMaxSize()) > 0 && $this->getSize() >= $m) {
            $err = "Maximum items count attained!";
        }

        // Check error
        if ($err != null) {
            // Check clean cache factor
            if ($this->cleanCacheFactor > 0 && mt_rand(0, $this->cleanCacheFactor - 1) == 0) {
                $this->clean();

                // Secondary check cache limits
                if ((!($m = $this->getMaxItemsCount()) || $this->getItemsCount() < $m) &&
                    (!($m = $this->getMaxSize()) || $this->getSize() < $m))
                {
                    return;
                }
            }
            throw new Exception($err);
        }
    }

    /**
     * Remove data from the cache
     * @param string $key The key that will be associated with the item
     */
    public abstract function remove($key);

    /**
     * Remove all cached data
     */
    public abstract function removeAll();

    /**
     * Clean expired cached data
     */
    public abstract function clean();

    /**
     * Get items count
     * @return integer Items count
     */
    public abstract function getItemsCount();

    /**
     * Get cached data size
     * @return integer Cache size, bytes
     */
    public abstract function getSize();

    /**
     * Get cache max size. If maxSize == -1 use cache driver specific value of cache max size
     * @return integer Cache maximum size, bytes
     */
    public function getMaxSize() {
        return $this->maxSize >= 0 ? $this->maxSize : $this->getTotalMaxSize();
    }

    /**
     * Get total cache max size.
     * @return integer Cache maximum size, bytes
     */
    public function getTotalMaxSize() {
        return 0;
    }

    /**
     * Get max items count
     * @return integer Maximum items count
     */
    public function getMaxItemsCount() {
        return $this->maxItemsCount;
    }
}

/**
 * MultiCacheFile is a class for work with file system storage.
 *
 * @author    Vadym Timofeyev <hide@address.com> http://weblancer.net/users/tvv/
 * @copyright 2007 Vadym Timofeyev
 * @license   http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version   1.01
 * @since     PHP 5.0
 * @example   examples/file/example.php
 */
class MultiCacheFile extends MultiCache {
    /**
     * File cache directory
     * @var string
     */
    public $cacheDir = '.';

    /**
     * Number of cache subderictories
     * @var string
     */
    public $subDirCount = 0;

    /**
     * Length of cache subderictories
     * @var string
     */
    public $subDirLength = 2;

    /**
     * Cache statistics. The structure is: array(count, size)
     * @var array Cache statistics
     */
    private $stats = null;

    /**
     * Get data
     * @param mixed $key The key that will be associated with the item
     * @param mixed $default Default value
     * @return mixed Stored data
     */
    public function get($key, $default = null) {
        // Get file name
        $fname = $this->getPathByKey($key);

        // Read file
        if (($data = @file_get_contents($fname)) && ($data = @unserialize($data))) {
            list($value, $expire) = $data;
            if ($expire > 0 && $expire < time()) {
                $this->remove($key);
            } else {
                return $value;
            }
        }
        return $default;
    }

    /**
     * Store data.
     * If expiration time set in seconds it must be not greater then 2592000 (30 days).
     * @param string $key The key that will be associated with the item
     * @param mixed $value The variable to store
     * @param integer $expire Expiration time of the item. Unix timestamp or number of seconds
     */
    public function set($key, $value, $expire = null) {
        parent::set($key, $value, $expire);

        // Get file name
        $fname  = $this->getPathByKey($key, true);
        if ($expire > 0 && $expire <= 2592000) {
            $expire = time() + $expire;
        }

        // Create file and save new data
        if (!($fh = fopen($fname, 'wb'))) {
            throw new Exception("File $fname not created!");
        }
        flock($fh, LOCK_EX);
        fwrite($fh, serialize(array($value, $expire)));
        flock($fh, LOCK_UN);
        fclose($fh);
    }

    /**
     * Remove data from the cache
     * @param string $key The key that will be associated with the item
     */
    public function remove($key) {
        // Get file name
        $fname = $this->getPathByKey($key);

        // Delete file
        if (is_file($fname)) {
            if (!unlink($fname)) {
                throw new Exception("File $fname not deleted!");
            }
            if ($this->stats && $this->stats[0] > 0) {
                $this->stats[0]--;
            }
        }
    }

    /**
     * Remove all cached data
     */
    public function removeAll() {
        self::rmdir($this->cacheDir);
        $this->stats = null;
    }

    /**
     * Clean expired cached data
     */
    public function clean() {
        foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->cacheDir)) as $file) {
            $this->get(@base64_decode(basename($file)));
        }
        $this->stats = null;
    }

    /**
     * Get items count
     * @return integer Items count
     */
    public function getItemsCount() {
        if ($this->stats != null) {
            $this->stats = $this->getStats();
        }
        return $this->stats[0];
    }

    /**
     * Get cached data size
     * @return integer Cache size, bytes
     */
    public function getSize() {
        if ($this->stats != null) {
            $this->stats = $this->getStats();
        }
        return $this->stats[1];
    }

    /**
     * Get cache statistics
     * @return array Cache statistics
     */
    private function getStats() {
        $cnt = 0;
        $size = 0;
        foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->cacheDir)) as $file) {
            $cnt++;
            $size += filesize($file);
        }
        return array($cnt, $size);
    }

    /**
     * Remove all files and subdirectories
     * @param string $dir Directory name
     */
    private static function rmdir($dir) {
        $dir = new RecursiveDirectoryIterator($dir);
        foreach(new RecursiveIteratorIterator($dir) as $file) {
            @unlink($file);
        }
        foreach($dir as $subDir) {
            if(!@rmdir($subDir)) {
                self::rmdir($subDir);
                @rmdir($subDir);
            }
        }
    }

    /**
     * Get file path by key
     * @param string $key The key that will be associated with the item
     * @param boolean $ismkdir If true this function creates new subdirectories
     * @return string File path
     */
    protected function getPathByKey($key, $ismkdir = false) {
        $fname = base64_encode($key);
        $dir = $this->cacheDir;
        if ($i = $this->subDirCount) {
            if (strlen($fname) > 250) {
                throw new Exception("Hash for key [$key] is bigger then 250 characters!");
            }
            $len = $this->subDirLength;
            $fcode = $fname;

            while ($i-- > 0) {
                $dcode = substr($fcode, 0, $len);
                if (strlen($dcode) < $len) {
                    break;
                }
                $dir .= "/$dcode";
                if ($ismkdir && !is_dir($dir) && !mkdir($dir, 0777)) {
                    throw new Exception("Directory $dir not created!");
                }
                $fcode = substr($fcode, $len);
            }
        }
        return "$dir/$fname";
    }
}

/**
 * MultiCacheFileString is a class for work with file system storage
 * for keeping simple strings without expiration. The key is simple string too.
 *
 * @author    Vadym Timofeyev <hide@address.com> http://weblancer.net/users/tvv/
 * @copyright 2007 Vadym Timofeyev
 * @license   http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version   1.01
 * @since     PHP 5.0
 * @example   examples/file/example.php
 */
class MultiCacheFileString extends MultiCacheFile {
    /**
     * Get data
     * @param mixed $key The key that will be associated with the item
     * @param mixed $default Default value
     * @return mixed Stored data
     */
    public function get($key, $default = null) {
        $fname = $this->getPathByKey($key);
        return is_file($fname) ? file_get_contents($fname) : $default;
    }

    /**
     * Store data.
     * If expiration time set in seconds it must be not greater then 2592000 (30 days).
     * @param string $key The key that will be associated with the item
     * @param string $value The variable to store
     * @param integer $expire Expiration time of the item. Unix timestamp or number of seconds
     */
    public function set($key, $value, $expire = null) {
        MultiCache::set($key, $value, $expire);
    
        // Get file name
        $fname = $this->getPathByKey($key, true);

        // Create file and save new data
        if (!($fh = fopen($fname, 'wb'))) {
            throw new Exception("File $fname not created!");
        }
        flock($fh, LOCK_EX);
        fwrite($fh, $value);
        flock($fh, LOCK_UN);
        fclose($fh);
    }

    /**
     * Get file path by key
     * @param string $key The key that will be associated with the item
     * @param boolean $ismkdir If true this function creates new subdirectories
     * @return string File path
     */
    protected function getPathByKey($key, $ismkdir = false) {
        $dir = $this->cacheDir;
        if ($i = $this->subDirCount) {
            $fcode = $key;
            $len = $this->subDirLength;
            while ($i-- > 0) {
                $dcode = substr($fcode, 0, $len);
                if (strlen($dcode) < $len) {
                    break;
                }
                $dir .= "/$dcode";
                if ($ismkdir && !is_dir($dir) && !mkdir($dir, 0777)) {
                    throw new Exception("Directory $dir not created!");
                }
                $fcode = substr($fcode, $len);
            }
        }
        return "$dir/$key";
    }
}

/**
 * MultiCacheFileSimple is a class for work with file system storage
 * for keeping simple strings without expiration. The key is simple string too.
 * This cache class doesn't check cache overflow and keeps items in single directory.
 *
 * @author    Vadym Timofeyev <hide@address.com> http://weblancer.net/users/tvv/
 * @copyright 2007 Vadym Timofeyev
 * @license   http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version   1.01
 * @since     PHP 5.0
 * @example   examples/file/example.php
 */
class MultiCacheFileSimple extends MultiCacheFile {
    /**
     * Get data
     * @param mixed $key The key that will be associated with the item
     * @param mixed $default Default value
     * @return mixed Stored data
     */
    public function get($key, $default = null) {
        $fname = $this->cacheDir . '/' . $key;
        return is_file($fname) ? file_get_contents($fname) : $default;
    }

    /**
     * Store data.
     * If expiration time set in seconds it must be not greater then 2592000 (30 days).
     * @param string $key The key that will be associated with the item
     * @param string $value The variable to store
     * @param integer $expire Expiration time of the item. Unix timestamp or number of seconds
     */
    public function set($key, $value, $expire = null) {
        // Get file name
        $fname = $this->cacheDir . '/' . $key;

        // Create file and save new data
        if (!($fh = fopen($fname, 'wb'))) {
            throw new Exception("File $fname not created!");
        }
        flock($fh, LOCK_EX);
        fwrite($fh, $value);
        flock($fh, LOCK_UN);
        fclose($fh);
    }
}

/**
 * MultiCacheMemcache is a class for work with memcache storage.
 *
 * @author    Vadym Timofeyev <hide@address.com> http://weblancer.net/users/tvv/
 * @copyright 2007 Vadym Timofeyev
 * @license   http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version   1.00
 * @since     PHP 5.0
 * @example   examples/memcache/example.php
 */
class MultiCacheMemcache extends MultiCache {
    /**
     * Memcache handler
     * @var class Memcache
     */
    private $memcache = null;

    /**
     * @var array Memcache statistics
     */
    private $stats = null;

    /**
     * Memcache host
     * @var string
     */
    public $host = 'localhost';

    /**
     * Memcache port
     * @var integer
     */
    public $port = 11211;

    /**
     * Is memcached connection persistent or no
     * @var boolean
     */
    public $isPersistent = true;

    /**
     * Get Memcache instance
     * @return object Memcache instance
     */
    private function getMemcache() {
        if ($this->memcache === null) {
            $this->memcache = new Memcache();
            if ($this->isPersistent) {
                if (!@$this->memcache->pconnect($this->host, $this->port)) {
                    throw new Exception("Don't open memcached server persistent connection!");
                }
            } else {
                if (!@$this->memcache->connect($this->host, $this->port)) {
                    throw new Exception("Don't open memcached server connection!");
                }
            }
        }
        return $this->memcache;
    }

    /**
     * Class destructor. Close opened handlers.
     */
    public function __destruct() {
        if (($memcache = $this->getMemcache()) != null && !$this->isPersistent) {
            $memcache->close();
        }
    }

    /**
     * Get data
     * @param mixed $key The key that will be associated with the item
     * @param mixed $default Default value
     * @return mixed Stored data
     */
    public function get($key, $default = null) {
        $result = $this->getMemcache()->get($key);
        return $result !== false ? $result : $default;
    }

    /**
     * Store data
     * @param string $key The key that will be associated with the item
     * @param mixed $value The variable to store
     * @param integer $expire Expiration time of the item. Unix timestamp or number of seconds
     */
    public function set($key, $value, $expire = 0) {
        parent::set($key, $value, $expire);
        $this->getMemcache()->set($key, $value, false, $expire);
    }

    /**
     * Remove data from the cache
     * @param string $key The key that will be associated with the item
     */
    public function remove($key) {
        if ($this->getMemcache()->delete($key) && $this->stats && $this->stats['curr_items'] > 0) {
            $this->stats['curr_items']--;
        }
    }

    /**
     * Remove all cached data
     */
    public function removeAll() {
        if (!$items = $this->getStats('items')) {
            return;
        }
        $memcache = $this->getMemcache();
        foreach ($items['items'] as $key => $item) {
            $dump = $memcache->getStats('cachedump', $key, $item['number'] * 2);
            foreach (array_keys($dump) as $ckey) {
                $memcache->delete($ckey);
            }
        }
        $this->stats = null;
    }

    /**
     * Clean expired cached data
     */
    public function clean() {
        if (!$items = $this->getStats('items')) {
            return;
        }
        $memcache = $this->getMemcache();
        foreach ($items['items'] as $key => $item) {
            $dump = $memcache->getStats('cachedump', $key, $item['number'] * 2);
            foreach (array_keys($dump) as $ckey) {
                $memcache->get($ckey);
            }
        }
        $this->stats = null;
    }

    /**
     * Get items count
     * @return integer Items count
     */
    public function getItemsCount() {
        return $this->getStats('curr_items');
    }

    /**
     * Get cached data size
     * @return integer Cache size, bytes
     */
    public function getSize() {
        return $this->getStats('bytes');
    }

    /**
     * Get total cache max size.
     * @return integer Cache maximum size, bytes
     */
    public function getTotalMaxSize() {
        return $this->getStats('limit_maxbytes');
    }

    /**
     * Get memcache statistics
     * @param string $param Statistics paramater
     * @return array Memcache statistics
     */
    private function getStats($param = null) {
        if ($this->stats != null) {
            $this->stats = $this->getMemcache()->getStats();
        }
        return $param ? $this->stats[$param] : $this->stats;
    }

    /**
     * Check CURL extension, etc.
     */
    public static function checkEnvironment() {
        if (!extension_loaded('memcache')) {
            throw new Exception('Memcache extension not loaded');
        }
    }
}
?>
Return current item: MultiCache library