Expiration and Invalidation
A cache is only useful as long as it does not serve outdated data. This page shows every way to tell Nette Caching when an item stops being valid:
- after a period of time, or when nobody has read it for a while
- when a file, a PHP constant or another item in the cache changes
- when your own function says so
- or right away, when you delete it by tag, priority or namespace
The examples continue the blog from the overview: $cache is a
cache in the namespace articles, $id is the ID of an article, and renderArticle() stands
for the expensive work of loading an article and turning it into HTML.
Setting Dependencies
Dependencies describe when an item expires. They are an array with Cache::* keys. The most convenient way
is to let the generator fill it in. Declare its parameter as a reference, &$dependencies, and load()
passes it an empty array:
use Nette\Caching\Cache;
$html = $cache->load("article/$id", function (&$dependencies) use ($id) {
$dependencies[Cache::Expire] = '1 hour';
return renderArticle($id);
});
The advantage is that the generator can decide based on what it computed. An article that is still being drafted can expire sooner than a published one, for example.
When the dependencies are known in advance, you can pass them as the third argument of load() instead, or of
save(), which stores a value directly without a generator:
$html = $cache->load("article/$id", fn() => renderArticle($id), [Cache::Expire => '1 hour']);
$cache->save("article/$id", $html, [Cache::Expire => '1 hour', Cache::Tags => ["article/$id"]]);
Prefer load() with a generator to save(). When several requests ask for a missing item at once,
FileStorage lets only one of them run the generator; with save(), they would all compute the value.
Before version 3.1.3, the constants were written in uppercase, such as Cache::EXPIRE instead of
Cache::Expire. Keep in mind that the array is not checked: a key with a typo, such as 'expir', is
silently ignored.
Expiration by Time
The key Cache::Expire sets when the item expires. It accepts a relative time as a string, a number of seconds, a
UNIX timestamp or a DateTimeInterface object:
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Expire] = 3600; // one hour
$dependencies[Cache::Expire] = new DateTimeImmutable('tomorrow');
With sliding expiration, every read moves the expiration further. An article that visitors keep reading then stays in the cache, while one nobody opens for 20 minutes is dropped:
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Sliding] = true;
Keep in mind that every read then also updates the item in the storage, which adds some overhead.
When a File Changes
Data computed from a file should expire when the file changes. Let's say the blog reads its list of categories from the file
categories.neon. The cached list then depends on that file:
$categories = $cache->load('categories', function (&$dependencies) {
$dependencies[Cache::Files] = __DIR__ . '/categories.neon';
return loadCategories(); // reads and processes categories.neon
});
As soon as you edit categories.neon, the cache notices on the next read and calls the generator again. Use
absolute paths. An item can depend on several files: $dependencies[Cache::Files] = [$file1, $file2].
When a Constant Changes
Similarly, an item can expire when the value of a PHP constant changes. That is handy for a version number you set at deployment, for example, so that every new version starts with a fresh cache:
$dependencies[Cache::Constants] = 'APP_VERSION';
When Another Item Changes
An item can depend on other items in the cache. Let's say the article and its comments are cached separately, under the keys
article/$id and comments/$id, and the whole page put together from them is cached as well. The page must
expire whenever one of its parts does:
$page = $cache->load("page/$id", function (&$dependencies) use ($id) {
$dependencies[Cache::Items] = ["article/$id", "comments/$id"];
return renderPage($id); // puts together the article and its comments
});
When the comments are saved again or removed, the page expires as well.
Custom Condition
When none of the above fits, write your own function. It is called on every read with the arguments you stored, and when it
returns false, the item expires. Let's say the rendered HTML must not survive an upgrade of PHP:
function checkPhpVersion(int $version): bool
{
return $version === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [['checkPhpVersion', PHP_VERSION_ID]];
Each entry is an array: the function, or a static method written as [ClassName::class, 'method'], followed by its
arguments. The function is called on every read, so it should be fast.
Combining Conditions
All these conditions can be combined. The item expires as soon as at least one of them is no longer met:
$dependencies[Cache::Expire] = '1 day';
$dependencies[Cache::Files] = __DIR__ . '/categories.neon';
$dependencies[Cache::Constants] = 'APP_VERSION';
Tags
Waiting for expiration is a compromise. It is better to delete an item exactly when the data it was made from changes. Tags let you do that. They are arbitrary strings you attach to an item:
$html = $cache->load("article/$id", function (&$dependencies) use ($id) {
$dependencies[Cache::Tags] = ["article/$id", "comments/$id"];
return renderArticle($id);
});
In the administration, right after saving an edited article to the database, you delete everything tagged with it:
$cache->clean([Cache::Tags => ["article/$id"]]);
The same goes for adding a comment, this time with the tag comments/$id. Notice that the code deleting the items
does not need to know their keys, or even how many there are. The article may sit in the cache as a whole page, as a preview on
the homepage and in the RSS feed; one call removes them all.
Tags work across namespaces: clean([Cache::Tags => [...]]) deletes the matching items in every namespace of the
storage, not just in the namespace of $cache.
FileStorage and MemcachedStorage need a journal for tags, SQLiteStorage keeps them
itself and MemoryStorage ignores them.
Priority
Each item can be given a priority, a number saying how important it is. When the cache grows too big, you can then delete the less important items first. Let's say previews of old articles are less valuable than the homepage:
$dependencies[Cache::Priority] = 10;
This deletes every item with a priority of 50 or less:
$cache->clean([Cache::Priority => 50]);
Items without a priority are never deleted this way.
FileStorage and MemcachedStorage need a journal for priorities; SQLiteStorage and
MemoryStorage ignore them.
Deleting Items
The method clean() deletes items by conditions, which you pass as an array with Cache::* keys:
| Key | Deletes |
|---|---|
Cache::Tags => [...] |
items with any of the tags |
Cache::Priority => 50 |
items with a priority of 50 or less |
Cache::Namespaces => [...] |
all items in the listed namespaces; only FileStorage supports it |
Cache::All => true |
everything in the storage, all namespaces included |
The conditions can be combined, and an item matching any of them is deleted:
$cache->clean([Cache::Tags => ["article/$id"], Cache::Priority => 50]);
Let's take a closer look at Cache::All. It deletes the entire storage, the items of all namespaces included, even
when you call it on a cache with a namespace. To delete just one namespace, use Cache::Namespaces; the namespaces
created from it by derive() stay.
The array is not checked, so a key with a typo is silently ignored and nothing is deleted.
Removing items that have already expired is a matter of the storage: call $storage->clean([]).
FileStorage and SQLiteStorage also do it on their own now and then, see Storages.
To delete a single item, use $cache->remove("article/$id").
What Each Storage Supports
Not every storage can honor every kind of dependency, and most of them silently ignore what they cannot do.
SQLiteStorage, for example, ignores priorities and dependencies on files, so such an item simply does not expire that
way, and MemoryStorage ignores all dependencies. Before relying on a dependency, check the table on the page Storages.
Bulk Reading and Writing
The homepage of the blog shows previews of ten articles. Reading them from the cache one by one means ten round trips to the
storage. The method bulkLoad() reads them all at once: you pass it the array $ids with their IDs as
keys, and it returns an array of values indexed by those keys:
$previews = $cache->bulkLoad($ids, function ($id, &$dependencies) {
$dependencies[Cache::Tags] = ["article/$id"];
return renderPreview($id); // renders a short preview of the article
});
The generator is called only for the previews that are missing, and it receives the key followed by the dependencies. Without a
generator, bulkLoad($ids) returns null for the missing items.
The method bulkSave() stores several items at once, all with the same dependencies:
$cache->bulkSave([
"preview/$id1" => $preview1,
"preview/$id2" => $preview2,
], [Cache::Expire => '1 hour']);