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.

version: 3.x 2.x