# 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](http://www.smira.ru), [Dmitryj Koteroff](http://dklab.ru/) and, perhaps, by somebody else) This software was developed inspired by Andrey Smirnoff`s theoretical work ["Cache tagging with Memcached (in Russian)"](http://www.smira.ru/tag/memcached/). 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 ## * plugin repository @ github [http://github.com/fruit/sfCacheTaggingPlugin](http://github.com/fruit/sfCacheTaggingPlugin "Repository") * plugin tickets @ github [http://github.com/fruit/sfCacheTaggingPlugin/issues](http://github.com/fruit/sfCacheTaggingPlugin/issues "Issues") ## Installation ## Install the plugin $ ./symfony plugin:install sfCacheTaggingPlugin ## Versions ## ### Since 1.3.0 - Resolved problem with caching empty collections with no tags - extra methods and validations are added to ``Doctrine_Collection_Cachetaggable`` class ### Since 1.2.0 Now all dql queries (update/delete) also updates version column and thier tags ## Setup ## 1. Check ``sfCacheTaggingPlugin`` plugin is enabled (``/config/ProjectConfiguration.class.php``) <?php class ProjectConfiguration extends sfProjectConfiguration { public function setup() { # … other plugins $this->enablePlugins('sfCacheTaggingPlugin'); } } 1. 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. 1. 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). 1. 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 1. 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) 1. 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 log_format_extended: 0 # (0 - print 1 char) # (1 - print 1 char + tag key) # # Char explanation: # "g": cache not found # "G": cache is found # "l": could not lock cache # "L": cache is locked # "s": could set new values to the cache # "S": new values is setted to the cache # "u": could not unlock cache # "U": cache is unlocked # "t": cache tag was expired # "T": cache tag is up-to-date # # Chars in lower case indicate negative operation # Chars in upper case indicate positive operation ## 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(); /* @var $tagger sfViewCacheTagManager */ $tagger = $this->getContext()->getViewCacheManager(); # 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); </fieldset> * ### 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 ## * 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 ## * 861 of 861 test are 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