MP Cache: Simple flexible Caching of parts of code

Derek Jones edited this page Jul 5, 2012 · 21 revisions
Clone this wiki locally

There's also a forum topic if you want to reply.

There's a second & improved version at this link.

I wanted to implement caching but neither solution provided with CodeIgniter does it for me:

  • DB caching uses a controller+function format which doesn’t work for me as most of the site is generated by 1 function of 1 controller and the input to that function decides what models/libraries to load and to display (which view files). (most of the url requests are in the format http://mydomain.com/page_name.htm and routed & remapped) Also I can’t delete on a query basis, only on a page basis which means that for changes to my navigation menu I have to delete the entire cache…
  • Output caching caches too much, since the navigation is loaded dynamicly and I would have to erase my entire cache for every little edit.

So I thought of another way which probably isn’t either new or very special, but it works like a charm. It works by putting the part of the output you want to cache in an array and serializing that array into a custom file that would be accessable from every page/controller/model/etc…

A code example:```php $this->load->library('MP_Cache');

$example = $this->mp_cache->get('example'); if ($example === false) { // parts of code that generate your $example array, like for example below $example = $this->my_model->get_pages_from_db();

$this->mp_cache->write($example, 'example');

}


Where the get() function either returns the cached values unserialized to the variable or returns false if there’s no cache. If it returns false the following code would be executed and the values would be cached afterwards (with the write() function).

Other functions would include delete_all() for deleting all the cache (or all cache files within a certain directory when specified), and delete() for deleting specific caches. The above example would need the cache to be deleted when an update to the db occurs, so in keeping with the example I’d add the following code to my update function within “my_model”:```php
$this->mp_cache->delete('example');

It might solve problems like:

  • Compared to DB caching: Reusable. Some parts (like your site menu’s) is the same on every page and probably doesnt need to be cached for each individual page (even highlighting the current page should’t be a problem with this solution).
  • Compared to DB caching: You only have to delete it once after a change was made to any part of it and not all the cache for every page that uses the changed information.
  • Compared to output cache: Caching portions of a page isn’t a problem.
  • Different cache for different uses of a function aren’t a problem as you can add a variable in the cache “name”

The downsides are:

  • It’s harder to keep track of your cache: when to delete and when to reuse. But that’s what the library is all about: it doesn’t have a build-in mechanism for managing cache which means more freedom to use it as you please.
  • Of course it's not really caching the code but the output, which means that in reusing cache you also have to reuse the code which generates it.

FUNCTION OVERVIEW $this->mp_cache->get($filename) Retrieves the cache from the file set by the $filename variable, the actual files are suffixed by '.cache' which you don't include in the variable. Directories are allowed, so a filename like 'pages/page1' actually links to a file 'page1.cache' in the subdirectory 'pages' of your main cache directory. Optional second parameter $use_expires: Ignores an expires time set with write() when set to false, defaults to true.

$this->mp_cache->write($values, $filename) Writes the cache by serializing the $values array to a file set by the $filename variable. For the $filename variable the same rules apply as with get(). You don't have to create subdirectories in advance, any specified subdirectory will be created if it doesn't exist. Note: This isn't tested with PHP4. PHP5 allows for directly creating a subdirectory of a subdirectory, like creating 'example/sub/sub' in an empty directory - PHP4 doesn't allow this and would need to create each subdirectory recursively. Optional third parameter $expires: time in seconds after which this cache expires and should be refreshed. Defaults to 0 and doesn't expire when set to 0.

$this->mp_cache->delete($filename) Deletes the cache file specified by $filename.

$this->mp_cache->delete_all($dirname) Deletes the full cache if no $dirname is set. Deletes a subdirectory and everything in it if a directory is set by $dirname.

INSTALLATION REQUIREMENTS

  • Just put the library code in a file MP_Cache.php and put it in your application/libraries directory.
  • You can load the library with $this->load->library('MP_Cache')
  • You have to add a config setting in an autoloaded config file (like config.php) which sets the main cache directory relative to your application directory. For example:```php $config['mp_cache_dir'] = APPPATH.'mp_cache/';

**THE LIBRARY**
Credit goes to the CodeIgniter folks, most of the code was copy-pasted from the Output class & the DB Cache files.
```php
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');

class MP_Cache
{
    var $ci;
    var $path;
    
    function MP_Cache()
    {
        $this->ci =& get_instance();
        
        $this->path = $this->ci->config->item('mp_cache_dir');
        if ($this->path == '') return false;
    }
    
    function get($filename, $use_expires = true)
    {
        $cache_path = $this->path;
            
        if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
        {
            return FALSE;
        }
        
        // Build the file path.
        $filepath = $cache_path.$filename.'.cache';
        
        if ( ! @file_exists($filepath))
        {
            return FALSE;
        }
    
        if ( ! $fp = @fopen($filepath, FOPEN_READ))
        {
            return FALSE;
        }
            
        flock($fp, LOCK_SH);
        
        $cache = '';
        if (filesize($filepath) > 0)
        {
            $cache = unserialize(fread($fp, filesize($filepath)));
        }
        else
        {
            $cache = NULL;
        }
        
        flock($fp, LOCK_UN);
        fclose($fp);
        
        if (isset($cache['mp_cache_expires_time']) && $cache['mp_cache_expires_time'] < time() && $use_expires)
        {
            $this->delete($filename);
            return false;
        }
        
        // Return the cache
        log_message('debug', "MP_Cache retrieved: ".$filename);
        return $cache;
    }
    
    function write($values, $filename, $expires = 0)
    {
        $cache_path = $this->path;
        
        if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
        {
            return;
        }
        
        // check if filename contains dirs
        $subdirs = explode('/', $filename);
        if (count($subdirs) > 1)
        {
            array_pop($subdirs);
            $test_path = $cache_path.implode('/', $subdirs);
            
            // check if specified subdir exists
            if ( ! @file_exists($test_path))
            {
                // create non existing dirs, asumes PHP5
                if ( ! @mkdir($test_path)) return false;
            }
        }
        
        $cache_path .= $filename.'.cache';

        if ( ! $fp = @fopen&#40;$cache_path, FOPEN_WRITE_CREATE_DESTRUCTIVE&#41;)
        {
            log_message('error', "Unable to write MP_cache file: ".$cache_path);
            return;
        }
        
        // Add expires variable if its set
        if ($expires > 0)
            $values['mp_cache_expires_time'] = time() + $expires;
        
        if (flock($fp, LOCK_EX))
        {
            fwrite($fp, serialize($values));
            flock($fp, LOCK_UN);
        }
        else
        {
            log_message('error', "MP_Cache was unable to secure a file lock for file at: ".$cache_path);
            return;
        }
        fclose($fp);
        @chmod($cache_path, DIR_WRITE_MODE);

        log_message('debug', "MP_Cache file written: ".$cache_path);
    }
    
    function delete($filename)
    {
        $file_path = $this->path.$filename.'.cache';
        
        if (file_exists($file_path)) unlink&#40;$file_path&#41;;
    }
    
    function delete_all($dirname)
    {
        if (empty($this->path)) return false;

        $this->ci->load->helper('file');
        if (file_exists($this->path.$dirname)) delete_files($this->path.$dirname, TRUE);
    }
}