Nette Documentation Preview

syntax
Expirace a invalidace
*********************

<div class=perex>

Cache je užitečná jen tak dlouho, dokud nevrací zastaralá data. Tato stránka ukazuje všechny způsoby, jak Nette Caching říct, kdy položka přestává platit:

- po uplynutí doby, nebo když ji delší dobu nikdo nečetl
- když se změní soubor, PHP konstanta nebo jiná položka v cache
- když to řekne vaše vlastní funkce
- nebo hned, když ji smažete podle tagu, priority či jmenného prostoru

</div>

Ukázky navazují na blog z [úvodu |@home]: `$cache` je cache ve jmenném prostoru `articles`, `$id` je ID článku a `renderArticle()` zastupuje náročnou práci, kdy se článek načte a převede do HTML.


Nastavení závislostí
====================

Kdy položka vyexpiruje, popisují **závislosti**, tedy objekt `Nette\Caching\Dependencies`. Nejpohodlnější je nechat je vyplnit generátor. Stačí deklarovat jeho parametr s typem `Dependencies` a `load()` mu předá prázdný objekt:

```php
use Nette\Caching\Dependencies;

$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) {
	$dp->expire = '1 hour';
	return renderArticle($id);
});
```

Výhoda je, že se generátor může rozhodnout podle toho, co spočítal. Rozepsaný článek může třeba vyexpirovat dřív než publikovaný.

Když závislosti znáte předem, můžete je místo toho předat jako třetí argument metodě `load()`, případně metodě `save()`, která hodnotu uloží rovnou, bez generátoru. Konstruktor přijímá všechny vlastnosti jako pojmenované argumenty:

```php
$html = $cache->load("article/$id", fn() => renderArticle($id), new Dependencies(expire: '1 hour'));

$cache->save("article/$id", $html, new Dependencies(expire: '1 hour', tags: ["article/$id"]));
```

Dávejte přednost `load()` s generátorem před `save()`. Když o chybějící položku požádá víc požadavků najednou, `FileStorage` a `RedisStorage` spustí generátor jen u jednoho z nich; se `save()` by hodnotu počítaly všechny.

Závislosti lze zapsat i jako pole s klíči `Cache::*`, třeba `[Cache::Expire => '1 hour', Cache::Tags => ["article/$id"]]`. Generátor, jehož parametr nemá typ `Dependencies`, dostane toto pole referencí a přidává do něj: `function (&$dependencies)`. Neznámý klíč nebo hodnota špatného typu v poli vyhodí výjimku, takže překlep jako `'expir'` nenechá položku potichu bez expirace.


Expirace podle času
===================

Vlastnost `expire` určuje, kdy položka vyexpiruje. Přijímá relativní čas jako řetězec, počet sekund, UNIX timestamp nebo objekt `DateTimeInterface`:

```php
$dp->expire = '20 minutes';
$dp->expire = 3600; // jedna hodina
$dp->expire = new DateTimeImmutable('tomorrow');
```

U **klouzavé expirace** každé čtení posune expiraci dál. Článek, který návštěvníci pořád čtou, pak zůstává v cache, zatímco ten, který 20 minut nikdo neotevře, z ní vypadne:

```php
$dp->expire = '20 minutes';
$dp->sliding = true;
```

Počítejte s tím, že každé čtení pak položku v úložišti zároveň aktualizuje, což přidává režii.


Když se změní soubor
====================

Data spočítaná ze souboru by měla vyexpirovat, když se soubor změní. Dejme tomu, že blog čte seznam kategorií ze souboru `categories.neon`. Seznam v cache na tom souboru tedy závisí:

```php
$categories = $cache->load('categories', function (Dependencies $dp) {
	$dp->files[] = __DIR__ . '/categories.neon';
	return loadCategories(); // přečte a zpracuje categories.neon
});
```

Jakmile `categories.neon` upravíte, cache si toho při dalším čtení všimne a zavolá generátor znovu. Používejte absolutní cesty. Položka může záviset i na více souborech: `$dp->files = [$file1, $file2]`.


Když se změní konstanta
=======================

Podobně může položka vyexpirovat, když se změní hodnota PHP konstanty. Hodí se to třeba pro číslo verze, které nastavujete při nasazení, aby každá nová verze začínala s čerstvou cache:

```php
$dp->constants[] = 'APP_VERSION';
```


