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