sfCacheTaggingPlugin - 1.1.0

Cache tagging plugin compatible with any cache backends

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


« Back to the Plugins Home

Signin


Forgot your password?
Create an account

Tools

Stats

advanced search
Information Readme Releases Changelog Contribute
Show source

sfCacheTaggingPlugin

The sfCacheTaggingPlugin is a Symfony plugin, that helps to store cache with associated tags and to keep cache content up-to-date based by incrementing tag version when cache objects are edited/removed or new objects are ready to be a part of cache content.

Description

Tagging a cache is a concept that was invented in the same time by many developers (Andrey Smirnoff, Dmitryj Koteroff and, perhaps, by somebody else)

This software was developed inspired by Andrey Smirnoff`s theoretical work "Cache tagging with Memcached (in Russian)". Some ideas are implemented in the real world (i.e. tag versions based on datetime and microtime, cache hit/set logging, cache locking) and part of them are not (atomic counter).

Repository

Installation

Install the plugin

$ ./symfony plugin:install sfCacheTaggingPlugin

Setup

  1. Check sfCacheTaggingPlugin plugin is enabled (/config/ProjectConfiguration.class.php)

    <?php
    
    class ProjectConfiguration extends sfProjectConfiguration
    {
      public function setup()
      {
        # … other plugins
        $this->enablePlugins('sfCacheTaggingPlugin');
      }
    }
    
  2. Create a new file /config/factories.yml or edit each application`s level /apps/%app%/config/factories.yml file

    Cache tagging works for you each time you save/update/delete Doctrine record or fetch them from DB. So you should enable caching (sf_cache: true) in all applications you work with. I recommend you to create default factories.yml for all applications you have by creating a file /config/factories.yml (bellow that will be this file content). Symfony will check this file and load it as a default factories.yml configuration to all applications you have in the project.

    This is /config/factories.yml content (you can copy&paste this code into your brand new created file) or merge this config with each application`s factories.yml (applications, where you need the data to be fetched/written from/to cache)

    Example: file /config/factories.yml

    all:
      view_cache:
        class: sfTagCache
        param:
          logging: true                 # logging is enabled ("false" to disable)
          cache:
            class: sfMemcacheCache      # Content will be stored in Memcache
                                        # Here you can switch to any other backend
                                        # (see Restrictions block for more info)
            param:
              persistent: true
              storeCacheInfo: false
              host: localhost
              port: 11211
              timeout: 5
              lifetime: 86400
          locker:
            class: sfAPCCache           # Locks will be stored in APC
                                        # Here you can switch to any other backend sf*Cache
                                        # (see Restrictions block for more info)
            param: {}
    
      view_cache_manager:
        class: sfViewCacheTagManager          # Extended sfViewCacheManager class
        param:
          cache_key_use_vary_headers: true
          cache_key_use_host_name:    true
    

    Easter eggs: If you remove "all_view_cache_param_locker" section, locker will be the same as section "all_view_cache_param_cache".

    Restrictions: Backend`s class should be inheritable from sfCache class. Also, it should support the caching of objects and/or arrays.

    Bonus: In additional to this plugin comes sfFileTaggingCache and sfSQLiteTaggingCache which are ready to use as backend class.

    This classes already have serialization/unserialization support.

  3. Edit Symfony`s predefined application`s level factories.yml files

    If you have edited each application`s level factories.yml file in 2nd step - go to 4th step.

    In each application you want to use cache tagging please remove "all_view_cache_manager" section (you have already configured it in global /config/factories.yml file).

  4. Add "Cachetaggable" behavior to each model, which you want to be a part of cache content.

    Exmaple: file /config/doctrine/schema.yml

    BlogPost:
      tableName: blog_post
      actAs:
        Cachetaggable: ~
        #Cachetaggable:
        #  uniqueColumn: id               # you can customize unique column name (default is "id")
        #  versionColumn: object_version  # you can customize column name to store versions (default is "object_version")
        #  uniqueKeyFormat: '%d'          # you can customize key format (default is "%d")
        #
        #  # if you have more then 1 unique column, you could pass all of them
        #  # as array (tag name will be based on them)
        #
        #  uniqueColumn: [id, is_enabled]
        #  uniqueKeyFormat: '%d-%02b'      # the order of unique columns
        #                                  # matches the "uniqueKeyFormat" template variables order
    
      columns:
        id:
          type: integer
          primary: true
          autoincrement: true
          unsigned: true
        title: string(255)
      relations:
        BlogPostComment:
          class: BlogPostComment
          type: many
          local: id
          foreign: blog_post_id
    
    BlogPostComment:
      tableName: blog_post_comment
      actAs:
        Cachetaggable: ~
      columns:
        id:
          type: integer
          primary: true
          autoincrement: true
          unsigned: true
        blog_post_id:
          type: integer
          unsigned: true
          notnull: false
        author: string(20)
        message: string(255)
      indexes:
        blog_post_id: { fields: [blog_post_id] }
      relations:
        BlogPost:
          onUpdate: CASCADE
          onDelete: CASCADE
    
  5. Enable cache in settings.yml and add additionals helpers to standart_helpers

    To setup cache, often, used a separate environment named "cache", but in the same way you can do it in any other environments which you have.

    prod:
      .settings:
        cache: true
    
    cache:
      .settings:
        cache: true
    
    all:
      .settings:
        cache: false
    

    Add helpers to the frontend application

    all:
      .settings:
        standard_helpers:
          - Partial     # symfony build in helper (mandatory)
          - CacheTag    # sfCacheTaggingPlugin helper (mandatory)
          - PartialTag  # sfCacheTaggingPlugin helper (mandatory)
    
  6. Customize sfCacheTaggingPlugin in the /config/app.yml

    all:
      sfcachetaggingplugin:
        template_lock: "lock_%s"    # name for locks
        template_tag: "tag_%s"      # name for tags
    
        microtime_precision: 5      # version precision (0, or positive number)
                                    # (0 - without micro time, version will be 10 digits length)
                                    # (5 - with micro time part, version will be 15 digits length)
    
        lock_lifetime: 2            # seconds to keep lock, if failed to unlock after locking it
    

Using

  • Native use:

    # Somewhere in the frontend, you need to print out latest posts
    $posts = Doctrine::getTable('BlogPost')
      ->createQuery()
      ->orderBy('id DESC')
      ->limit(3)
      ->execute();
    
    # write data to the cache ($posts is instance of the Doctrine_Collection_Cachetaggable)
    $tagger->set('my_posts', $posts, 86400, $posts->getTags());
    
    # fetch latest post to edit it
    $post = posts->getFirst();
    
    # prints something like "126070596212512"
    print $post->getObjectVersion();
    
    $post->setTitle('How to use sfCacheTaggingPlugin');
    
    # save and update/upgrade version of the tag
    $post->save();
    
    # prints something like "126072290862231" (new version of the tag)
    print $post->getObjectVersion();
    
    # will return null
    # $post object was updated, so, all $posts in cache "my_posts” is invalidated automatically)
    if ($data = $tagger->get('my_posts'))
    {
      # this block will not be executed
    }
    
    # save new data to the cache
    $tagger->set('my_posts', $posts, null, $posts->getTags());
    
    # will return data (objects are fresh)
    if ($data = $tagger->get('my_posts'))
    {
      # this code block will be executed
    }
    
    $post = new BlogPost();
    $post->setTitle('New post should be in inserted to the cache results');
    $post->save();
    
    # will return null, because 'my_posts' cache knows that it contains BlogPost objects
    # and listens on new objects with same type that are newer
    if ($data = $tagger->get('my_posts'))
    {
      # this block will not be executed
    }
    
    $posts = Doctrine::getTable('BlogPost')
      ->createQuery()
      ->orderBy('id DESC')
      ->limit(3)
      ->execute();
    
    $tagger->set('my_posts', $posts, null, $posts->getTags());
    
    # will return data
    if ($data = $tagger->get('my_posts'))
    {
      # this block will be executed
    }
    
  • non-Doctrine use:

    indexSuccess.php
    <fieldset>
      <legend>Daylight</legend>
    
      <?php if (! cache_tag('daylight_content')) { ?>
    
        <h1>Text to cache No-<?php rand(1, 1000000) ?></h1>
    
        Text text text text text text text text text text text text text.
        <?php cache_tag_save(array('sun' => time(), 'moon' => time())); ?>
      <?php } ?>
    
    </fieldset>
    
    code.php
      # when you want to update Daylight content
      $this->getContext()->getViewCacheManager()->getTagger()->setTag('moon', time(), 86400);
    

  • Using with partials:

    <fieldset>
      <legend>Partial</legend>
    
      <?php if (! cache_tag('latest-blog-posts-index-on-page')) { ?>
        <?php foreach ($posts as $post) { ?>
          <?php include_partial('posts/one_post', array('post' => $post) ?>
        <?php } ?>
        <?php cache_tag_save($posts->getTags()); ?>
      <?php } ?>
    
    </fieldset>
    
  • Using with components (simple):

    indexSuccess.php
    <fieldset>
      <legend>Component</legend>
    
      <?php if (! cache_tag('latest-blog-posts-index-on-page')) { ?>
        <?php include_component_tag('posts', 'listOfPosts') ?>
        <?php cache_tag_save(); ?>
      <?php } ?>
    
    </fieldset>
    
    components.class.php
    <?php
    
    class postsComponents extends sfComponents
    {
      public function executeListOfPosts($request)
      {
        $posts = Doctrine::getTable('BlogPost')
          ->createQuery('bp')
          ->select('bp.*')
          ->orderBy('bp.id DESC')
          ->limit(3)
          ->execute();
    
        $this->getContext()->getViewCacheManager()->setTags($posts->getTags());
    
        $this->posts = $posts;
      }
    }
    
  • Using with components (complex - Combining posts with comments)

    components.class.php
    <fieldset>
      <legend>Component (posts and comments)</legend>
    
      <?php if (! cache_tag('posts_and_comments')) { ?>
        <?php include_component_tag('post', 'listOfPostsAndComments') ?>
        <?php cache_tag_save(); ?>
      <?php } ?>
    
    </fieldset>
    
    components.class.php
    <?php
    
    class postsComponents extends sfComponents
    {
      public function executeListOfPostsAndComments($request)
      {
        $posts = Doctrine::getTable('BlogPost')
          ->createQuery('bp')
          ->addSelect('bp.*, bpc.*')
          ->innerJoin('bp.BlogPostComments bpc')
          ->orderBy('bp.id DESC')
          ->limit(3)
          ->execute();
    
        foreach ($posts as $post)
        {
          # 1st variant (shorter)
          # our cache should be updated if one of comment will be edited/deleted
          # therefore, we are collecting comment's tags
    
          $posts->addTags($post->getBlogPostComment()->getTags());
          # or shorter
          # $posts->addTags($post->getBlogPostComment());
        }
    
        $this->getContext()->getViewCacheManager()->setTags($posts->getTags());
    
        $this->posts = $posts;
      }
    }
    

Limitations / Peculiarities

  • Do not use Doctrine::getTable('BlogPost')->delete()->where('id > ?', 10)->execute(); Object deletion should be performed using Doctrine_Record::delete() method. This is necessary to remove object tags when object is fizicaly removed. Otherwise your cache will be not expired.

  • In case, when model has translations (I18n behavior), it is enough to add "actAs: Cachetaggable" to the model. I18n behavior should be free from Cachetaggable behavior.

Unit test

  • 400 of 400 successfully completed

Every combination is tested (data backend / locker backend) of listed below:

  • sfMemcacheCache
  • sfAPCCache
  • sfSQLiteTaggingCache - file (extended from sfSQLiteCache)
  • sfSQLiteTaggingCache - memory (extended from sfSQLiteCache)
  • sfFileTaggingCache (extended from sfFilecache)

Partialy tested listed below backends. If anyone could help me to run functional tests for this backends, I will be thankful to you:

  • sfXCacheCache
  • sfEAcceleratorCache

Contacts

  • @: Ilya Sabelnikov <fruit dot dev at gmail dot com>
  • IRC:
    • server: irc.freenode.net
    • channel: #symfony
    • username: fruit
  • skype: ilya_roll