Prejsť na obsah

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 nullConfig::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)

  1. Renderer::useCache(true) is broken. It calls cachePageExists / cachePageSave, which exist only on the legacy Cache_OLD class. Do not enable it. Cache the list in Shop, then Renderer::new()->renderView().
  2. Config::db('cache', true) breaks Entity::save(). Entity requires deleteKeys() on the cache driver. No shipped driver implements it, so save throws. Leave query cache off unless you provide a custom driver that actually has deleteKeys().
  3. Redis clear() has buggy key tracking. Do not assume every Shop key disappeared. Prefer delete() of keys you know, or rebuild with TTL.
  4. Memcached clear() flushes the whole server. Every other application on that Memcached instance loses its keys. Never call clear() 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.

See also