Caching Reference
Jetonomy\Cache — includes/class-cache.php — is the single object-cache
wrapper every model routes through (group jetonomy, default TTL 300s). Do
not add per-service wp_cache_* calls or a second cache group; extend this
wrapper instead.
get() / set() / delete()
Section titled “get() / set() / delete()”Cache::get( string $key )Cache::set( string $key, $value, int $ttl = 0 ): void // ttl 0 = DEFAULT_TTL (300s)Cache::delete( string $key ): voidThin wrappers over wp_cache_get/set/delete() scoped to the jetonomy group.
delete_many()
Section titled “delete_many()”Cache::delete_many( string[] $keys ): voidBusts several keys in one call — for writers that must invalidate more than
one key whose value changed (e.g. a space row is served under both
space:{id} and space:slug:{slug}, so a single write invalidates both).
remember() / remember_object()
Section titled “remember() / remember_object()”Cache::remember( string $key, callable $callback, int $ttl = 0 )Cache::remember_object( string $key, callable $callback, int $ttl = 0 ): ?objectStandard cache-aside: return the cached value, or call $callback(), cache
it, and return it. Use remember_object() for any read with an ?object
return contract — some persistent object-cache backends (Redis/Memcached)
materialise a cached null as '' on the next read, which remember()
alone would hand back as a string and fatal a typed caller. remember_object()
coerces any non-object hit back to null.
flush()
Section titled “flush()”Cache::flush(): voidFlushes the whole jetonomy group. Not a per-write path — this is for
one-shot admin/CLI/import recomputes whose set-based writes can’t name the
rows they touched, and the admin “Clear cache” action. Guarded by
wp_cache_supports( 'flush_group' ); falls back to a full wp_cache_flush()
on a persistent drop-in without group support, and is a no-op with no
persistent cache (nothing to flush).
is_persistent()
Section titled “is_persistent()”Cache::is_persistent(): boolWraps wp_using_ext_object_cache().
Invalidation rule: bust after the write, in the write method
Section titled “Invalidation rule: bust after the write, in the write method”Every write method busts the exact keys whose value it changed, immediately after the database write completes — never before. Busting first is a re-prime race: a concurrent read between the bust and the write re-caches the stale value, and it stays stale for the full TTL.
A set-based UPDATE ... WHERE id IN (...) cannot invalidate what it doesn’t
name — bust each affected id explicitly, or call Cache::flush() for a
one-shot admin/CLI/import path that can’t enumerate the rows cheaply.
Member-visible state a user just changed (their own post count, a space they just made private) must never rely on TTL expiry alone — bust the exact key.
Reference pattern: Space::bust_cache()
Section titled “Reference pattern: Space::bust_cache()”includes/models/class-space.php:
/** * Bust every object-cache key that serves a space row: the id key and, * when given, the slug->id mapping key. Row data lives once, under * space:{id}; find_by_slug() caches only the stable slug->id mapping under * space:slug:{slug}. Callers bust AFTER the DB write. */public static function bust_cache( int $id, ?string $slug = null ): void { $keys = [ "space:{$id}" ]; if ( ! empty( $slug ) ) { $keys[] = "space:slug:{$slug}"; } Cache::delete_many( $keys );}Callers invoke it after every write that changes a space row: update()
(old + new slug on rename), the hard delete() override, and both counter
increments (id-only — the slug mapping doesn’t change on a count bump). This
is the model to follow for any new cached row: one bust_cache( $id, ...)
helper on the model, called from every writer, after the write, never a
listener on a do_action (a listener misses any caller that doesn’t fire the
action — REST, AJAX, CLI, import, and Abilities callers all mutate a space,
and only busting inside the model methods catches all of them).
public static function update( int $id, array $data ): bool { $old_slug = ( parent::find( $id )->slug ?? null ); $result = parent::update( $id, $data ); self::bust_cache( $id, $old_slug ); if ( ! empty( $data['slug'] ) && $data['slug'] !== $old_slug ) { Cache::delete( "space:slug:{$data['slug']}" ); // rename invalidates the new key too } return $result;}Rendering many avatars? Prime first
Section titled “Rendering many avatars? Prime first”Jetonomy answers WordPress’s standard avatar filters (pre_get_avatar_data)
from its own profile table. The filter runs once per avatar and cannot know
which user ids your page will ask for next — so a loop that renders N avatars
issues N profile queries unless the cache is warmed up front.
Before any loop that renders many users (leaderboards, member directories, large comment threads — yours or another plugin’s), prime once:
if ( class_exists( '\Jetonomy\Avatar' ) ) { \Jetonomy\Avatar::prime( wp_list_pluck( $rows, 'user_id' ) );}// Now render normally — get_avatar() / get_avatar_url() answer from cache.One WHERE user_id IN (...) query replaces N single-row lookups. Users with
no profile row are cached as absent, so they don’t re-query either. Jetonomy’s
own leaderboard, sidebar, and REST user lists already do this; the public
seam exists so pages Jetonomy has never heard of can too.

