A simple widget cache

I wanted a simple way to use any cache from within a widget, possibly even a cache that is privately constructed and owned by the widget itself. Sometimes you need a cache that is specific to a widget, such as a menu - having a general cache for all widgets (and possibly other fragments/pags) may not be a good approach, since flushing that cache would cause all manner of cached content to be flushed, even if it wasn’t invalidated.

My approach is a behavior, that I can attach to any CCache instance, which allows me to simply begin() and end() a block of content - it supports default settings for expiration and dependency, so you can configure defaults for these in advanced, or override them when you call begin().

Here’s the code:




<?php


/**

 * The behavior extends any of the CCache subclasses with begin() and end() methods,

 * allowing for quick, simple caching in widgets, views or actions.

 *

 * You are responsible for introducing variability, e.g. appending variables to the

 * cache $id for uniqueness.

 *

 * Example configuration:

 *

 * <code>

 *   'components'=>array(

 *     ...

 *     'widgetCache'=>array(

 *       'class'=>'CDummyCache',

 *       'behaviors'=>array('output'=>'GCacheOutputBehavior'),

 *       'cachePath'=>APP_PATH.DIRECTORY_SEPARATOR.'runtime'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'widgets',

 *     ),

 *   ),

 * </code>

 * 

 * Example Widget using the widgetCache configured in the example above:

 *

 * <code>

 *   class Menu extends CWidget

 *   {

 *     public $active=null;

 *     

 *     public function run()

 *     {

 *       if (Yii::app()->widgetCache->begin(__CLASS__.'.'.$this->active))

 *       {

 *         $this->render('menu', array(

 *           ...

 *         ));

 *         Yii::app()->widgetCache->end();

 *       }

 *     }

 *   }

 * <code>

 *

 */

class GCacheOutputBehavior extends CBehavior

{

  protected $_stack=array();

  

  /**

   * Default $expire setting for the begin() method

   */

  public $expire=0;

  

  /**

   * Default $dependency setting for the begin() method

   */

  public $dependency=null;

  

  /**

   * Begins caching. This method will display cached content if it is availabe. If not,

   * it will start caching and would expect a call to end(), to save the content into cache.

   *

   * @return boolean returns true if your code needs to render the cached content, or

   * false if the cached content was already available to output.

   */

  public function begin($id, $expire=null, $dependency=null)

  {

    if ($this->getOwner()->offsetExists($id))

    {

      echo $this->getOwner()->get($id);

      return false;

    }


    $this->_stack[] = array(

      $id,

      null,

      $expire===null ? $this->expire : $expire,

      $dependency===null ? $this->dependency : $dependency,

    );

    

    ob_start();

    ob_implicit_flush(false);

    

    return true;

  }

  

  /**

   * Ends caching.

   */

  public function end()

  {

    if (count($this->_stack)==0)

      throw new CException(__CLASS__."::end() : end() without matching begin()");

    

    $params = array_pop($this->_stack);

    $params[1] = ob_get_flush();

    

    call_user_func_array(array($this->getOwner(), 'set'), $params);

  }

}



Of course, much better than this would be using the COutputCache widget, so that this would work with dynamic content… hmmm…

What about declaring a normal cache component named ‘widgetCache’ and using COutputCache as follows:




if($this->controller->beginCache('xyz', array('cacheID'=>'widgetCache'))) {

      ...content to be cached...

      $this->controller->endCache();

}



The thing I don’t like about that approach is, you need to explicitly configure a cache for every widget. I would like for certain widgets to be able to cache themselves, by declaring their own throw-away cache instance, rather than requiring you to configure it every time.

A good example would be a widget that parses an RSS feed - such a widget is basically useless without caching, as you would have to wait for the feed to load and parse every time, so it makes sense to have caching built into the widget itself. Also, when the widget has it’s own private cache, it can be flushed exclusively, without flushing the cache of any other widgets, pages, fragments, etc.

Here’s another take on a better behavior:




<?php


