Caching
A file-backed cache with one method worth knowing. The producer contract is easy to break silently, and a few things must never go into it.
Overview
Repeated reads that are the same for everyone are cached: a catalogue, a menu tree, a price table. Entries are grouped into stores, one file per store, and reached through a single read-through call.
The installation-wide cache switch is honoured inside that call, so the producer runs on every request when caching is off. Do not check the switch yourself before calling it.
Reference
public static function remember(string $name, string $key, int $ttl, callable $producer);
public static function getInstance(): self;
// $config: a string is the store name, an array accepts 'name', 'path', 'extension'.
public function __construct($config = []);
public function store($key, $data, $expiration = 86400): bool;
public function retrieve($key, $timestamp = false);
public function isCached($key): bool;
public function erase($key): self;
public function eraseExpired(): int;
public function eraseAll(): self;
public function clear($keys = []): void;
$ttl seconds and returns that.
new also replaces that shared instance, which is accepted behaviour but worth knowing.
$expiration of 0 means it never expires. Answers false when the licence domain does not match the host.
null when it is absent or unreadable. The second argument asks for the write time instead of the value. That field is stored unserialized and comes back as false, so do not build on it.
isCached() or catch.
Store, Key, Lifetime
| Argument | Names | Ends up as |
|---|---|---|
$name | The store | One file under the storage cache directory, <name>.cache. Lowercased, and anything outside letters, digits, dot, underscore and hyphen is stripped. |
$key | One entry inside that store | An array key in that file, holding the serialized value together with its write time and lifetime. |
$ttl | The lifetime in seconds | 3600 for an hour, 86400 for a day of static reference data, 0 for never expires. |
What the Producer Must Return
Anything that survives serialize() and comes back equal: arrays, strings, numbers, plain objects. Closures, resources and open connections do not. Declare the return type on the closure so a wrong shape fails where it is written rather than where it is read.
// Fine: an array, and the type says so.
Cache::remember('website', 'tld_requirements', 3600, fn (): array => self::build_requirements());
// Fine: a rendered string.
Cache::remember('website', 'sitemap_' . $lang, 3600, fn (): string => $this->build($lang));
// Fine: several captured values, block body.
Cache::remember('website', 'plans_' . $categoryId . '_' . $ucid, 3600, function () use ($categoryId, $ucid): array {
$rows = self::plans($categoryId);
return self::decorate($rows, $ucid);
});
// Broken: null is written but reads back as "missing", so the producer runs
// on every request forever and the cache silently does nothing.
Cache::remember('website', 'maybe_nothing', 3600, fn () => null);
What Belongs in the Key
A cached value is shared by everyone who computes the same key. The key has to name everything the value depends on.
| If the value depends on | The key must carry | Typical fragment |
|---|---|---|
| Money or formatting | The currency being served | Money::selected() |
| Any text or link | The active language | Language::selected() |
| A record, category or filter | That identifier | $categoryId |
| The signed-in visitor | Nothing, because it must not be cached | Not applicable |
Example
static function catalog_plans(int $categoryId, string $type, string $layout = 'grid'): array
{
$ucid = Money::selected();
// Prices depend on the currency and labels on the language, so the key carries
// both, plus everything that selects the rows.
$plans = Cache::remember('website', 'plans_' . $categoryId . '_' . $type . '_' . $layout . '_' . $ucid . '_' . Language::selected(), 3600,
fn (): array => self::catalog_plans_fresh($categoryId, $type, $layout, $ucid));
// Stock moves with every order, so it is refreshed live over the cached payload
// rather than being part of it.
if ($plans) {
$stocks = self::catalog_stocks(array_map(fn ($p) => (int) $p["id"], $plans));
foreach ($plans as &$plan) {
$stock = trim((string) ($stocks[(int) $plan["id"]] ?? ''));
$plan["in_stock"] = ($stock === '' || (int) $stock > 0);
}
unset($plan);
}
// Hooks run on EVERY request, so they sit outside the producer. The context goes
// into a variable first: runRefs takes all of its arguments by reference.
$ctx = ['category_id' => $categoryId, 'type' => $type, 'layout' => $layout];
Hook::runRefs('filter:product.catalog_plans', $plans, $ctx);
return $plans;
}
trait AdminManageWebsite
{
public function save_menu_changes(Operation $operation): bool
{
$operation->demo();
// ... write the rows ...
// Targeted: only the 'menus' store, addressed by store name.
Cache::getInstance()->clear(['menus']);
return $operation->output(['status' => "successful"]);
}
}
// Removing a single entry is the other method, and it throws on a miss.
$cache = new Cache('website');
if ($cache->isCached('plans_5_hosting_grid_1_en')) $cache->erase('plans_5_hosting_grid_1_en');
Pitfalls
Passing an entry key to clear() does not remove that entry. It treats the string as a store name. It then deletes a file that probably does not exist. The stale entry stays exactly where it was, with no error. Use erase() for one entry, and remember it throws when the key is not there.
Stock levels, a cart, anything behind a sign-in. A cached per-visitor value is served to the next visitor, which is a data leak rather than a stale page.
A hook inside the producer only fires on a cache miss. A listener appears to work while the entry is cold, then silently stops for the rest of the lifetime. Cache the data, then let listeners act on it.
If an operator can edit the value from a screen, that screen has to clear the store. Newly cached data whose editing screen does not clear keeps serving the old version for the whole lifetime. The report that reaches you will be about the screen, not about the cache.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.