AI blog · DotApp PHP Framework 2.0
How to use cache in DotApp PHP Framework
Cache is a named store for data you can rebuild: top-selling items, a rendered fragment, a remote catalog snapshot.
Open it with Cache::use('Shop'). save() is chainable. load() returns the value or null on a miss — never false.
An optional $context array is folded into the physical key, so the same logical key can hold per-user entries.
TTL defaults to Config::cache('lifetime') (36000 seconds) when you pass null. Switch File, Redis, Memcached, or Null in app/config.php.
Do not enable Renderer::useCache(true) (broken). Do not set Config::db('cache', true) with ORM Entity::save() (missing deleteKeys()).
This article is a complete Shop catalog controller that caches a list, plus driver rules and a custom callable map.
Common mistakes
| Wrong | Right |
|---|---|
Treat load() miss as false. |
Miss is null. Compare !== null. |
Put the visitor cart in Cache::use('Shop'). |
Cart is session state: DSM::use('Shop'). |
Call Renderer::useCache(true) to cache HTML. |
That API is broken. Cache data in Shop, then render. |
Set Config::db('cache', true) and save ORM entities. |
Keep query cache off. Shipped drivers have no deleteKeys(). |
clear() on Memcached to drop one Shop prefix. |
Memcached clear() flushes the whole server. Use delete($key, $context). |
Trust Redis clear() to drop every Shop key. |
Redis key tracking is buggy. Delete known keys, or accept the gap. |
Reuse one logical key for every user without $context. |
Pass ['user' => $userId] so physical keys stay isolated. |
Register a custom driver missing gc. |
Required callables: save, load, exists, delete, clear, gc. Missing → \Exception. |
When to use cache — and when not
| Store | Use when | Do not use when |
|---|---|---|
Cache::use('Shop') |
Derived lists you can rebuild; shared across visitors or keyed by $context |
Per-browser carts, CSRF material, auth identity |
DSM::use('Shop') |
Visitor application state that must survive the next request | Public catalog pages identical for everyone |
HTTP headers on Response |
A public page that intermediaries may reuse | Personalized HTML, or as a substitute for Cache::save |
Cache is not DSM. DSM is not HTTP caching. Mixing them is the usual Shop bug: a cached cart leaks across visitors, or a catalog is stored in the session and never shared. Sessions: How to use sessions in DotApp PHP Framework (DSM).
Cache::use('Shop') — save, load, exists, delete, clear, gc
Cache::use($cacheName = null, $folder = null, $driver = null) returns a singleton per name. Pass 'Shop'.
Defaults: folder ← Config::cache('folder'), driver ← Config::cache('driver').
use Dotsystems\App\Parts\Cache;
$cache = Cache::use('Shop');
$cache->save('items.top', $rows, 600);
$rows = $cache->load('items.top');
if ($rows === null) {
$rows = /* query */;
$cache->save('items.top', $rows, 600);
}
$context is folded into the physical key. Same logical key + different context = different entry:
$cache->save('menu', $items, 600, ['user' => $userId]);
$items = $cache->load('menu', ['user' => $userId]);
$cache->delete('menu', ['user' => $userId]);
load($key, $context = [], $destroy = false) — third argument true drops the entry after read.
exists($key, $context = [], $load = false) — bool, or the value when $load is true.
gc() is driver-specific (file expires; Redis/Memcached/Null are no-ops for GC).
API table
| Method | Args | Exact return |
|---|---|---|
Cache::use($name, $folder, $driver) |
All optional; use 'Shop' |
Cache singleton |
save($key, $data, $lifetime = null, $context = []) |
TTL null → Config::cache('lifetime') |
$this |
load($key, $context = [], $destroy = false) |
Context folded into the physical key | Stored value, or null on miss — not false |
exists($key, $context = [], $load = false) |
$load = true returns the value |
bool, or the value when $load is true |
delete($key, $context = []) |
Same key + context as save | $this |
clear() |
None | $this — see driver matrix for blast radius |
gc() |
None | $this |
folder() / name() |
None | string |
Cache::normalizeData($data) |
Static | Normalized value (arrays key-sorted recursively) |
Drivers: File, Redis, Memcached, Null
| Concern | File (default) | Redis | Memcached | Null |
|---|---|---|---|---|
load() miss |
null |
null |
null |
null |
clear() scope |
cache_*.php in the folder |
Buggy key tracking — may miss keys | Flushes the whole server | no-op |
gc() |
Expires files | no-op (TTL) | no-op | no-op |
| Extra config | — | cache.redis_* |
cache.memcached_* |
— |
deleteKeys() |
Not implemented | Not implemented | Not implemented | Not implemented |
Switch the driver in app/config.php before new DotApp():
use Dotsystems\App\Parts\CacheDriverFile;
use Dotsystems\App\Parts\CacheDriverRedis;
use Dotsystems\App\Parts\CacheDriverMemcached;
use Dotsystems\App\Parts\CacheDriverNull;
Config::cacheDriver('default', CacheDriverFile::driver());
Config::cache('lifetime', 36000);
Config::cache('driver', 'default');
Config::cacheDriver('redis', CacheDriverRedis::driver());
Config::cache('redis_host', '127.0.0.1');
Config::cache('redis_port', 6379);
Config::cacheDriver('memcached', CacheDriverMemcached::driver());
Config::cache('memcached_host', '127.0.0.1');
Config::cache('memcached_port', 11211);
Config::cacheDriver('null', CacheDriverNull::driver());
Then Config::cache('driver', 'redis') (or memcached / null) to select. Full config map: How app/config.php works in DotApp PHP Framework.
Custom driver callables
Config::cacheDriver($name, $driver) requires six callables: save, load, exists, delete, clear, gc.
Each receives the Cache instance as the last argument. Missing or non-callable → \Exception ("Incompatible cache driver!" or "All cache driver functions must be callable!").
Getter Config::cacheDriver($name) returns the driver array or throws if undefined.
use Dotsystems\App\Parts\Config;
Config::cacheDriver('memory', [
'save' => function ($key, $data, $lifetime, $context, $cache) { /* persist */ },
'load' => function ($key, $context, $destroy, $cache) { return null; },
'exists' => function ($key, $context, $load, $cache) { return false; },
'delete' => function ($key, $context, $cache) {},
'clear' => function ($cache) {},
'gc' => function ($cache) {},
]);
Config::cache('driver', 'memory');
Still no deleteKeys() unless you add it and teach ORM to call it. Shipped Entity code expects that method on the query-cache driver — none of the stock drivers have it.
Gotchas (read these)
-
Renderer::useCache(true)is broken. It callscachePageExists/cachePageSave, which exist only on the legacyCache_OLDclass. Do not enable it. Cache the list in Shop, thenRenderer::new()->renderView(). -
Config::db('cache', true)breaksEntity::save(). Entity requiresdeleteKeys()on the cache driver. No shipped driver implements it, so save throws. Leave query cache off unless you provide a custom driver that actually hasdeleteKeys(). -
Redis
clear()has buggy key tracking. Do not assume every Shop key disappeared. Preferdelete()of keys you know, or rebuild with TTL. -
Memcached
clear()flushes the whole server. Every other application on that Memcached instance loses its keys. Never callclear()there as a Shop “invalidate list” shortcut.
Also: on a database cache hit, $execution_data from execute() is an empty array. Always use ?? null. That is a DB gotcha that appears when someone turned query cache on — another reason to leave it off.
Complete module.init.php
<?php
namespace Dotsystems\App\Modules\Shop;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Router;
class Module extends \Dotsystems\App\Parts\Module
{
public function initialize($dotApp)
{
Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');
Config::module('Shop', 'itemsPerPage') ?? Config::module('Shop', 'itemsPerPage', 20);
$p = Config::module('Shop', 'prefix');
Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);
Router::post($p . '/items/refresh', 'Shop:Home@refresh!', Router::STATIC_ROUTE);
}
public function initializeRoutes()
{
return ['/shop', '/shop/*'];
}
public function initializeCondition($routeMatch)
{
return $routeMatch;
}
}
new Module($dotApp);
Complete Shop controller that caches a list
File: app/modules/Shop/Controllers/Home.php.
Trailing ! on the route skips DI — create Renderer::new() inside the method.
Query with all(), never unguarded first(). Pass both execute() callbacks if you write.
<?php
namespace Dotsystems\App\Modules\Shop\Controllers;
use Dotsystems\App\Parts\Cache;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\DB;
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;
class Home extends \Dotsystems\App\Parts\Controller
{
public static function index($request)
{
$limit = (int) (Config::module('Shop', 'itemsPerPage') ?? 20);
$rows = self::topItems($limit);
$html = Renderer::new()
->module('Shop')
->setView('home')
->setViewVar('title', 'Shop')
->setViewVar('items', $rows)
->setViewVar('prefix', Config::module('Shop', 'prefix'))
->renderView();
if ($html === '') {
Logger::use()->error('Shop home view produced empty output');
return new Response(500, 'Template error');
}
return $html;
}
public static function refresh($request)
{
if (!$request->crcCheck()) {
return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
}
$limit = (int) (Config::module('Shop', 'itemsPerPage') ?? 20);
Cache::use('Shop')->delete('items.top.' . $limit);
$rows = self::topItems($limit);
return Response::json(['status' => 1, 'count' => count($rows)]);
}
private static function topItems(int $limit): array
{
$cache = Cache::use('Shop');
$key = 'items.top.' . $limit;
$rows = $cache->load($key);
if ($rows !== null) {
return $rows;
}
$rows = DB::module('RAW')->q(function ($qb) use ($limit) {
$qb->select('*')
->from('shop_items')
->where('active', '=', 1)
->orderBy('sold', 'DESC')
->limit($limit);
})->all();
$cache->save($key, $rows, 600);
return $rows;
}
}
Complete view
File: app/modules/Shop/views/home.view.php. Directives use {{ var: $title }}, not {{ $title }}.
JSON refresh uses Router + Response::json + crcCheck() as in the controller — not a separate API dispatcher.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ var: $title }}</title>
</head>
<body>
<h1>{{ var: $title }}</h1>
<ul>
{{ foreach $items as $item }}
<li>{{ var: $item['title'] }}</li>
{{ /foreach }}
</ul>
<script src="/assets/dotapp/dotapp.js"></script>
</body>
</html>
Views: How to render views and layouts in DotApp PHP Framework.
FAQ
Why not if (!$rows) after load()?
A cached empty array is valid. !$rows is true for both miss (null) and []. Compare $rows !== null.
Must the cache name match the module?
Use Cache::use('Shop') so keys stay in a Shop namespace. A second name is a second singleton.
Can I cache the rendered HTML with Renderer?
Not with Renderer::useCache(true). That path is broken. Cache the list, then render.
Can I turn on Config::db('cache') to speed Entity::save?
No. It breaks save because deleteKeys() is missing on every shipped driver.
Is Cache::use('Shop')->clear() safe on Memcached?
No. It flushes the entire Memcached server, not just Shop.
What if I omit lifetime?
null uses Config::cache('lifetime'), default 36000 seconds.
Does context need to be in delete() too?
Yes. Physical keys include context. Delete with the same array you used in save / load.
Should I also set HTTP cache headers?
Only for public, identical responses. Personalized Shop pages stay on DSM + uncached HTML. Server-side Cache::use('Shop') is independent of the browser cache.