/**

 * The behavior extends any of the CCache subclasses with begin() and end() methods,

 * allowing for quick, simple caching in widgets, views or actions.

 *

 * You are responsible for introducing variability, e.g. appending variables to the

 * cache $id for uniqueness.

 *

 * Example configuration:

 *

 * <code>

 *   'components'=>array(

 *     ...

 *     'widgetCache'=>array(

 *       'class'=>'CDummyCache',

 *       'behaviors'=>array('output'=>'GFragmentCacheBehavior'),

 *       'cachePath'=>APP_PATH.DIRECTORY_SEPARATOR.'runtime'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'widgets',

 *     ),

 *   ),

 * </code>

 * 

 * Example Widget using the widgetCache configured in the example above:

 *

 * <code>

 *   class Menu extends CWidget

 *   {

 *     public $active=null;

 *     

 *     public function run()

 *     {

 *       if (Yii::app()->widgetCache->begin(__CLASS__.'.'.$this->active))

 *       {

 *         $this->render('menu', array(

 *           ...

 *         ));

 *         Yii::app()->widgetCache->end();

 *       }

 *     }

 *   }

 * <code>

 *

 */

class GFragmentCacheBehavior extends CBehavior

{

  /**

   * @var array Default properties for the output cache widget.

   */

  public $properties=array(

  );

  

  /**

   * @var int default expiration time (in seconds) - defaults to 3600 seconds (1 hour)

   * @see COutputCache::$duration

   */

  public $duration=3600;

  

  /**

   * Begins fragment caching.

   * This method will display cached content if it is availabe.

   * If not, it will start caching and would expect a {@link endCache()}

   * call to end the cache and save the content into cache.

   *

   * A typical usage of fragment caching is as follows,

   *

   * <pre>

   *   if(Yii::app()->myCache->begin($id))

   *   {

   *     // ...generate content here

   *     Yii::app()->myCache->end();

   *   }

   * </pre>

   * 

   * @param string a unique ID identifying the fragment to be cached.

   * @param array initial property values for {@link COutputCache}.

   * @return boolean whether we need to generate content for caching. False if cached version is available.

   * @see end()

   */

  public function begin($id, $duration=null, $properties=array())

  {

    $properties = $this->properties;

    

    $properties['id'] = $id;

    $properties['cache'] = $this->getOwner();

    

    $properties['duration'] = $duration===null ? $this->duration : $duration;

    

    $cache = Yii::app()->getController()->beginWidget('GOutputCache', $properties);

    

    if($cache->getIsContentCached())

    {

      $this->end();

      return false;

    }

    else

      return true;

  }


  /**

   * Ends fragment caching.

   * @see begin()

   */

  public function end()

  {

    Yii::app()->getController()->endWidget('GOutputCache');

  }

}


class GOutputCache extends COutputCache

{

  /**

   * @var CCache overrides the $cacheID with a custom CCache component reference

   */

  public $cache;

  

  /**

   * @return ICache the cache used for caching the content.

   */

  protected function getCache()

  {

    return isset($this->cache) ? $this->cache : Yii::app()->getComponent($this->cacheID);

  }

}




This implementation is much cleaner, and uses COutputCache to do the actual caching.

I had to override COutputCache::getCache() to allow injection of a custom CCache instance - you might want to consider integrating this tiny enhancement for COutputCache? Dependency injection is generally healthy :slight_smile:

On an unrelated note, it seems this forum is somehow mettling with the case in portions of the code?! "class GFragmentCacheBehavior extends CBehavior" comes out "GFragmentCachebehavior extends Cbehavior" in a code block…

Edit: Yikes, I can’t paste this at all - looks like the content is stored in correct case, it’s still there when I hit “edit”, but the output is messed up…

Yes this is forum issue. IPB knows about it but didn’t fixed it yet I think.

// Just found this. Seems like they won’t fix it anymore. “Working as Intended” …

Guess they intended for it to suck :wink:

Posted a few updates to the behavior above - default duration can now be set directly…

Not quite sure if I understood you correctly. If COutputCache has a writable property ‘cache’, do you still need this behavior? It means you can have the following code:




if($this->controller->beginCache('xyz', array('cache'=>$customCache))) {

      ...content to be cached...

      $this->controller->endCache();

}



That would be nice, but $cache is currently read-only.

hi,mindplay

I would like to know how to use your widget cache.

my code:

main.php


'widgetCache'=>array(

     'class'=>'CDummyCache',

     'behaviors'=>array('output'=>'GFragmentCacheBehavior'),

'cachePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'widgets',

),

my widget MyTop.php code


    public function run()

    {

		if (Yii::app()->widgetCache->begin(__CLASS__.'.'.$this->active))

		{

	    	$arealist=Area::model()->findAll('display=1 AND is_hot=1');

			$this->render('pagetop',array('arealist'=>$arealist));

			Yii::app()->widgetCache->end();

		}

    }

error:

help me ,ths

I form china

You configure a CDummyCache - I guess it doesn’t have a $cachePath property. It won’t give you real caching either way - refer to the documentation pls…