Popis Cache třídy¶
Finální třída Cache v core/Cache/Cache.php – vlastní implementace bez dependencies. Minimalistická, transparentní, určená pro malý tým.
Třída Cache¶
Statická třída pro file-based cachování. Bez ORM, bez Stash, bez magic. Co vidíš v kódu, to se děje.
Design Goals¶
- ✓ Minimal IO
- ✓ Expirace uložena UVNITŘ souboru (ne v
mtime) – viz overview.md, aby se netloukla s OPcache - ✓ Atomic writes (unikátní tmp → rename)
- ✓ Hashed directory structure (2-level:
XX/YY/hash.php) - ✓
read()nikdy nemaže soubor – úklid dělá výhradněpurge()/clear()
Konstanta – Default TTL¶
Lze přepsat v třetím parametru metod set() a file().
Úložiště¶
- Cesta:
$path(inicializovaná vCache::init()) - Oprávnění:
0770na adresáře (přesmkdir()) - Struktura:
path/ab/cd/{md5(key)}.php ab= znaky 0–1 z MD5(key)cd= znaky 2–3 z MD5(key)- název souboru = celý MD5(key)
Inicializace¶
init() normalizuje cestu (rtrim '/'), zamítne prázdnou/root cestu a vytvoří adresář.
Veřejné metody¶
init(string $path): void¶
Inicializuje cache storage path. Volej jednou v bootstrapu.
Vyhodí InvalidArgumentException, pokud je $path po rtrim('/') prázdný řetězec.
set(string $key, mixed $value, int $ttl = self::SECONDS): void¶
Uloží data do cache s TTL. Expirace ($expiresAt = time() + $ttl) je uložena jako první
prvek pole přímo v obsahu souboru, ne ve filemtime().
$expiresAt = time() + $ttl;
$php = "<?php\n\nreturn [" . $expiresAt . ', ' . var_export($value, true) . "];\n";
// unikátní tmp jméno – sdílené jméno by umožnilo souběžnému zápisu přepsat rozepsaný soubor
$tmp = $file . '.' . bin2hex(random_bytes(8)) . '.tmp';
file_put_contents($tmp, $php);
rename($tmp, $file);
opcache_invalidate($file, true);
Argumenty:
$key– Cache key (string, libovolný)$value– Data, vizassertCacheable()níže$ttl– Expirace v sekundách (default: 3600)
Validace:
$ttl < 0nebo$ttl > PHP_INT_MAX - time()(přetečení) →InvalidArgumentException- Necacheable hodnota (viz níže) →
InvalidArgumentException - Zápis nebo rename selže →
RuntimeException
Příklad:
get(string $key): mixed¶
Načte data z cache. Vrací null pokud neexistuje, je expirovaná, nebo je soubor
poškozený/neplatný.
$entry = self::read($file); // [$expiresAt, $value] nebo null
if ($entry === null) {
return null;
}
[$expiresAt, $value] = $entry;
if ($expiresAt <= time()) {
return null; // expirováno – soubor se NEMAŽE (viz purge())
}
return $value;
Důležité: get() expirovaný soubor nemaže. Mazání při čtení by mohlo smazat
záznam, který mezitím přepsal souběžný writer novými daty (race condition). Úklid
expirovaných souborů dělá výhradně purge().
Vrací:
mixed– Uložená datanull– Cache miss, expirace nebo poškozený soubor
Příklad:
$users = Cache::get('users_all');
if ($users === null) {
$users = DB::results("SELECT * FROM users");
Cache::set('users_all', $users, 1800);
}
delete(string $key): void¶
Smaž konkrétní cache položku okamžitě (pokud existuje) a invaliduj OPcache.
Příklad:
// Po UPDATE musíš invalidovat cache
DB::query("UPDATE users SET name = ? WHERE id = ?", [$name, $id]);
Cache::delete('users_all');
clear(): void¶
Vymaž VŠECHNY cache soubory (bez ohledu na expiraci) a prázdné adresáře.
foreach (self::files() as $file) { // glob path/*/*/*
self::removeFile($file); // unlink + opcache_invalidate
}
foreach (glob(self::path() . '/*/*') as $dir) {
@rmdir($dir);
}
foreach (glob(self::path() . '/*') as $dir) {
@rmdir($dir);
}
self::files() vrací i osiřelé .tmp soubory – clear() je smaže také.
purge(): void¶
Odstraní EXPIROVANÉ cache soubory a osiřelé .tmp soubory (např. po pádu procesu mezi
file_put_contents() a rename()).
$now = time();
foreach (self::files() as $file) {
if (str_ends_with($file, '.tmp')) {
// starší než default TTL → osiřelý zápis, smaž
if (filemtime($file) < $now - Cache::SECONDS) {
self::removeFile($file);
}
continue;
}
$entry = self::read($file);
if ($entry === null || $entry[0] <= $now) { // poškozený nebo expirovaný
self::removeFile($file);
}
}
Toto je jediné místo, které skutečně maže expirované záznamy – proto se má volat pravidelně (cron), viz usage.md.
file(string $filepath, string $type = 'yaml', int $ttl = self::SECONDS): array¶
Speciální metoda pro cachování obsahu souborů (aktuálně jen YAML). Cache záznam si
nese hash zdrojového obsahu – jakákoliv změna souboru (i restore se starým mtime,
i přepis stejné velikosti ve stejnou sekundu) záznam invaliduje.
$realpath = realpath($filepath); // kanonizace cesty
$content = file_get_contents($realpath);
$key = md5($type . ':' . $realpath);
$hash = hash('xxh128', $content);
$cached = self::get($key);
if (is_array($cached) && $cached['hash'] === $hash && is_array($cached['data'] ?? null)) {
return $cached['data'];
}
$data = Yaml::parse($content) ?? [];
self::set($key, ['hash' => $hash, 'data' => $data], $ttl);
return $data;
Argumenty:
$filepath– Cesta k souboru (relativní i absolutní – kanonizuje se přesrealpath())$type– Typ souboru; podporováno pouze'yaml'(jiný typ vrátí[])$ttl– Expirace v sekundách (default: 3600)
Výjimky:
- Soubor neexistuje / nejde přečíst →
RuntimeException - YAML root není mapa/list (např. skalár) →
RuntimeException
Vrací:
array– Parsovaný obsah souboruarray– Prázdné pole, pokud$type !== 'yaml'
Příklad:
// První volání: parsuje + cachuje (uloží i hash obsahu)
$settings = Cache::file('app/config/settings.yaml');
// Druhé volání: vrací z cache, pokud se soubor nezměnil
$settings = Cache::file('app/config/settings.yaml');
Privátní metody a properties¶
$path¶
Inicializovaná v init(). Přístup jen přes self::path(), která vyhodí
LogicException, pokud init() ještě neproběhl.
read(string $file): ?array¶
Načte a rozbalí surový cache záznam ([$expiresAt, $value]). Nikdy nemaže soubor.
Chráněné proti:
- neexistujícímu souboru (is_file() check)
- výjimce z include (poškozený/zkrácený soubor → miss, ne fatal error)
- neplatné struktuře (include vrátí false/1/cokoliv, co není [int, mixed])
assertCacheable(mixed $value, string $key, int $depth = 0): void¶
Zamítne hodnoty, které var_export() neumí spolehlivě zrekonstruovat – kontroluje se
při zápisu (set()), ne až při čtení.
Povoleno: skaláry, null, pole (rekurzivně), UnitEnum, stdClass (rekurzivně po
property). Cokoliv jiného objektové nebo resource → InvalidArgumentException.
Max hloubka rekurze 64 (ochrana proti cyklickým strukturám).
files(): array¶
glob(path/*/*/*) – všechny soubory ve stromu, včetně osiřelých .tmp.
pathFor(string $key): string¶
Spočítá cestu k souboru z MD5 hashe klíče.
makeDir(string $dir): void¶
mkdir($dir, 0770, true) – vytvoří adresář, pokud neexistuje.
removeFile(string $file): void¶
unlink() (pokud soubor existuje) + invalidate().
invalidate(string $file): void¶
opcache_invalidate($file, true), pokud je funkce dostupná (CLI SAPI ji obvykle nemá).
Chování¶
Cache hit¶
Cache miss (neexistuje)¶
Cache miss (expirovaná)¶
Cache::set('key', $data, 1); // TTL = 1 sekunda
sleep(2);
$data = Cache::get('key');
// Vrací: null – soubor ale zůstává na disku, dokud ho neuklidí purge()/clear()
File format¶
První prvek pole je $expiresAt (unix timestamp), druhý je uložená hodnota z
var_export(). Uloženo takto (ne přes mtime) kvůli konfliktu s OPcache – viz
overview.md.
Výhody a omezení¶
✓ Výhody¶
- Žádné dependencies – Stash je pryč
- Atomické writes – unikátní tmp file + rename (no corruption, no race mezi souběžnými writery)
- Expirace nezávislá na OPcache – žádný konflikt mtime vs. zkompilovaný soubor
- Transparent – čti soubory přímo v debuggingu
- Hashovaná struktura – miliony souborů bez FS problémů
- Bez race condition při čtení –
read()nikdy nemaže
⚠️ Omezení¶
- Filesystem-only – nejde Redis, Memcached, atd.
- Lokální pouze – multi-server cache potřebuje ruční řešení
- Malá data – optimizováno pro konfiguraci, DB výsledky, ne binární data
- Omezené typy hodnot – jen to, co
var_export()/assertCacheable()umí bezpečně zrekonstruovat (skaláry, pole,stdClass, enumy) - Expirované soubory se nemažou samy – nutný pravidelný
purge()(cron)
Typické use cases¶
- DB query results – Cachuj výsledky
SELECT, které se opakují - YAML config – Cachuj parsované config soubory (
Cache::file()) - API responses – Cachuj externí API volání
- Expensive computations – Math, parsing, transformations