A new cache subsystem was introduced in Cotonti Siena which is currently under intensive development and can be obtained from SVN trunk. This article will help you to understand the idea of Cotonti cache, how it works and how you can use it in your modules, plugins or core programming.
#1. Design Overview
What would you need a cache for in a PHP application? Well, first look how a PHP application handles every request: it processes user input, queries the database, reads data from disk and generates complex HTML output... all using local variables which exist only for a few milliseconds and then they are gone, so that the next request needs to do all the job again. A cache helps you to save some time and effort by keeping temporary results which can be used by other requests as well.
#1.1. Components and Layers
In Cotonti cache is a bit more complex thing than in other CMF. It has 3 layers available all at once for different purposes and a solid controller to provide easy access to all of its functions. The following component diagram describes its logical structure overview:
When writing modules or plugins you see it all from the “Cotonti CMS” component and use the “Cache Controller” interface to all the 3 layers which are all very similar from this point of view. But each of them has a different use case:
Disk Cache stores data entries as files in local folders. You should use it to store quite large blocks of data that are modified quite rarely, such as entire pages. For example, disk cache is used by XTemplate to store pre-parsed templates.
Comparative Specs: fast reading and med writing of large data objects, slow reading and writing of small objects, individual element access only.
Database Cache stores all of its data in a database table. It has a speed advantage over disk cache when it loads all of the required data with just one SQL query, but it may cause a slow down if your database grows too large or you modify DB cache contents frequently. So, this is general purpose cache for mid-size data which is persistent or is kept the same for at least a few minutes.
Comparative Specs: fast reading of entire realms of objects, slow access to individual objects, medium writing speed.
Memory Cache is available on hosts which provide shared memory PHP extensions, such as APC, eAccelerator, Memcached and XCache. It provides very fast access to individual objects, though if you need to get many objects at once, the database cache would do it faster with one call. But the writing speed is much better than with DB. So, use memory cache to store temporary objects which have short lifetime and are frequently modified.
Comparative Specs: fast reading and writing of individual objects.
All cache entries are grouped into realms. Realms are similar to domains or namespaces: cache entries must have unique names within a realm, the realm name is used to reference a cache entry as well as the entry name. Realms are illustrated by the next objects diagram:
Normally a realm means a module or a plugin the data belongs to, so each module has its own cache realm and so can have a plugin if necessary. This way you can load or remove all cache entries for your module at once. There are 2 special realms though which are always loaded automatically into global variable scope: 'system' (used by Cotonti core) and 'cot' (a default general-purpose namespace).
#1.3. Event Bindings
As you know, cache entries need to be refreshed somewhen. Traditionally it is done when entry's lifetime is over, so it gets erased. On one hand if the lifetime is too short, then there are more refreshes than necessary. On the other hand if the lifetime is too long, the cache runs out of date. Surely you can (and should) refresh cache manually in the most appropriate places of your code (for example, a page cache should be updated when adding or editing that page). But what if the entry needs to be invalidated in case of some optional event you cannot directly control from your code? In this case Event Bindings will help you.
Event bindings are mostly for third-party plugins and modules. When a plugin is installed, it can bind some cache fields (realm + id) to some events. The same event can handle several fields and the same field may be invalidated by multiple events. When a plugin is uninstalled, it must remove its bindings as well.
Most of hooks (yes, which are used by plugins) are considered as “events”. Events also can be triggered directly via cache controller. When event is triggered, the controller checks if there are any bindings associated with it and invalidates cache entry referenced by each binding it finds.
#2. How to choose a cache layer
There are 3 cache layers: $cache->db, $cache->disk and $cache->mem. But what should you use when? Here is a short question list that should help you to choose a cache layer for an object:
- How often does the object change?
- Quite often (every request or every few requests) - $cache->mem.
- Not very often or rarely - $cache->db or $cache->disk.
- How large is the object?
- Quite large (a size of a page or more than 4kB) - $cache->disk.
- Not large - $cache->db or $cache->mem.
- Should the object be loaded in a bulk with other cache objects?
- Yes, I load it almost on every request together with other objects - $cache->db.
- No, it is loaded individually - $cache->disk or $cache->mem.
It is important to note that $cache->mem isn't available on some hosts. If you have frequently changing pieces of data that you would store in $cache->mem but there is no memory driver available, then just don't cache them. Using another layer, such as $cache->db would most probably just slow things down.
#3. Using the API
Cache API reference is available here. In this chapter there are only examples of its use. For all the operations global $cache object of Cache class is used.
It is strongly recommended to check if cache is enabled before calling cache methods, you can do it as simple as
// Do something with cache
Or for one-line calls use a shorter way, e.g.
$cache && $cache->db->store('myvar', $myvar, 'myrealm');
#3.1. Getting, Updating and Removing Data
Basic operations are all pretty simple and similar for all layers. Each layer has its own member variable: db-> (database cache), disk-> (disk cache) and mem-> (memory cache). Common operations are: get, exists, store and remove. Here is an example for db cache:
if ($cache->db->exists('test', 'myrealm'))
$value = $cache->db->get('test', 'myrealm');
if ($value > 100)
$newval = time() % 200;
$cache->db->store('test', $newval, 'myrealm', 1200);
// lifetime of 1200 seconds
Database drivers use buffers and writeback, so calling db->get() and db->store()/db->remove() doesn't always mean querying the database. In the above example db->get() returns a value from a local buffer. The update/remove operations are performed all at once, just before the script termination.
Disk cache has similar interface, but it doesn't have the fourth parameter of db->store() - the TTL (time to live), because disk cache is permanent. And all operations are immediate.
Memory access is immediate too, and TTL is available for mem->store() if the memory driver is available itself of course. You should check it this way:
if ($cache && $cache->mem)
$counter = $cache->mem->inc('test', 'myrealm');
This example also demonstrates mem->inc() method which can be used for quick inter-process counters.
#3.2. Realm Autoloading
As it was mentioned in the first chapter, some realms are loaded automatically into the global variable scope on script startup. There are 2 realms which are always loaded: system and cot. For example, if you have set some core cache variable like
$cache->db->store('often_used', $often_used, 'system');
Then you can simply use this variable in your script as $often_used.
Another realm which is loaded automatically is the same as module name. To be exact, the realm name is taken from $z global variable, which is usually a module or “zone” name. So if you are in “pages” module, you can be sure “pages” realm is autoloaded.
What if you want to load some more realms automatically for your module or plugin? If you write a third-party module, you can add extra realms, besides $z, into $cot_cache_autoload array before common.php script is executed, e.g.:
$location = 'MyModule';
$z = 'mymodule';
$cot_cache_autoload = array('realm1', 'realm2');
require_once $cfg['system_dir'] . '/functions.php';
require_once $cfg['system_dir'] . '/common.php';
But what if you need to load it in the module body or it's a plugin you develop? Then it won't be so effective, but you can still import entire realms this way:
You need to be aware of possible name conflicts when using autoloading, because such cache variables may conflict with some other variables located in the global scope.
#3.3. Using Event Bindings
We will explain this feature with a clear and practical example. Imagine we're writing a news plugin which caches all of its contents in 'pages' variable of the 'news' realm:
$plugin_output = $cache->disk_get('pages', 'news');
// $plugin_output = some data
$cache->disk->store('pages', $plugin_output, 'news');
But how do we force our plugin to refresh that cache variable when a new page is added or existing one is updated? Of course we could use hooks and write some cleaning code. Or even hack the core if things get so bad. But a better solution is binding this cache field to appropriate events. We do so by registering event bindings on plugin installation. Here is the code for our news.setup.php:
if ($action == 'install')
$cache->bind('page.add.add.done', 'pages', 'news');
$cache->bind('page.edit.update.first', 'pages', 'news');
elseif ($action == 'uninstall')
What the cache trigger does on the event is invalidating the stored value. Though it does not store an updated one, so your code should do it later like in our news example above.
#4. Writing Your Own Cache Drivers (advanced developers)
Let's take a closer look at how the Cotonti Cache Subsystem is organized inside. The hierarchy of cache driver classes is show on the figure below:
If you want to add another memory cache driver, you should derive it from Temporary_cache_driver class. If you want to add another database cache driver, then derive it from Db_cache_driver.
For further documentation please read Cache code reference and see system/cache.php source.