# 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. # Table of contents * <a href="#desc">Description</a> * <a href="#install">Installation</a> * <a href="#quick-setup">Quick setup</a> * <a href="#usage">Usage</a> * <a href="#advanced-setup">Advanced setup</a> * <a href="#misc">Miscellaneous</a> # <a id="desc">Description</a> 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 (on Russian)"](http://www.smira.ru/tag/memcached/). Some ideas are implemented in the real world (e.i. tag versions based on datetime and micro time, cache hit/set logging, cache locking) and part of them are not (atomic counter). # <a id="install">Installation</a> * As Symfony plugin * Installation $ ./symfony plugin:install sfCacheTaggingPlugin * Upgrading $ ./symfony cc $ ./symfony plugin:upgrade sfCacheTaggingPlugin * As a git submodule (master or devel branch) * Installation $ git submodule add git://github.com/fruit/sfCacheTaggingPlugin.git plugins/sfCacheTaggingPlugin $ git submodule init plugins/sfCacheTaggingPlugin * Upgrading $ cd plugins/sfCacheTaggingPlugin $ git pull origin master $ cd ../.. * Migrating $ ./symfony doctrine:migrate-generate-diff # <a id="quick-setup">Quick setup</a> _After quick setup you may be interested in "<a href="#advanced-setup">Advanced setup</a>"_ ## 1. Check "_sfCacheTaggingPlugin_" plugin is enabled (``/config/ProjectConfiguration.class.php``). [php] class ProjectConfiguration extends sfProjectConfiguration { public function setup () { # … other plugins $this->enablePlugins('sfCacheTaggingPlugin'); } } ## 2. Change default model class "_sfDoctineRecord_" to "_sfCachetaggableDoctrineRecord_" [php] <?php class ProjectConfiguration extends sfProjectConfiguration { # … public function configureDoctrine (Doctrine_Manager $manager) { sfConfig::set( 'doctrine_model_builder_options', array('baseClassName' => 'sfCachetaggableDoctrineRecord') ); } } Then rebuild your models: ./symfony doctrine:build-model ## 3. Configure "_view_cache_" and "_view_cache_manager_" in ``/config/factories.yml`` all: view_cache_manager: class: sfViewCacheTagManager view_cache: class: sfTaggingCache param: storage: class: sfFileTaggingCache param: automatic_cleaning_factor: 0 cache_dir: %SF_CACHE_DIR%/sf_tag_cache logger: class: sfFileCacheTagLogger param: file: %SF_LOG_DIR%/cache_%SF_ENVIRONMENT%.log format: "%char% %microtime% %key%%EOL%" ## 4. Add "_Cachetaggable_" behavior to the each model you want to cache Article: tableName: articles actAs: Cachetaggable: ~ ## 5. Enable cache and declare required helpers in ``/apps/%APP%/config/settings.yml``: dev: .settings: cache: true all: .settings: standard_helpers: - Partial - Cache # <a id="usage">Usage</a> ## How to cache partials? * Enable cache in ``/apps/%APP%/modules/%MODULE%/config/cache.yml``: _listing: enabled: true * Action template ``indexSuccess.php``: [php] <?php /* @var $articles Doctrine_Collection_Cachetaggable */ ?> <h1><?php __('Articles') ?></h1> <?php include_partial('articles/listing', array( 'articles' => $articles, 'sf_cache_tags' => $articles, )) ?> ## How to cache components? (one-table) * ``components.class.php`` [php] class articlesComponents extends sfComponents { public function executeListOfArticles ($request) { /* @var $articles Doctrine_Collection_Cachetaggable */ $articles = Doctrine::getTable('Article') ->createQuery('a') ->select('a.*') ->orderBy('a.id DESC') ->limit(3) ->execute(); $this->setContentTags($articles); $this->articles = $articles; } } * Action template: ``indexSuccess.php`` [php] <fieldset> <legend>Articles inside component</legend> <?php include_component('articles', 'listOfArticles'); ?> </fieldset> * Enable component caching in ``/apps/%APP%/modules/%MODULE%/config/cache.yml``: _listOfArticles: enabled: true ## How to cache components? (many-table, combining articles and comments 1:M relation) * ``components.class.php`` [php] class articlesComponents extends sfComponents { public function executeListOfArticlesAndComments($request) { $articles = Doctrine::getTable('Article') ->createQuery('a') ->addSelect('a.*, ac.*') ->innerJoin('a.ArticleComments ac') ->orderBy('a.id DESC') ->limit(3) ->execute(); $this->setContentTags($articles); $this->articles = $articles; } } * ``indexSuccess.php`` [php] <fieldset> <legend>Component (articles and comments)</legend> <?php include_component('article', 'listOfArticlesAndComments'); ?> </fieldset> * Enable component caching in ``/apps/%APP%/modules/%MODULE%/config/cache.yml`` _listOfArticlesAndComments: enabled: true ## How to cache action with layout? * Controller example: [php] class carActions extends sfActions { public function executeShow (sfWebRequest $request) { $car = Doctrine::getTable('car') ->find($request->getParameter('id')); $driver = Doctrine::getTable('driver') ->find($request->getParameter('driverId')); $this->setContentTags($car); $this->addContentTags($driver); $this->car = $car; $this->driver = $driver; } } * Enable caching in ``/apps/%APP%/modules/%MODULE%/config/cache.yml``: showSuccess: with_layout: true enabled: true ## How to cache action _without_ layout? * Action example [php] class carActions extends sfActions { public function executeShow (sfWebRequest $request) { $car = Doctrine::getTable('car')->find($request->getParameter('id')); $this->setContentTags($car); $this->car = $car; } } * Enable cache in ``/apps/%APP%/modules/%MODULE%/config/cache.yml``: show: with_layout: false enabled: true ## How to cache Doctrine_Records/Doctrine_Collections? * Does not depends on ``cache.yml`` file * To cache objects/collection with tags you need to enable result cache by calling ``Doctrine_Query::useResultCache()``: [php] class articleActions extends sfActions { public function executeArticles (sfWebRequest $request) { $articles = Doctrine::getTable('Article') ->createQuery() ->useResultCache() ->addWhere('lang = ?', 'en_GB') ->addWhere('is_visible = ?', true) ->limit(15) ->execute(); $this->articles = $articles; } } # <a id="advanced-setup">Advanced setup</a> _NB. Please read "<a href="#quick-setup">Quick setup</a>" before reading this._ ## Explaining ``/config/factories.yml`` all: view_cache_manager: class: sfViewCacheTagManager view_cache: class: sfTaggingCache param: # Content will be stored in Memcache # Here you can switch to any other backend # (see below "Restrictions" for more info) storage: class: sfMemcacheTaggingCache param: persistent: true storeCacheInfo: true host: localhost port: 11211 lifetime: 86400 logger: class: sfFileCacheTagLogger # to disable logger, set class to "sfNoCacheTagLogger" param: # All given parameters are default file: %SF_LOG_DIR%/cache_%SF_ENVIRONMENT%.log file_mode: 0640 dir_mode: 0750 time_format: "%Y-%b-%d %T%z" # e.i. 2010-Sep-01 15:20:58+0300 skip_chars: "" # Logging format # There are such available place-holders: # %char% - Operation char (see char explanation in sfCacheTagLogger::explainChar()) # %char_explanation% - Operation explanation string # %time% - Time, when data/tag was accessed # %key% - Cache name or tag name with its version # %microtime% - Micro time timestamp when data/tag was accessed # %EOL% - Whether to append \n in the end of line # # (Example: "%char% %microtime% %key%%EOL%") format: "%char%" > **Restrictions**: Backend's class should be inherited from ``sfCache`` class. Then, it should be implement ``sfTaggingCacheInterface`` (due to a ``Doctrine`` cache engine compatibility). Also, it should support the caching of objects and/or arrays. Therefor, plugin comes with additional extended backend classes: - sfAPCTaggingCache - sfEAcceleratorTaggingCache - sfFileTaggingCache - sfMemcacheTaggingCache - sfSQLiteTaggingCache - sfXCacheTaggingCache And bonus one: - sfSQLitePDOTaggingCache (based on stand alone sfSQLitePDOCache) ## Adding "Cachetaggable" behavior to the models Two major setups to pay attention: * **Model setup** * When object tag will be invalidated * How object tag will stored (tag naming) * **Relation setup** * What will happen with related objects in case root-object is deleted or updated * Choosing cascading type (deleteTags, invalidateTags) Explained behavior setup, file ``/config/doctrine/schema.yml``: Article: tableName: articles actAs: Cachetaggable: # If you have more then 1 unique column, you could pass all of them # as array (tag name will be based on all of them) # (default: [], primary keys will be auto-detected) uniqueColumn: [id, is_visible] # cache tag will be based on 2 columns # (e.g. "Article:5:01", "Article:912:00") # matches the "uniqueColumn" column order # (default: "") uniqueKeyFormat: '%d-%02b' # Column name, where object version will be stored in table # (default: "object_version") versionColumn: version_microtime # Option to skip object invalidation by changing listed columns # Useful for sf_guard_user.last_login or updated_at # (default: []) skipOnChange: - last_accessed # Invalidates or not object collection tag when any # record was updated (BC with v2.*) # Useful, when table contains rarely changed data (e.g. Countries, Currencies) # allowed values: true/false # (default: false) invalidateCollectionVersionOnUpdate: false # Useful option when model contains columns like "is_visible", "is_active" # updates collection tag, if one of columns was updated. # will not work if "invalidateCollectionVersionOnUpdate" is set to "true" # will not work if one of columns are in "skipOnChange" list. # (default: []) invalidateCollectionVersionByChangingColumns: - is_visible columns: id: type: integer(4) autoincrement: true primary: true culture_id: type: integer(4) notnull: false default: null category_id: type: integer(4) notnull: true slug: string(255) is_visible: boolean(true) is_moderated: boolean(false) last_accessed: date(25) relations: Culture: class: Culture local: culture_id foreign: id foreignAlias: Articles type: one foreignType: many # Cascading type chosen "invalidateTags" # Due to foreign key "onDelete" type is "SET NULL" cascade: [invalidateTags] Category: class: Category local: category_id foreign: id foreignAlias: Categories type: one foreignType: many # Cascading type chosen "deleteTags" # Due to foreign key "onDelete" type is "CASCADE" cascade: [deleteTags] Culture: tableName: cultures columns: id: type: integer(4) autoincrement: true primary: true lang: string(10) is_visible: boolean(true) relations: Articles: onDelete: SET NULL onUpdate: CASCADE Category: tableName: categories columns: id: type: integer(4) autoincrement: true primary: true name: string(127) relations: Articles: onDelete: CASCADE onUpdate: CASCADE ## Explained ``sfCacheTaggingPlugin`` options (file ``/config/app.yml``): all: sfCacheTagging: # Tag name delimiter # (default: ":") model_tag_name_separator: ":" # Version of precision # 0: without micro time, version length 10 digits # 5: with micro time part, version length 15 digits # allowed decimal numbers in range [0, 6] # (default: 5) microtime_precision: 5 # Callable array # Example: [ClassName, StaticClassMethod] # useful when tag name should contains extra information # (e.g. Environment name, or application name) # (default: []) object_class_tag_name_provider: [] ## Tag manipulations Hire is all available methods you can call inside sfComponent & sfAction to manage tags: - setContentTags (mixed $tags) - addContentTags (mixed $tags) - getContentTags () - removeContentTags () - setContentTag (string $tagName, string $tagVersion) - hasContentTag (string $tagName) - removeContentTag (string $tagName) - disableCache (string $moduleName = null, string $actionName = null) - addDoctrineTags (mixed $tags, Doctrine_Query $q, array $params = array()) More about is you could find in ``sfViewCacheTagManagerBridge.class.php`` Component example: [php] <?php class articlesComponents extends sfComponents { public function executeList ($request) { $articles = ArticleTable::getInstance()->findAll(); $this->setContentTags($articles); # Appending tags to already set $articles tags $banners = BannerTable::getInstance()->findByCategoryId(4); $this->addContentTags($articles); # adding only Culture collection tag "Culture" # useful when page contains all cultures output in form widget $this->addContentTags(CultureTable::getInstance()); # adding personal tag $this->addContentTag('Portal_EN', sfCacheTaggingToolkit::generateVersion()); # deleting added before tag $this->removeContentTag('Article:31'); # printing all set tags, excepting removed one var_dump($this->getContentTags()); $this->articles = $articles; $this->banners = $banners; } } ## Configurating Doctrine`s query cache Remember to enable Doctrine query cache in production: [yml] # config/app.yml dev: doctrine: query_cache: ~ prod: doctrine: query_cache: class: Doctrine_Cache_Apc # or another backend class Doctrine_Cache_* param: prefix: doctrine_dql_query_cache lifetime: 86400 And plug in query cache: [php] <?php class ProjectConfiguration extends sfProjectConfiguration { public function configureDoctrine (Doctrine_Manager $manager) { $doctrineQueryCache = sfConfig::get('app_doctrine_query_cache'); if ($doctrineQueryCache) { list($class, $param) = array_values($doctrineQueryCache); $manager->setAttribute( Doctrine_Core::ATTR_QUERY_CACHE, new $class($param) ); if (isset($param['lifetime'])) { $manager->setAttribute( Doctrine_Core::ATTR_QUERY_CACHE_LIFESPAN, (int) $param['lifetime'] ); } } } } ## Clarifying Doctrine`s result cache Plugin contains universal proxy class ``Doctrine_Cache_Proxy`` to connect Doctrine cache mechanisms with Symfony's one. This mean, when you setup "storage" cache back-end to file cache, [Doctrine`s result cache](http://www.doctrine-project.org/projects/orm/1.2/docs/manual/caching/en#query-cache-result-cache:result-cache) will use it to store cached ``DQL`` results. To enable result cache use: $q->useResultCache(); Set hydration to ``Doctrine_Core::HYDRATE_RECORD`` (NB! using another hydrator, its impossible to cache ``DQL`` result with tags.) $q ->setHydrationMode(Doctrine_Core::HYDRATE_RECORD) ->execute(); // or $q->execute(array(), Doctrine_Core::HYDRATE_RECORD); Cached ``DQL`` results will be associated with all linked tags based on query results. # <a id="misc">Miscellaneous</a> ## New in v4.0.0: * Update: Many API changes * New: Cascading tag deletion through the model relations [GH-6](https://github.com/fruit/sfCacheTaggingPlugin/issues#issue/6) * New: Option ``invalidateCollectionVersionByChangingColumns`` to setup ``Cachetaggable`` behavior [GH-8](https://github.com/fruit/sfCacheTaggingPlugin/issues#issue/8) * New: New methods in the sfComponent to add collection tags [GH-10](https://github.com/fruit/sfCacheTaggingPlugin/issues#issue/10) * New: ``Doctrine_Record::link`` and ``Doctrine_Record::unlink`` updates refTable's tags * New: Added default plugin's app.yml config * Fixed: ``skipOnChange`` did not work properly ## Limitations / Specificity * In case, when model has translations (I18n behavior), it is enough to add ``Cachetaggable`` behavior to the root model. I18n behavior should be free from ``Cachetaggable`` behavior. * You can`t pass ``I18n`` table columns to the ``skipOnChange``. * Doctrine ``$q->count()`` can't be cached with tags * Be careful with joined I18n tables, cached result may differs from the expected. Due the [unresolved ticket](http://trac.symfony-project.org/ticket/7220) it *could be* impossible. ## TDD * Environment: PHP 5.3 * Unit tests: 12 * Functional tests: 30 * Checks: 1330 * Code coverage: 95% ## Contribution * [Repository (GitHub)](http://github.com/fruit/sfCacheTaggingPlugin "Repository (GitHub)") * [Issues (GitHub)](http://github.com/fruit/sfCacheTaggingPlugin/issues "Issues") ## Contacts ## * @: Ilya Sabelnikov `` <fruit dot dev at gmail dot com> `` * skype: ilya_roll