Cache accelerates your application by storing data – once hard 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
Using the cache is very easy in Nette, while it also covers very advanced caching needs. It is designed for performance and100% durability. Basically, you will find adapters for the most common backend storage. Allows tags-based invalidation, cachestampede protection, time expiration, etc.
Installation
Download and install the package usingComposer:
composer require nette/caching
Basic Usage
The center of work with the cache is the objectNette\Caching\Cache. We create its instance and pass theso-called storage to the constructor as a parameter. Which is an object representing the place where the data will be physicallystored (database, Memcached, files on disk, …). You get the storage object by passing it usingdependency injection with typeNette\Caching\Storage
. You will find out all the essentials insection Storage.
In version 3.0, the interface still had theI
prefix, so the name wasNette\Caching\IStorage
. Also, the constants of theCache
class were capitalized, so for exampleCache::EXPIRE
instead ofCache::Expire
.
For the following examples, suppose we have an aliasCache
and a storage in the variable$storage
.
use Nette\Caching\Cache;$storage = /* ... */; // instance of Nette\Caching\Storage
The cache is actually akey–value store, so we read and write data under keys just like associative arrays.Applications consist of a number of independent parts, and if they all used one storage (for idea: one directory on a disk),sooner or later there would be a key collision. The Nette Framework solves the problem by dividing the entire space intonamespaces (subdirectories). Each part of the program then uses its own space with a unique name and no collisions can occur.
The name of the space is specified as the second parameter of the constructor of the Cache class:
$cache = new Cache($storage, 'Full Html Pages');
We can now use object$cache
to read and write from the cache. The methodload()
is used for both.The first argument is the key and the second is the PHP callback, which is called when the key is not found in the cache. Thecallback generates a value, returns it and caches it:
$value = $cache->load($key, function () use ($key) {$computedValue = /* ... */; // heavy computationsreturn $computedValue;});
If the second parameter is not specified$value = $cache->load($key)
, thenull
is returned if theitem is not in the cache.
The great thing is that any serializable structures can be cached, not only strings. And the same appliesfor keys.
The item is cleared from the cache using methodremove()
:
$cache->remove($key);
You can also cache an item using method$cache->save($key, $value, array $dependencies = [])
. However, theabove method usingload()
is preferred.
Memoization
Memoization means caching the result of a function or method so you can use it next time instead of calculating the same thingagain and again.
Methods and functions can be called memoized usingcall(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
The functiongethostbyaddr()
is called only once for each parameter$ip
and the next time the valuefrom the cache will be returned.
It is also possible to create a memoized wrapper for a method or function that can be called later:
function factorial($num){return /* ... */;}$memoizedFactorial = $cache->wrap('factorial');$result = $memoizedFactorial(5); // counts it$result = $memoizedFactorial(5); // returns it from cache
Expiration & Invalidation
With caching, it is necessary to address the question that some of the previously saved data will become invalid over time.Nette Framework provides a mechanism, how to limit the validity of data and how to delete them in a controlled way (“toinvalidate them”, using the framework's terminology).
The validity of the data is set at the time of saving using the third parameter of the methodsave()
, eg:
$cache->save($key, $value, [$cache::Expire => '20 minutes',]);
Or using the$dependencies
parameter passed by reference to the callback in theload()
method, eg:
$value = $cache->load($key, function (&$dependencies) {$dependencies[Cache::Expire] = '20 minutes';return /* ... */;});
Or using the 3rd parameter in theload()
method, eg:
$value = $cache->load($key, function () {return ...;}, [Cache::Expire => '20 minutes']);
In the following examples, we will assume the second variant and thus the existence of a variable$dependencies
.
Expiration
The simplest expiration is the time limit. Here's how to cache data valid for 20 minutes:
// it also accepts the number of seconds or the UNIX timestamp$dependencies[Cache::Expire] = '20 minutes';
If we want to extend the validity period with each reading, it can be achieved this way, but beware, this will increase thecache overhead:
$dependencies[Cache::Sliding] = true;
The handy option is the ability to let the data expire when a particular file is changed or one of several files. This can beused, for example, for caching data resulting from procession these files. Use absolute paths.
$dependencies[Cache::Files] = '/path/to/data.yaml';// or$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
We can let an item in the cache expired when another item (or one of several others) expires. This can be used when we cachethe entire HTML page and fragments of it under other keys. Once the snippet changes, the entire page becomes invalid. If we havefragments stored under keys such asfrag1
andfrag2
, we will use:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
Expiration can also be controlled using custom functions or static methods, which always decide when reading whether the itemis still valid. For example, we can let the item expire whenever the PHP version changes. We will create a function that comparesthe current version with the parameter, and when saving we will add an array in the form[function name, ...arguments]
to the dependencies:
function checkPhpVersion($ver): bool{return $ver === PHP_VERSION_ID;}$dependencies[Cache::Callbacks] = [['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false];
Of course, all criteria can be combined. The cache then expires when at least one criterion is not met.
$dependencies[Cache::Expire] = '20 minutes';$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidation Using Tags
Tags are a very useful invalidation tool. We can assign a list of tags, which are arbitrary strings, to each item stored in thecache. For example, suppose we have an HTML page with an article and comments, which we want to cache. So we specify tags whensaving to cache:
$dependencies[Cache::Tags] = ["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 adatabase, we call theclean()
command, which will delete cached items by tag:
$cache->clean([$cache::Tags => ["article/$articleId"],]);
Likewise, in the place of adding a new comment (or editing a comment), we will not forget to invalidate the relevant tag:
$cache->clean([$cache::Tags => ["comments/$articleId"],]);
What have we achieved? That our HTML cache will be invalidated (deleted) whenever the article or comments change. When editingan article with ID = 10, the tagarticle/10
is forced to be invalidated and the HTML page carrying the tag is deletedfrom the cache. The same happens when you insert a new comment under the relevant article.
Tags requireJournal.
Invalidation by Priority
We can set the priority for individual items in the cache, and it will be possible to delete them in a controlled way when, forexample, the cache exceeds a certain size:
$dependencies[Cache::Priority] = 50;
Delete all items with a priority equal to or less than 100:
$cache->clean([$cache::Priority => 100,]);
Priorities require so-calledJournal.
Clear Cache
TheCache::All
parameter clears everything:
$cache->clean([$cache::All => true,]);
Bulk Reading
For bulk reading and writing to cache, thebulkLoad()
method is used, where we pass an array of keys and obtain anarray of values:
$values = $cache->bulkLoad($keys);
MethodbulkLoad()
works similarly toload()
with the second callback parameter, to which the key ofthe generated item is passed:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {$computedValue = /* ... */; // heavy computationsreturn $computedValue;});
Using with PSR-16
To use Nette Cache with the PSR-16 interface, you can utilize thePsrCacheAdapter
. It allows seamless integrationbetween Nette Cache and any code or library that expects a PSR-16 compatible cache.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Now you can use$psrCache
as a PSR-16 cache:
$psrCache->set('key', 'value', 3600); // stores the value for 1 hour$value = $psrCache->get('key', 'default');
The adapter supports all methods defined in PSR-16, includinggetMultiple()
,setMultiple()
, anddeleteMultiple()
.
Output Caching
The output can be captured and cached very elegantly:
if ($capture = $cache->capture($key)) {echo ... // printing some data$capture->end(); // save the output to the cache}
In case that the output is already present in the cache, thecapture()
method prints it and returnsnull
, so the condition will not be executed. Otherwise, it starts to buffer the output and returns the$capture
object using which we finally save the data to the cache.
In version 3.0 the method was called$cache->start()
.
Caching in Latte
Caching inLatte templates is very easy, just wrap part of the template with tags{cache}...{/cache}
. The cache is automatically invalidated when the source template changes (including any includedtemplates within the{cache}
tags). Tags{cache}
can be nested, and when a nested block is invalidated(for example, by a tag), the parent block is also invalidated.
In the tag it is possible to specify the keys to which the cache will be bound (here the variable$id
) and set theexpiration andinvalidation tags
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}...{/cache}
All parameters are optional, so you don't have to specify expiration, tags, or keys.
The use of the cache can also be conditioned byif
– the content will then be cached only if the conditionis met:
{cache $id, if: !$form->isSubmitted()}{$form}{/cache}
Storages
A storage is an object that represents where data is physically stored. We can use a database, a Memcached server, or the mostavailable storage, which are files on disk.
Storage | Description |
---|---|
FileStorage | default storage with saving to files on disk |
MemcachedStorage | uses theMemcached server |
MemoryStorage | data are temporarily in memory |
SQLiteStorage | data is stored in SQLite database |
DevNullStorage | data aren't stored – for testing purposes |
You get the storage object by passing it usingdependency injection with theNette\Caching\Storage
type. By default, Nette provides a FileStorage object that stores data in a subfoldercache
in the directory fortemporaryfiles .
You can change the storage in the configuration:
services:cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Writes the cache to files on disk. The storageNette\Caching\Storages\FileStorage
is very well optimized forperformance and above all ensures full atomicity of operations. What does it mean? That when using the cache, it cannot happenthat we read a file that has not yet been completely written by another thread, or that someone would delete it “under yourhands”. The use of the cache is therefore completely safe.
This storage also has an important built-in feature that prevents an extreme increase in CPU usage when the cache is cleared orcold (ie not created). This iscache stampede prevention. It happensthat at one moment there are several concurrent requests that want the same thing from the cache (eg the result of an expensiveSQL query) and because it is not cached, all processes start executing the same SQL query. The processor load is multiplied and itcan even happen that no thread can respond within the time limit, the cache is not created and the application crashes.Fortunately, the cache in Nette works in such a way that when there are multiple concurrent requests for one item, it is generatedonly by the first thread, the others wait and then use the generated result.
Example of creating a FileStorage:
// the storage will be the directory '/path/to/temp' on the disk$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
The serverMemcached is a high-performance distributed storage system whose adapter isNette\Caching\Storages\MemcachedStorage
. In the configuration, specify the IP address and port if it differs from thestandard 11211.
Requires PHP extensionmemcached
.
services:cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage
is a storage that stores data in a PHP array and is thus lost when therequest is terminated.
SQLiteStorage
The SQLite database and adapterNette\Caching\Storages\SQLiteStorage
offer a way to cache in a single file ondisk. The configuration will specify the path to this file.
Requires PHP extensionspdo
andpdo_sqlite
.
services:cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
A special implementation of storage isNette\Caching\Storages\DevNullStorage
, which does not actually store dataat all. It is therefore suitable for testing if we want to eliminate the effect of the cache.
Using Cache in Code
When using caching in code, you have two ways how to do it. The first is that you get the storage object by passing it usingdependency injection and then create an objectCache
:
use Nette;class ClassOne{private Nette\Caching\Cache $cache;public function __construct(Nette\Caching\Storage $storage){$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');}}
The second way is that you get the storage objectCache
:
class ClassTwo{public function __construct(private Nette\Caching\Cache $cache,) {}}
TheCache
object is then created directly in the configuration as follows:
services:- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette stores tags and priorities in a so-called journal. By default, SQLite and filejournal.s3db
are used forthis, andPHP extensionspdo
andpdo_sqlite
are required.
You can change the journal in the configuration:
services:cache.journal: MyJournal
DI Services
These services are added to the DI container:
Name | Type | Description |
---|---|---|
cache.journal | Nette\Caching\Storages\Journal | journal |
cache.storage | Nette\Caching\Storage | repository |
Turning Off Cache
One of the ways to turn off caching in the application is to set the storage toDevNullStorage:
services:cache.storage: Nette\Caching\Storages\DevNullStorage
This setting does not affect the caching of templates in Latte or the DI container, as these libraries do not use the servicesof nette/caching and manage their cache independently. Moreover, their cachedoes not need to be turned off indevelopment mode.