Caching
Cache [kæʃ]
speeds up your application by storing data that was once computationally expensive to retrieve,
allowing for faster access 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 and 100% 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 using Composer:
composer require nette/caching
Basic Usage
The core element for working with the cache is the Nette\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 be
stored (database, Memcached, files on disk, etc.). You typically obtain the storage object via dependency injection by requesting the type
Nette\Caching\Storage
. You'll learn the essentials in the Storages section.
In version 3.0, the interface still had the I
prefix, so the name was
Nette\Caching\IStorage
. Furthermore, constants of the Cache
class were written in uppercase, e.g.,
Cache::EXPIRE
instead of Cache::Expire
.
For the following examples, assume we have an alias Cache
and a storage instance in the $storage
variable.
use Nette\Caching\Cache;
$storage = /* ... */; // instance of Nette\Caching\Storage
The cache is essentially a key-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), key collisions 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 the Cache
class constructor:
$cache = new Cache($storage, 'Full Html Pages');
Now, we can use the $cache
object to read from and write to the cache. The load()
method serves both
purposes. 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 the load()
method caches it:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // expensive computation
return $computedValue;
});
If the second parameter is omitted ($value = $cache->load($key)
), load()
returns null
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 the remove()
method:
$cache->remove($key);
You can also save an item to the cache using the $cache->save($key, $value, array $dependencies = [])
method.
However, the load()
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 using call(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
The gethostbyaddr()
function is thus called only once for each unique $ip
argument. Subsequent calls
with 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 Framework provides mechanisms to limit data validity or delete it explicitly (referred to as “invalidation” in the framework's terminology).
Data validity is set at the time of saving, typically using the third parameter of the save()
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 the
load()
method, e.g.:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
Or by using the 3rd parameter of the load()
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 the
callback.
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 aware that 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, for
instance, an entire HTML page and its fragments under different keys. When a fragment changes, the entire page should be
invalidated. If the fragments are stored under keys like frag1
and frag2
, 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 the
item is still valid. For example, we can make an item expire whenever the PHP version changes. Create a function that compares the
current version with a parameter, and when saving, add an array in the format [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
];
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 the cache. For example, suppose we have an HTML page displaying an article and its comments, which we want to cache. When saving, we specify 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 the
database, we call the clean()
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 tag article/10
is invalidated, and the cached HTML page carrying this tag
is deleted. The same occurs when a new comment is added under the respective article.
Tags require a Journal.
Invalidation by Priority
We can assign priorities to individual cache items. This allows for controlled deletion, for example, when the cache exceeds a certain 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-called Journal.
Clear Cache
The Cache::All
parameter clears everything:
$cache->clean([
$cache::All => true,
]);
Bulk Reading
For bulk reading and writing to the cache, use the bulkLoad()
method. Pass it an array of keys, and it returns an
array of corresponding values:
$values = $cache->bulkLoad($keys);
The bulkLoad()
method works similarly to load()
, also accepting a second callback parameter. This
callback receives the key of the item being generated:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // expensive computation
return $computedValue;
});
Using with PSR-16
To use Nette Cache with a PSR-16 interface, you can utilize the PsrCacheAdapter
. It enables seamless integration
between 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, including getMultiple()
, setMultiple()
, and
deleteMultiple()
.
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, the capture()
method prints it and returns null
, so
the if
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 its end()
method.
In version 3.0, this method was named $cache->start()
.
Caching in Latte
Caching in Latte templates is very simple. Just wrap the portion of the template you
want to cache with the {cache}...{/cache}
tags. The cache is automatically invalidated whenever the source template
file changes (including any templates included within the cached block). The {cache}
tags can be nested. When a
nested 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 an
expiration time, and define invalidation 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 using if
– the content will only be cached if the condition
is 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, or the most readily available storage: files on disk.
Storage | Description |
---|---|
FileStorage | Default storage, saves cache to files on disk. |
MemcachedStorage | Uses a Memcached 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 via dependency
injection by requesting the type Nette\Caching\Storage
. By default, Nette provides a FileStorage
object that stores data in the cache
subdirectory within the directory for temporary 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. The Nette\Caching\Storages\FileStorage
storage is highly optimized for
performance and, crucially, ensures full atomicity of operations. What does this mean? When using the cache, it cannot happen that
you 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 cleared or is still “cold” (i.e., not yet created). This is known as cache stampede prevention. It occurs when multiple concurrent requests simultaneously ask for the same cached item (e.g., the result of an expensive SQL query). If the item isn't currently cached, all these processes might start executing the same expensive operation (like the SQL query). This multiplies the server load, and it can even happen that no thread manages to respond within the time limit, the cache doesn't get created, and the application may crash. Fortunately, Nette's cache handles this: when multiple concurrent requests are made for the same item, only the first thread generates it. The other threads wait and then use the result generated by the first one.
Example of creating a FileStorage
:
// the storage will be the directory '/path/to/temp' on disk
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
The Memcached server is a high-performance distributed memory object caching system. Its
adapter in Nette is Nette\Caching\Storages\MemcachedStorage
. In the configuration, specify the server's IP address
and port if it differs from the standard 11211.
Requires the memcached
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 is
lost when the request ends.
SQLiteStorage
The SQLite database, along with the Nette\Caching\Storages\SQLiteStorage
adapter, provides a method for caching
data within a single file on disk. The configuration specifies the path to this database file.
Requires the pdo
and pdo_sqlite
PHP extensions.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
A special storage implementation is Nette\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 via dependency injection and then create the
Cache
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 the Cache
object directly:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
The Cache
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 file
journal.s3db
, and the pdo
and pdo_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 to DevNullStorage:
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 utilize
nette/caching
services and manage their caches independently. Furthermore, their caches do not typically need to be
disabled during development mode.