Caching
Cache accelerates your application by storing data – once hardly retrieved – for future use. We will show you:
- How to use the cache
- How to change the cache storage
- How to properly invalidate the cache
Nette Framework offers a very intuitive API for cache manipulation. After all, you wouldn't expect anything else, right? ;-) Before we show you the first example, we need to think about place where to store data physically. We can use a database, Memcached server, or the most available storage – hard drive:
// the `temp` directory will be the storage
$storage = new Nette\Caching\Storages\FileStorage('temp');
The Nette\Caching\Storages\FileStorage storage is very well optimized for performance and in the first place, it provides full atomicity of operations. What does that mean? When we use cache we can be sure we are not reading a file that is not fully written yet (by another thread) or that the file gets deleted “under our hands”. Using the cache is therefore completely safe.
For manipulation with cache, we use the Nette\Caching\Cache:
use Nette\Caching\Cache;
$cache = new Cache($storage); // $storage from the previous example
Let's save the contents of the ‘$data’ variable under the
‘$key’ key:
$cache->save($key, $data);
This way, we can read from the cache: (if there is no such item in the cache,
the NULL value is returned)
$value = $cache->load($key);
if ($value === NULL) ...
So we delete the item from the cache by saving NULL:
$cache->save($key, NULL);
It's possible to save any structure to the cache, not only strings. The same applies for keys.
Web applications typically consist of a number of independent parts, and if
they all cache data in the same storage (for example the same directory), sooner
or later there would be collisions in names. Nette Framework solves this by
splitting the whole storage to sections (in the FileStorage case
using subdirectories). Each part of the application uses it's own section with
unique name, therefore no collision can occur. The name of the section can be
passed as the second parameter to the Cache class constructor.
(These sections are often refered to as cache namespaces.)
$cache = new Cache($storage, 'htmlOutput');
The cacheStorage Service
We can use the system DI container so
we do not have to create or pass the $storage object everywhere,
because the container provides a cacheStorage service that defaults
to the FileStorage saving data to the directory specified by the
tempDir parameter, or the TEMP_DIR constant.
// $container is the system DI container
$cache = new Cache($container->cacheStorage, 'htmlOutput');
$cache->save($key, $data);
Caching in Templates
Caching in templates is very easy, just wrap the part of the template with
the {cache} ... {/cache}. The cache is automatically invalidated
when the source template is changed, including any templates included inside the
cache macro. {cache} blocks can be nested, and when a nested block
gets invalidated (for example by a tag), the parent block is also
invalidated.
It's possible to define keys to which the cache will be binded (in this
case, the $id variable) and set the expiration and tags for invalidation.
{cache $id, expire => '+20 minutes', tags => [tag1, tag2]}
...
{/cache}
All parameters are option, so you do not have to specify expiration, tags or keys.
The use of a cache can also use ‘if’ condition – the
contents will be cached only when the condition is met:
{cache $id, if => !$form->isSubmitted()}
{$form}
{/cache}
If we only retrieve the data from our model in the template (on demand principle, or lazy), the cache will be particularly effective.
Caching Function Results
Caching the result of a function or method call can be achieved using the
call() method:
$name = $cache->call('gethostbyaddr', $ip);
The gethostbyaddr($ip) will therefore be called only once and
next time, only the value from cache will be returned. Of course, for different
$ip, different results are cached.
Output Caching
The output can be cached not only in templates:
if ($block = $cache->start($key)) [
... printing some data ...
$block->end(); // save the output to the cache
}
In case that the output is already present in the cache, the
start() method prints it and return NULL. Otherwise,
it starts to buffer the output and returns the $block object using
which we finally save the data to the cache.
Expiration and Invalidation
Here come two problems of storing data in the cache. First, there is a possibility that the storage is completely filled and you cannot save more data inside. And it may happen that some od the previously saved data will become invalid over time. Therefore, Nette Framework provides a mechanism, how to limit the validity of data and how to delete them in a controlled way (“to invalidate them”, using the framework's terminology).
Data validity is set when saving the data using the third parameter of the
save() method:
$cache->save($key, $data, array(
Cache::EXPIRE => '+ 20 minutes', // accepts also seconds or a timestamp.
));
It's obvious from the code itself, that we saved the data for next
20 minutes. After this period, the cache will report that there is no record
under the ‘$key’ key (ie, will return NULL). In
fact, you can use any time value that is a valid value for PHP function strToTime() or the DateTime class. If we want to
extend the validity period with each reading, it can be achieved this way:
$cache->save($key, $data, array(
Cache::EXPIRE => '+ 20 minutes',
Cache::SLIDING => TRUE,
));
Very handy is also the ability to let the data expire when a particular file is changed or one of several files. That can be used for stroring data resulting from parsing these files to the cache. For trouble-free functionality, it's recommended to use absolute paths.
$cache->save($key, $data, array(
Cache::FILES => 'data.yaml', // an array of files can also be specified
));
The Cache::FILES criteria, of course, can be combined with the
time expiration using Cache::EXPIRE etc.
The cache can also depend on other cached items. That can be used when we save the whole HTML page in the cache and under different keys, some of its fragments. As soon as a part changes, the whole page is invalidated.
$cache->save('page', $html, array(
// will expire if frag1 or frag2 expires
Cache::ITEMS => array('frag1', 'frag2'),
));
Expiration can be controlled even by your own callbacks:
function controlExpiration($val)
{
return $val;
}
$cache->save($key, $value, array(
Cache::CALLBACKS => array(array('controlExpiration', 1)),
));
Expiration Using Tags and Priority
Very useful invalidation tool are so called tags. We can assign a list of tags to each item. For example, suppose we have an HTML page with an article and comments, which we want to cache. So we specify tags when saving to cache:
$cache->save($articleId, $html, array(
Cache::TAGS => array("article/$articleId", "comments/$articleId"),
));
Now, let's move to the administration. Here we have a form for article
editing. Together with saving the article to a database, we call the
clean() command, which will delete cached items by tag:
$cache->clean(array(
Cache::TAGS => array("article/$articleId"),
));
And in the place for adding new comments (or editing them), don't forget to invalidate appropriate tag:
$cache->clean(array(
Cache::TAGS => array("comments/$articleId"),
));
What we have achieved? That the HTML cache will invalidate automatically.
Whenever someone changes the article with ID of 10, it will force the
article/10 tag to invalidate and the tagged HTML page in cache is
cleared. The same will happen when someone inserts a new comment below the
article.
Similar to tags, you can control expiration by priority:
$cache->save($key, $value, array(
Cache::PRIORITY => 50,
));
// all cached items with priority less than or equal to 100 will be removed.
$cache->clean(array(
Cache::PRIORITY => 100,
));
Cache Storage
In addition to already mentioned FileStorage, Nette Framework
also provides MemcachedStorage
which stores data to the Memcached server, and also MemoryStorage
for storing data in memory for duration of the request. The special DevNullStorage,
which does precisely nothing, can be used for testing, when we want to eliminate
the influence of caching.
Of course, it's possible to create your own storage. The only requirement is to implement the IStorage interface.
@serializationVersion
We can also store objects in the cache. But what if you change the structure
of the class and there is still older serialized version of the object stored in
the cache? That could lead to creation of invalid object. How to avoid this? You
just need to specify the @serializationVersion annotation for the
class containing some kind of “version” of the class:
/**
* @serializationVersion 123
*/
class Data
{
...
}
When you save an object to the cache, Nette Framework remembers the value of this annotation. When the structure of the class changes, all you need to do is to change the verson number in the annotation and Nette will automatically delete old cached objects.
Concurrent Caching
Deleting the cache is a common operation when uploading a new application version to the server. At that moment, however, the server gets pretty hard time, because it has to build a complete new cache. Retrieving some data can be quite difficult, for example the RobotLoader cache building. And moreover, if, say, 30 requests come in a short period, the resource consumption is even higher.
The solution is to modify application behaviour so that data are created only by one thread and others are waiting. To do this, specify the value as a callback or use an anonymous function:
$result = $cache->save($key, function() { // or callback(...)
return buildData(); // difficult operation
});
Framework will ensure that the body of the function will be called only by one thread at once, and other threads will be waiting. If the thread fails for some reason, another gets chance.
