Nette Caching

Počítá vaše aplikace pořád dokola totéž, třeba převádí článek do HTML, spouští pomalý databázový dotaz nebo volá vzdálené API? Nette Caching si výsledek zapamatuje a příště ho vrátí ve zlomku času:

  • Jedna metoda hodnotu přečte z cache, a když tam chybí, vypočítá a uloží ji
  • Data vyexpirují po čase, nebo když se změní soubor, jiná položka či vaše vlastní podmínka
  • Tagy smažou přesně ty položky, které souvisejí s právě upraveným článkem
  • Nával požadavků na chybějící položku server nepřetíží
  • Ukládá do souborů, do Memcached nebo SQLite
  • Cachuje části šablon Latte

Instalace

Knihovnu stáhnete a nainstalujete pomocí nástroje Composer:

composer require nette/caching

První cache

Nejlíp se to ukáže na příkladu. Dejme tomu, že provozujete blog. Pokaždé, když někdo otevře článek, načte ho váš kód z databáze a převede jeho text do HTML. Chvíli to trvá a výsledek je pro všechny návštěvníky stejný, dokud článek neupravíte. Ideální kandidát pro cache.

Napíšeme si to jako krátký skript. Nejprve vytvoříme dva objekty:

use Nette\Caching\Cache;
use Nette\Caching\Storages\FileStorage;

require __DIR__ . '/vendor/autoload.php';

$storage = new FileStorage(__DIR__ . '/temp');
$cache = new Cache($storage, 'articles');

Úložiště určuje, kde data fyzicky leží. FileStorage je ukládá do souborů v adresáři temp, který musí existovat. Cache je objekt, se kterým budete pracovat. Jeho druhý argument, 'articles', je jmenný prostor: odděluje články od všeho ostatního, co aplikace cachuje, takže dvě části programu mohou použít stejný klíč, aniž by si překážely.

Teď to podstatné. Cache funguje jako asociativní pole: hodnoty čtete a zapisujete pod klíči. Metoda load() dělá obojí najednou:

$html = $cache->load("article/$id", function () use ($id) {
	return renderArticle($id); // načte článek z databáze a převede ho do HTML
});

Proměnná $id je ID zobrazovaného článku. Prvním argumentem load() je klíč, druhým funkce, které se říká generátor. Když klíč v cache ještě není, load() generátor zavolá, uloží, co vrátí, a vrátí to vám. Příště už hodnotu v cache najde a vrátí ji rovnou, generátor vůbec nevolá.

Všimněte si, že nikde nezjišťujete, jestli už hodnota v cache je. Jen řeknete, jak ji spočítat.

A co když stejný článek otevře sto návštěvníků zrovna ve chvíli, kdy v cache ještě není, třeba hned po nasazení? Bez ochrany by ho všech sto požadavků renderovalo naráz a mohlo by server přetížit. Takové situaci se říká cache stampede a FileStorage jí brání: generátor spustí jen první požadavek, ostatní počkají na jeho výsledek.

Do cache můžete ukládat jakoukoliv hodnotu, kterou PHP umí serializovat, nejen řetězce: pole, objekty, čísla. Totéž platí i pro klíče.

Pokud chcete hodnotu jen přečíst a negenerovat, zavolejte load() jen s klíčem; když položka v cache není, vrátí null. Metoda remove() položku smaže:

$cache->remove("article/$id");

Jak dlouho data platí?

Náš článek v cache se teď nikdy nezmění. Jenže co když v něm opravíte překlep? Návštěvníci by dál viděli starou verzi. Musíte cache říct, kdy položka přestává platit. Nejjednodušší je časový limit:

$html = $cache->load("article/$id", function (&$dependencies) use ($id) {
	$dependencies[Cache::Expire] = '1 hour';
	return renderArticle($id);
});

Generátor teď přijímá referencí parametr $dependencies. Je to pole, do kterého generátor zapíše, kdy položka vyexpiruje. Říká se mu závislosti, protože na nich závisí platnost položky. Tady je to jedna hodina.

Čas je jen jedna z možností. Položka může vyexpirovat i ve chvíli, kdy se změní soubor nebo jiná položka v cache, případně když to řekne vaše vlastní podmínka. To všechno popisuje stránka Expirace a invalidace.

Smazat přesně to, co se změnilo

Časový limit je kompromis: nastavíte ho krátký a cache moc nepomůže, nastavíte dlouhý a návštěvníci vidí stará data. Lepší je smazat článek z cache přesně ve chvíli, kdy ho upravíte. K tomu slouží tagy. Položce je přiřadíte jako libovolné řetězce:

$html = $cache->load("article/$id", function (&$dependencies) use ($id) {
	$dependencies[Cache::Tags] = ["article/$id", "comments/$id"];
	return renderArticle($id);
});

Stránka ukazuje článek i s komentáři, proto jsme jí dali dva tagy. V administraci pak hned po uložení upraveného článku do databáze smažete všechno, co nese jeho tag:

$cache->clean([Cache::Tags => ["article/$id"]]);

A totéž, když někdo přidá komentář, tentokrát s tagem comments/$id. Od teď stránka z cache zmizí přesně ve chvíli, kdy se změní její článek nebo komentáře, a další návštěvník dostane čerstvě vyrenderovanou.

FileStorage si pamatuje, která položka nese který tag, v tzv. journalu, malé SQLite databázi. V aplikaci postavené na Nette je nastavený za vás; v našem skriptu ho vytvoříme a předáme úložišti:

use Nette\Caching\Storages\SQLiteJournal;

$journal = new SQLiteJournal(__DIR__ . '/temp/journal.s3db');
$storage = new FileStorage(__DIR__ . '/temp', $journal);