Když se změní jiná položka
==========================

Položka může záviset na jiných položkách v cache. Dejme tomu, že článek a jeho komentáře cachujete zvlášť, pod klíči `article/$id` a `comments/$id`, a cachujete i celou stránku z nich složenou. Stránka musí vyexpirovat, kdykoliv vyexpiruje některá z jejích částí:

```php
$page = $cache->load("page/$id", function (Dependencies $dp) use ($id) {
	$dp->items = ["article/$id", "comments/$id"];
	return renderPage($id); // poskládá článek a jeho komentáře
});
```

Když se komentáře znovu uloží nebo smažou, vyexpiruje i stránka.


Vlastní podmínka
================

Když nic z uvedeného nesedí, napište si vlastní funkci. Volá se při každém čtení s argumenty, které jste uložili, a když vrátí `false`, položka vyexpiruje. Dejme tomu, že vyrenderované HTML nesmí přežít upgrade PHP:

```php
function checkPhpVersion(int $version): bool
{
	return $version === PHP_VERSION_ID;
}

$dp->callbacks[] = ['checkPhpVersion', PHP_VERSION_ID];
```

Každá položka je pole: funkce, případně statická metoda zapsaná jako `[ClassName::class, 'method']`, a za ní její argumenty. Funkce se volá při každém čtení, takže by měla být rychlá.


Kombinace podmínek
==================

Všechny tyto podmínky lze kombinovat. Položka vyexpiruje, jakmile přestane platit alespoň jedna z nich:

```php
$dp->expire = '1 day';
$dp->files[] = __DIR__ . '/categories.neon';
$dp->constants[] = 'APP_VERSION';
```


Tagy
====

Čekat na expiraci je kompromis. Lepší je položku smazat přesně ve chvíli, kdy se změní data, ze kterých vznikla. To umožňují **tagy**. Jsou to libovolné řetězce, které položce přiřadíte:

```php
$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) {
	$dp->tags = ["article/$id", "comments/$id"];
	return renderArticle($id);
});
```

V administraci pak hned po uložení upraveného článku do databáze smažete všechno, co nese jeho tag:

```php
$cache->clean(tags: ["article/$id"]);
```

Totéž platí pro přidání komentáře, tentokrát s tagem `comments/$id`. Všimněte si, že kód, který položky maže, nemusí znát jejich klíče, ani kolik jich je. Článek může v cache ležet jako celá stránka, jako náhled na úvodní stránce i v RSS kanálu; jedno volání je odstraní všechny.

Tagy fungují napříč jmennými prostory: `clean(tags: [...])` smaže odpovídající položky ve všech jmenných prostorech úložiště, nejen v prostoru `$cache`.

