Saturday, October 22, 2011

FCache Revisited

After some refactoring I found a way to use a closure for my content cacher instead of fake "code blocks". Now using it is much easier to read and understand, the code that needs to be cached is passed as the implementation of an anonymous function. The new cache_block() method:
/**
 * cache a block of code
 * @param string $key the cache key
 * @param int $seconds the number of seconds before a cache file is outdated
 * @param function the closure to cache the contents of
 */
public static function cache_block( $key, $seconds = null, $function ) {
  if ( self::is_cached( $key, $seconds ) ) {
    echo file_get_contents( self::get_path( $key ) );
    return true;
  }
  self::init_buffer();
  call_user_func( $function );
  echo self::save_buffer( $key );
  return true;
}
And an example usage of the new method:
public function demo() {
  $self = $this; # php5.3 cant pass $this with use(), but 5.4 will be able to!

  \appcore\fcache::cache_block('demo_cache_1', 2, function() use ($self) {
    $self->show('header');
    $self->show('index');
    \appcore\fcache::cache_block('demo_cache_2', 4, function() use ($self) {
      $self->show('footer');
    });
  });
}
This creates 2 cached elements, 'demo_cache_1' and 'demo_cache_2'. The demo_cache_1 element expires after 2 seconds and the demo_cache_2 after 4 seconds. The means when the contents of the demo_cache_1 expire and the block is re-executed, the contents of demo_cache_2 may be re-used if they havn't expired yet in the creation of the outer block cache. Here's the entire fcache class refactored:
namespace appcore;

/**
 * file caching methods. this is a key/value implementation.
 * @author smassey
 * @since may 5th 2011
 * may 22nd 2011 - added events
 * oct 16th 2011 - refactored + added the cache_block() method
 */
class fcache extends namespace\base\object {
  private function __construct() {}
  private function __clone() {}

  /**
   * check if a file is cached and optionaly non expired
   * @param mixed $key
   * @param int $seconds
   * @return false if a cached file for the given key isnt found
   * or if the file exits and is outdated. return true otherwise. 
   */
  public static function is_cached( $key, $seconds = null ) {
    if ( ! file_exists( self::get_path( $key ) ) ) return false;
    return ( $seconds ) ? ( ! self::is_outdated( $key, $seconds ) ) : true;
  }

  /**
   * get a cached file
   * @param mixed $key the key of the cached file
   * @param int $exp_seconds the expire time of the cached file in seconds
   */
  public static function get_cache( $key ) {
    if (self::is_cached( $key )) {
      \appcore\events::send('trace', 'found cache ' . md5($key));
      return file_get_contents( self::get_path( $key ) );
    }
    return false;
  }

  /**
   * save contents into a cached file
   * @param $key the cache key
   * @param $value the contents to cache
   */
  public static function cache( $key, $value ) {
    $path = self::get_path( $key );
    if ( file_put_contents( $path, $value ) === false ) {
      throw new \Exception("failed to write file: $path");
    }
  }

  /**
   * cache a block of code
   * @param string $key the cache key
   * @param int $seconds the number of seconds before a cache file is outdated
   * @param function the closure to cache the contents of
   */
  public static function cache_block( $key, $seconds = null, $function ) {
    if ( self::is_cached( $key, $seconds ) ) {
      echo file_get_contents( self::get_path( $key ) );
      return true;
    }
    self::init_buffer();
    call_user_func( $function );
    echo self::save_buffer( $key );
    return true;
  }

  /**
   * start the buffers
   */
  public static function init_buffer() {
    ob_start();
  }

  /**
   * save a buffer
   * @param $key the cache key
   */
  public static function save_buffer( $key ) {
    \appcore\events::send('trace', 'saving buffer to file cache');
    $contents = ob_get_contents();
    ob_end_clean();
    self::cache( $key, $contents );
    return $contents;
  }

  /**
   * determine the real full path + filename for a cache file
   * @param string $key the cache key
   * @returns the full path + filename
   */
  protected static function get_path( $key ) {
    return CACHE_PATH . md5($key) . '.cache.php';
  }
 
  /**
   * deterine if a file is outdated
   * @param string $key the cached key
   * @param int $seconds number of seconds to check the caches age against
   */
  protected static function is_outdated( $key, $seconds = false ) {
    if ( ! $seconds ) return false;
    $file_stats = stat( self::get_path( $key ) );
    if ( ( time() - $file_stats['mtime'] ) > $seconds ) return true;
    return false;
  }
}