Kromě tagů můžete mazat položky podle priority nebo celý jmenný prostor najednou. Viz Expirace a invalidace.

Jmenné prostory

Jmenný prostor 'articles' jsme už potkali v konstruktoru cache. Každá část aplikace by měla mít svůj, aby se její klíče nikdy nepotkaly s cizími. Komentáře by mohly mít třeba vlastní cache:

$commentCache = new Cache($storage, 'comments');

Metoda derive() vytvoří cache ve vnořeném jmenném prostoru: $cache->derive('previews') dá prostor previews uvnitř articles. A když chcete zahodit všechno ve jmenném prostoru, zavolejte:

$cache->clean([Cache::Namespaces => ['articles']]);

Ostatní jmenné prostory, i ty vnořené, zůstanou nedotčené. Mazání podle jmenného prostoru funguje s FileStorage, ostatní úložiště ho ignorují. Pozor na Cache::All, který smaže celé úložiště včetně všech jmenných prostorů.

Memoizace

Někdy chcete cachovat výsledek volání funkce. V moderaci komentářů třeba ukazujete u každého autora název počítače podle jeho IP adresy. Zjistit ho funkcí gethostbyaddr() znamená zeptat se DNS serveru a to může trvat i sekundy. Metoda call() zavolá funkci pro každý argument jen jednou a pak vrací výsledek z cache:

$host = $cache->call('gethostbyaddr', $ip);

Proměnná $ip je IP adresa autora komentáře. Metoda wrap() dělá totéž, ale místo aby funkci hned zavolala, vrátí novou funkci, kterou můžete volat později a kolikrát chcete:

$getHost = $cache->wrap('gethostbyaddr');

$host = $getHost($ip); // poprvé se zeptá DNS serveru
$host = $getHost($ip); // podruhé vrátí výsledek z cache

Cachování části stránky

Ne všechno, co chcete cachovat, je návratová hodnota. Někdy je to kus HTML, který nějaký kód vypisuje. Ten zachytí metoda capture():

if ($capture = $cache->capture('sidebar')) {
	// echo ... vypíše postranní panel s nejnovějšími články
	$capture->end(); // uloží vypsaný výstup do cache
}

Když už je výstup v cache, capture() ho vypíše a vrátí null, takže se blok uvnitř if vůbec nevykoná. Jinak začne výstup zachytávat a vrátí objekt, jehož metoda end() uloží všechno, co se mezitím vypsalo. Můžete jí předat i závislosti, třeba $capture->end([Cache::Tags => ['articles']]). Ve verzi 3.0 se metoda capture() jmenovala start().

Cachování v Latte

V šablonách Latte je to ještě jednodušší. Část, kterou chcete cachovat, obalíte značkami {cache}...{/cache}:

{cache expire: '20 minutes', tags: [articles]}
	<h2>Nejnovější články</h2>
	...
{/cache}

Blok se automaticky zneplatní, kdykoliv se změní šablona, včetně šablon vložených uvnitř bloku. Bloky lze vnořovat; když se zneplatní vnořený blok, zneplatní se i ten nadřazený.

Všechny parametry jsou volitelné. Bez expire blok vyexpiruje po 7 dnech. Pokud obsah závisí na proměnné, uveďte ji na začátku a pro každou hodnotu vznikne vlastní kopie v cache: {cache $article->id}. A parametr if rozhodne, jestli vůbec cachovat:

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

Kam se data ukládají

Zatím jsme používali FileStorage, kterému stačí adresář. Díky atomickým operacím se soubory je bezpečné, i když najednou čte a zapisuje mnoho požadavků. Je to ale jen jedno z několika úložišť:

Úložiště Kde data leží
FileStorage v souborech na disku; výchozí úložiště
MemcachedStorage na serveru Memcached
SQLiteStorage v souboru databáze SQLite
MemoryStorage v paměti, jen do konce požadavku
DevNullStorage nikde; pro testy a pro vypnutí cache

Úložiště můžete vyměnit, aniž byste sáhli na kód, který cache používá. Které si vybrat, co které umí a jak ho nastavit, včetně vypnutí cache, popisuje stránka Úložiště.

Cache v aplikaci Nette

V aplikaci postavené na Nette si úložiště sami nevytváříte. Nette poskytuje FileStorage, které ukládá data do podadresáře cache v adresáři pro dočasné soubory, a k němu i journal. Dostanete ho pomocí dependency injection, když si řeknete o typ Nette\Caching\Storage (před verzí 3.1 se jmenoval Nette\Caching\IStorage), a cache si vytvoříte v konstruktoru:

use Nette\Caching\Cache;
use Nette\Caching\Storage;

class ArticleFacade
{
	private Cache $cache;


	public function __construct(Storage $storage)
	{
		$this->cache = new Cache($storage, 'articles');
	}
}

Zbytek je stejný jako v našem skriptu. Případně si můžete nechat do konstruktoru předat rovnou objekt Cache a nadefinovat ho v konfiguraci:

services:
	- ArticleFacade( Nette\Caching\Cache(namespace: 'articles') )

Cache šablon Latte a DI kontejneru je samostatná a toto úložiště nepoužívá. Během vývoje ji není potřeba vypínat.

Co dál

  • Expirace a invalidace – všechny způsoby, jak cache říct, kdy data vyexpirují, a mazání podle tagů, priority a jmenného prostoru
  • Úložiště – výběr a nastavení úložiště, PSR-16, vlastní úložiště
  • Upgrade – co se změnilo v jednotlivých verzích
verze: 4.x 3.x 2.x