Cache speeds up your application by storing data that was once computationally expensive to retrieve, allowing for fasteraccess in the future. We will cover:
- how to use the cache
- how to change the storage backend
- how to correctly invalidate the cache
Using the cache in Nette is very straightforward, yet it covers sophisticated caching needs. It's designed for performance and100% durability. It includes adapters for the most common storage backends. It supports tag-based invalidation, time expiration,protection against cache stampede, and more.
Installation
Download and install the package usingComposer:
composer require nette/caching
Basic Usage
The core element for working with the cache is theNette\Caching\Cache object. We create an instance of it,passing a storage backend object to the constructor. This storage object represents the physical location where data will bestored (database, Memcached, files on disk, etc.). You typically obtain the storage object viadependency injection by requesting the typeNette\Caching\Storage
. You'll learn the essentials in theStorages section.
In version 3.0, the interface still had theI
prefix, so the name wasNette\Caching\IStorage
. Furthermore, constants of theCache
class were written in uppercase, e.g.,Cache::EXPIRE
instead ofCache::Expire
.
For the following examples, assume we have an aliasCache
and a storage instance in the$storage
variable.
use Nette\Caching\Cache;$storage = /* ... */; // instance of Nette\Caching\Storage
The cache is essentially akey-value store, meaning we read and write data using keys, similar to associative arrays.Applications consist of multiple independent parts. If all parts used a single storage (imagine a single directory on disk), keycollisions would eventually occur. The Nette Framework addresses this by partitioning the storage space into namespaces(conceptually like subdirectories). Each part of the application then works within its own namespace using a unique name,preventing any collisions.
Specify the namespace name as the second argument to theCache
class constructor:
$cache = new Cache($storage, 'Full Html Pages');
Now, we can use the$cache
object to read from and write to the cache. Theload()
method serves bothpurposes. The first argument is the key, and the second is a PHP callback that gets invoked if the key is not found in the cache.The callback generates the value, returns it, and theload()
method caches it:
$value = $cache->load($key, function () use ($key) {$computedValue = /* ... */; // expensive computationreturn $computedValue;});
If the second parameter is omitted ($value = $cache->load($key)
),load()
returnsnull
if the item is not found in the cache.
It's great that any serializable structures can be cached, not just strings. The same applies to keys.
To delete an item from the cache, use theremove()
method:
$cache->remove($key);
You can also save an item to the cache using the$cache->save($key, $value, array $dependencies = [])
method.However, theload()
approach shown above is generally preferred.
Memoization
Memoization involves caching the result of a function or method call, so the next time it's called with the same arguments,the cached result is returned instead of recalculating it.
Methods and functions can be called in a memoized way usingcall(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
Thegethostbyaddr()
function is thus called only once for each unique$ip
argument. Subsequent callswith the same$ip
will return the cached value.
It's also possible to create a memoized wrapper around a method or function, which can then be called later:
function factorial($num){return /* ... */;}$memoizedFactorial = $cache->wrap('factorial');$result = $memoizedFactorial(5); // calculates it the first time$result = $memoizedFactorial(5); // returns from cache the second time
Expiration & Invalidation
When using caching, it's necessary to address the issue of when previously stored data becomes invalid. Nette Frameworkprovides mechanisms to limit data validity or delete it explicitly (referred to as “invalidation” in theframework's terminology).
Data validity is set at the time of saving, typically using the third parameter of thesave()
method, e.g.:
$cache->save($key, $value, [$cache::Expire => '20 minutes',]);
Alternatively, it can be set using the$dependencies
parameter passed by reference to the callback in theload()
method, e.g.:
$value = $cache->load($key, function (&$dependencies) {$dependencies[Cache::Expire] = '20 minutes';return /* ... */;});
Or by using the 3rd parameter of theload()
method itself, e.g.:
$value = $cache->load($key, function () {return ...;}, [Cache::Expire => '20 minutes']);
In the following examples, we'll assume the second variant, utilizing the$dependencies
variable within thecallback.
Expiration
The simplest form of expiration is a time limit. This caches data with a validity of 20 minutes:
// accepts number of seconds or a UNIX timestamp as well$dependencies[Cache::Expire] = '20 minutes';
If you want the validity period to extend with each read (sliding expiration), you can achieve this as follows, but be awarethat this increases cache overhead:
$dependencies[Cache::Sliding] = true;
A useful option is to have data expire when a specific file or one of several files is modified. This is useful, for example,when caching data derived from processing 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 make a cache item expire when another specific item (or one of several others) expires. This is useful when caching, forinstance, an entire HTML page and its fragments under different keys. When a fragment changes, the entire page should beinvalidated. If the fragments are stored under keys likefrag1
andfrag2
, use:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
Expiration can also be controlled using custom functions or static methods. These are called upon each read to determine if theitem is still valid. For example, we can make an item expire whenever the PHP version changes. Create a function that compares thecurrent version with a parameter, and when saving, add an array in the format[function name, ...arguments]
to thedependencies:
function checkPhpVersion($ver): bool{return $ver === PHP_VERSION_ID;}$dependencies[Cache::Callbacks] = [['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false];
Naturally, all these criteria can be combined. The cache item expires if at least one criterion is no longer met.
$dependencies[Cache::Expire] = '20 minutes';$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidation Using Tags
Tags provide a very useful invalidation mechanism. We can assign a list of tags (arbitrary strings) to each item stored in thecache. For example, suppose we have an HTML page displaying an article and its comments, which we want to cache. When saving, wespecify the relevant tags:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Now, let's move to the administration section. Here, we have a form for editing articles. Along with saving the article to thedatabase, we call theclean()
method to delete cached items based on their tag:
$cache->clean([$cache::Tags => ["article/$articleId"],]);
Similarly, when adding a new comment (or editing one), we must remember to invalidate the corresponding tag:
$cache->clean([$cache::Tags => ["comments/$articleId"],]);
What have we achieved? Our HTML cache will now be invalidated (deleted) whenever the associated article or its comments change.When editing the article with ID = 10, the tagarticle/10
is invalidated, and the cached HTML page carrying this tagis deleted. The same occurs when a new comment is added under the respective article.
Tags require aJournal.
Invalidation by Priority
We can assign priorities to individual cache items. This allows for controlled deletion, for example, when the cache exceeds acertain size limit:
$dependencies[Cache::Priority] = 50;
To delete all items with a priority equal to or less than 100:
$cache->clean([$cache::Priority => 100,]);
Priorities require a so-calledJournal.
Clear Cache
TheCache::All
parameter clears everything:
$cache->clean([$cache::All => true,]);
Bulk Reading
For bulk reading and writing to the cache, use thebulkLoad()
method. Pass it an array of keys, and it returns anarray of corresponding values:
$values = $cache->bulkLoad($keys);
ThebulkLoad()
method works similarly toload()
, also accepting a second callback parameter. Thiscallback receives the key of the item being generated:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {$computedValue = /* ... */; // expensive computationreturn $computedValue;});
Using with PSR-16
To use Nette Cache with a PSR-16 interface, you can utilize thePsrCacheAdapter
. It enables seamless integrationbetween Nette Cache and any code or library expecting a PSR-16 compatible cache implementation.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Now you can use$psrCache
as a standard 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
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}
If the output is already present in the cache, thecapture()
method prints it and returnsnull
, sotheif
condition block is skipped. Otherwise, it starts buffering the output and returns a$capture
object, which you use to finally save the captured data to the cache via itsend()
method.
In version 3.0, this method was named$cache->start()
.
Caching in Latte
Caching inLatte templates is very simple. Just wrap the portion of the template youwant to cache with the{cache}...{/cache}
tags. The cache is automatically invalidated whenever the source templatefile changes (including any templates included within the cached block). The{cache}
tags can be nested. When anested block is invalidated (e.g., via a tag), its parent block is also invalidated.
Within the tag, you can specify keys to which the cache entry will be bound (here, the variable$id
), set anexpiration time, and defineinvalidation tags.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}...{/cache}
All these parameters are optional, so you don't need to specify expiration, tags, or even keys.
The use of caching can also be made conditional usingif
– the content will only be cached if the conditionis met:
{cache $id, if: !$form->isSubmitted()}{$form}{/cache}
Storages
A storage is an object representing the physical location where data is stored. We can use a database, a Memcached server, orthe most readily available storage: files on disk.
Storage | Description |
---|---|
FileStorage | Default storage, saves cache to files on disk. |
MemcachedStorage | Uses aMemcached server for storage. |
MemoryStorage | Data is stored temporarily in memory (lost on request end). |
SQLiteStorage | Data is stored in an SQLite database file. |
DevNullStorage | Data isn't actually stored; useful for testing. |
You obtain the storage object viadependencyinjection by requesting the typeNette\Caching\Storage
. By default, Nette provides aFileStorage
object that stores data in thecache
subdirectory within the directory fortemporary files.
You can change the default storage in the configuration:
services:cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Writes cache entries to files on disk. TheNette\Caching\Storages\FileStorage
storage is highly optimized forperformance and, crucially, ensures full atomicity of operations. What does this mean? When using the cache, it cannot happen thatyou read a file that hasn't been completely written by another thread yet, or that someone deletes it while you are reading it.Therefore, using this cache storage is completely safe.
This storage also includes an important built-in feature that prevents an extreme surge in CPU usage when the cache is clearedor is still “cold” (i.e., not yet created). This is known ascachestampede prevention. It occurs when multiple concurrent requests simultaneously ask for the same cached item (e.g., the resultof an expensive SQL query). If the item isn't currently cached, all these processes might start executing the same expensiveoperation (like the SQL query). This multiplies the server load, and it can even happen that no thread manages to respond withinthe time limit, the cache doesn't get created, and the application may crash. Fortunately, Nette's cache handles this: whenmultiple concurrent requests are made for the same item, only the first thread generates it. The other threads wait and then usethe result generated by the first one.
Example of creating aFileStorage
:
// the storage will be the directory '/path/to/temp' on disk$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
TheMemcached server is a high-performance distributed memory object caching system. Itsadapter in Nette isNette\Caching\Storages\MemcachedStorage
. In the configuration, specify the server's IP addressand port if it differs from the standard 11211.
Requires thememcached
PHP extension.
services:cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage
is a storage that holds data within a PHP array. Consequently, the data islost when the request ends.
SQLiteStorage
The SQLite database, along with theNette\Caching\Storages\SQLiteStorage
adapter, provides a method for cachingdata within a single file on disk. The configuration specifies the path to this database file.
Requires thepdo
andpdo_sqlite
PHP extensions.
services:cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
A special storage implementation isNette\Caching\Storages\DevNullStorage
, which doesn't actually store any data.It is therefore suitable for testing purposes when you want to eliminate the effects of caching.
Using Cache in Code
When using caching in your code, there are two main approaches. The first is to obtain the storage object viadependency injection and then create theCache
object yourself:
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 option is to request theCache
object directly:
class ClassTwo{public function __construct(private Nette\Caching\Cache $cache,) {}}
TheCache
object must then be defined in the configuration, for example like this:
services:- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette stores tags and priorities information in a so-called journal. By default, SQLite is used for this purpose via the filejournal.s3db
, andthepdo
andpdo_sqlite
PHP extensions are required.
You can change the journal implementation 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 | The cache journal storage |
cache.storage | Nette\Caching\Storage | The primary cache storage |
Turning Off Cache
One way to disable caching in your application is to set the storage backend toDevNullStorage:
services:cache.storage: Nette\Caching\Storages\DevNullStorage
This setting does not affect the caching of Latte templates or the DI container, as these libraries do not utilizenette/caching
services and manage their caches independently. Furthermore, their cachesdo not typically need to bedisabled during development mode.