.[note]
`FileStorage`, `MemcachedStorage` a `Psr16Storage` potřebují pro tagy [journal |storages#Journal]; ostatní úložiště si je drží sama.


Priorita
========

Každé položce můžete dát prioritu, tedy číslo, které říká, jak je důležitá. Když cache příliš naroste, můžete pak smazat nejdřív ty méně důležité. Dejme tomu, že náhledy starých článků mají menší cenu než úvodní stránka:

```php
$dp->priority = 10;
```

Takto smažete všechny položky s prioritou 50 nebo menší:

```php
$cache->clean(priority: 50);
```

Položky bez priority se tímto způsobem nikdy nesmažou.

.[note]
`FileStorage`, `MemcachedStorage` a `Psr16Storage` potřebují pro priority [journal |storages#Journal], `RedisStorage` a `MemoryStorage` si je drží samy a `SQLiteStorage` je nepodporuje.


Mazání položek
==============

Metoda `clean()` maže položky podle podmínek, které předáte jako pojmenované argumenty:

| Argument | Smaže
|----------------------
| `tags: [...]` | položky s kterýmkoliv z tagů
| `priority: 50` | položky s prioritou 50 nebo menší
| `namespaces: [...]` | všechny položky v uvedených jmenných prostorech, včetně prostorů z nich odvozených
| `all: true` | vše ve jmenném prostoru této cache, včetně prostorů z něj odvozených

Podmínky lze kombinovat a smaže se položka, která splňuje kteroukoliv z nich:

```php
$cache->clean(tags: ["article/$id"], priority: 50);
```

Podívejme se blíž na `all`. Smaže celý jmenný prostor cache spolu s prostory, které z něj vznikly pomocí `derive()`, a ostatních jmenných prostorů se nedotkne. Jen cache vytvořená bez jmenného prostoru, `new Cache($storage)`, takhle smaže celé úložiště.

Podmínky lze zapsat i jako pole s klíči `Cache::*`, třeba `$cache->clean([Cache::Tags => ["article/$id"]])`. Stejně jako u závislostí vyhodí neznámý klíč nebo hodnota špatného typu výjimku.

Volání `clean()` bez jakékoliv podmínky vyhodí výjimku také, protože není jasné, co by mělo smazat. Odstranit položky, které už vyexpirovaly, je věc úložiště: zavolejte `$storage->clean([])`. Většina úložišť to ostatně dělá sama, viz [Úložiště |storages].

Jednu položku smažete pomocí `$cache->remove("article/$id")`.


Co které úložiště umí
=====================

Ne každé úložiště umí každý druh závislosti. Úložiště, které některou neumí, vyhodí `Nette\NotSupportedException`, místo aby ji potichu ignorovalo, takže na to přijdete hned, a ne až podle zastaralých dat. `SQLiteStorage` třeba nepodporuje priority ani závislosti na souborech. Jedinou výjimkou je `MemoryStorage`, které ignoruje, co neumí. Úplný přehled najdete v tabulce na stránce [Úložiště |storages#Výběr úložiště].


Hromadné čtení a zápis
======================

Úvodní stránka blogu ukazuje náhledy deseti článků. Číst je z cache jeden po druhém znamená desetkrát se obrátit na úložiště. Metoda `bulkLoad()` je přečte všechny najednou: předáte jí pole `$ids` s jejich ID jako klíči a vrátí pole hodnot indexované těmito klíči:

```php
$previews = $cache->bulkLoad($ids, function ($id, Dependencies $dp) {
	$dp->tags = ["article/$id"];
	return renderPreview($id); // vyrenderuje krátký náhled článku
});
```

Generátor se zavolá jen pro náhledy, které chybí, a dostane klíč a za ním závislosti. Bez generátoru vrátí `bulkLoad($ids)` pro chybějící položky `null`.

Metoda `bulkSave()` uloží několik položek najednou, všechny se stejnými závislostmi:

```php
$cache->bulkSave([
	"preview/$id1" => $preview1,
	"preview/$id2" => $preview2,
], new Dependencies(expire: '1 hour'));
```

{{maintitle: Nette Caching: expirace, tagy a invalidace cache}}
{{description: Zajistěte, aby cache nikdy nevracela zastaralá data. Expirace podle času, změny souboru či konstanty a mazání podle tagů, priority a jmenného prostoru.}}

Expirace a invalidace

Cache je užitečná jen tak dlouho, dokud nevrací zastaralá data. Tato stránka ukazuje všechny způsoby, jak Nette Caching říct, kdy položka přestává platit:

  • po uplynutí doby, nebo když ji delší dobu nikdo nečetl
  • když se změní soubor, PHP konstanta nebo jiná položka v cache
  • když to řekne vaše vlastní funkce
  • nebo hned, když ji smažete podle tagu, priority či jmenného prostoru

Ukázky navazují na blog z úvodu: $cache je cache ve jmenném prostoru articles, $id je ID článku a renderArticle() zastupuje náročnou práci, kdy se článek načte a převede do HTML.

Nastavení závislostí

Kdy položka vyexpiruje, popisují závislosti, tedy objekt Nette\Caching\Dependencies. Nejpohodlnější je nechat je vyplnit generátor. Stačí deklarovat jeho parametr s typem Dependencies a load() mu předá prázdný objekt:

use Nette\Caching\Dependencies;

$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) {
	$dp->expire = '1 hour';
	return renderArticle($id);
});

Výhoda je, že se generátor může rozhodnout podle toho, co spočítal. Rozepsaný článek může třeba vyexpirovat dřív než publikovaný.

Když závislosti znáte předem, můžete je místo toho předat jako třetí argument metodě load(), případně metodě save(), která hodnotu uloží rovnou, bez generátoru. Konstruktor přijímá všechny vlastnosti jako pojmenované argumenty:

$html = $cache->load("article/$id", fn() => renderArticle($id), new Dependencies(expire: '1 hour'));

$cache->save("article/$id", $html, new Dependencies(expire: '1 hour', tags: ["article/$id"]));

Dávejte přednost load() s generátorem před save(). Když o chybějící položku požádá víc požadavků najednou, FileStorage a RedisStorage spustí generátor jen u jednoho z nich; se save() by hodnotu počítaly všechny.

Závislosti lze zapsat i jako pole s klíči Cache::*, třeba [Cache::Expire => '1 hour', Cache::Tags => ["article/$id"]]. Generátor, jehož parametr nemá typ Dependencies, dostane toto pole referencí a přidává do něj: function (&$dependencies). Neznámý klíč nebo hodnota špatného typu v poli vyhodí výjimku, takže překlep jako 'expir' nenechá položku potichu bez expirace.

Expirace podle času

Vlastnost expire určuje, kdy položka vyexpiruje. Přijímá relativní čas jako řetězec, počet sekund, UNIX timestamp nebo objekt DateTimeInterface:

$dp->expire = '20 minutes';
$dp->expire = 3600; // jedna hodina
$dp->expire = new DateTimeImmutable('tomorrow');

U klouzavé expirace každé čtení posune expiraci dál. Článek, který návštěvníci pořád čtou, pak zůstává v cache, zatímco ten, který 20 minut nikdo neotevře, z ní vypadne:

$dp->expire = '20 minutes';
$dp->sliding = true;

Počítejte s tím, že každé čtení pak položku v úložišti zároveň aktualizuje, což přidává režii.

Když se změní soubor

Data spočítaná ze souboru by měla vyexpirovat, když se soubor změní. Dejme tomu, že blog čte seznam kategorií ze souboru categories.neon. Seznam v cache na tom souboru tedy závisí:

$categories = $cache->load('categories', function (Dependencies $dp) {
	$dp->files[] = __DIR__ . '/categories.neon';
	return loadCategories(); // přečte a zpracuje categories.neon
});

Jakmile categories.neon upravíte, cache si toho při dalším čtení všimne a zavolá generátor znovu. Používejte absolutní cesty. Položka může záviset i na více souborech: $dp->files = [$file1, $file2].

Když se změní konstanta

Podobně může položka vyexpirovat, když se změní hodnota PHP konstanty. Hodí se to třeba pro číslo verze, které nastavujete při nasazení, aby každá nová verze začínala s čerstvou cache:

$dp->constants[] = 'APP_VERSION';

Když se změní jiná položka

Položka může záviset na jiných položkách v cache. Dejme tomu, že článek a jeho komentáře cachujete zvlášť, pod klíči article/$id a comments/$id, a cachujete i celou stránku z nich složenou. Stránka musí vyexpirovat, kdykoliv vyexpiruje některá z jejích částí:

$page = $cache->load("page/$id", function (Dependencies $dp) use ($id) {
	$dp->items = ["article/$id", "comments/$id"];
	return renderPage($id); // poskládá článek a jeho komentáře
});

Když se komentáře znovu uloží nebo smažou, vyexpiruje i stránka.

Vlastní podmínka

Když nic z uvedeného nesedí, napište si vlastní funkci. Volá se při každém čtení s argumenty, které jste uložili, a když vrátí false, položka vyexpiruje. Dejme tomu, že vyrenderované HTML nesmí přežít upgrade PHP:

function checkPhpVersion(int $version): bool
{
	return $version === PHP_VERSION_ID;
}

$dp->callbacks[] = ['checkPhpVersion', PHP_VERSION_ID];

Každá položka je pole: funkce, případně statická metoda zapsaná jako [ClassName::class, 'method'], a za ní její argumenty. Funkce se volá při každém čtení, takže by měla být rychlá.

Kombinace podmínek

Všechny tyto podmínky lze kombinovat. Položka vyexpiruje, jakmile přestane platit alespoň jedna z nich:

$dp->expire = '1 day';
$dp->files[] = __DIR__ . '/categories.neon';
$dp->constants[] = 'APP_VERSION';

Tagy

Čekat na expiraci je kompromis. Lepší je položku smazat přesně ve chvíli, kdy se změní data, ze kterých vznikla. To umožňují tagy. Jsou to libovolné řetězce, které položce přiřadíte:

$html = $cache->load("article/$id", function (Dependencies $dp) use ($id) {
	$dp->tags = ["article/$id", "comments/$id"];
	return renderArticle($id);
});

V administraci pak hned po uložení upraveného článku do databáze smažete všechno, co nese jeho tag:

$cache->clean(tags: ["article/$id"]);

Totéž platí pro přidání komentáře, tentokrát s tagem comments/$id. Všimněte si, že kód, který položky maže, nemusí znát jejich klíče, ani kolik jich je. Článek může v cache ležet jako celá stránka, jako náhled na úvodní stránce i v RSS kanálu; jedno volání je odstraní všechny.

Tagy fungují napříč jmennými prostory: clean(tags: [...]) smaže odpovídající položky ve všech jmenných prostorech úložiště, nejen v prostoru $cache.

FileStorage, MemcachedStorage a Psr16Storage potřebují pro tagy journal; ostatní úložiště si je drží sama.

Priorita

Každé položce můžete dát prioritu, tedy číslo, které říká, jak je důležitá. Když cache příliš naroste, můžete pak smazat nejdřív ty méně důležité. Dejme tomu, že náhledy starých článků mají menší cenu než úvodní stránka:

$dp->priority = 10;

Takto smažete všechny položky s prioritou 50 nebo menší:

$cache->clean(priority: 50);

Položky bez priority se tímto způsobem nikdy nesmažou.

FileStorage, MemcachedStorage a Psr16Storage potřebují pro priority journal, RedisStorage a MemoryStorage si je drží samy a SQLiteStorage je nepodporuje.

Mazání položek

Metoda clean() maže položky podle podmínek, které předáte jako pojmenované argumenty:

Argument Smaže
tags: [...] položky s kterýmkoliv z tagů
priority: 50 položky s prioritou 50 nebo menší
namespaces: [...] všechny položky v uvedených jmenných prostorech, včetně prostorů z nich odvozených
all: true vše ve jmenném prostoru této cache, včetně prostorů z něj odvozených

Podmínky lze kombinovat a smaže se položka, která splňuje kteroukoliv z nich:

$cache->clean(tags: ["article/$id"], priority: 50);

Podívejme se blíž na all. Smaže celý jmenný prostor cache spolu s prostory, které z něj vznikly pomocí derive(), a ostatních jmenných prostorů se nedotkne. Jen cache vytvořená bez jmenného prostoru, new Cache($storage), takhle smaže celé úložiště.

Podmínky lze zapsat i jako pole s klíči Cache::*, třeba $cache->clean([Cache::Tags => ["article/$id"]]). Stejně jako u závislostí vyhodí neznámý klíč nebo hodnota špatného typu výjimku.

Volání clean() bez jakékoliv podmínky vyhodí výjimku také, protože není jasné, co by mělo smazat. Odstranit položky, které už vyexpirovaly, je věc úložiště: zavolejte $storage->clean([]). Většina úložišť to ostatně dělá sama, viz Úložiště.

Jednu položku smažete pomocí $cache->remove("article/$id").

Co které úložiště umí

Ne každé úložiště umí každý druh závislosti. Úložiště, které některou neumí, vyhodí Nette\NotSupportedException, místo aby ji potichu ignorovalo, takže na to přijdete hned, a ne až podle zastaralých dat. SQLiteStorage třeba nepodporuje priority ani závislosti na souborech. Jedinou výjimkou je MemoryStorage, které ignoruje, co neumí. Úplný přehled najdete v tabulce na stránce Úložiště.

Hromadné čtení a zápis

Úvodní stránka blogu ukazuje náhledy deseti článků. Číst je z cache jeden po druhém znamená desetkrát se obrátit na úložiště. Metoda bulkLoad() je přečte všechny najednou: předáte jí pole $ids s jejich ID jako klíči a vrátí pole hodnot indexované těmito klíči:

$previews = $cache->bulkLoad($ids, function ($id, Dependencies $dp) {
	$dp->tags = ["article/$id"];
	return renderPreview($id); // vyrenderuje krátký náhled článku
});

Generátor se zavolá jen pro náhledy, které chybí, a dostane klíč a za ním závislosti. Bez generátoru vrátí bulkLoad($ids) pro chybějící položky null.

Metoda bulkSave() uloží několik položek najednou, všechny se stejnými závislostmi:

$cache->bulkSave([
	"preview/$id1" => $preview1,
	"preview/$id2" => $preview2,
], new Dependencies(expire: '1 hour'));