Caching
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.
API
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 ): void
Thin wrappers over wp_cache_get/set/delete() scoped to the jetonomy group.
delete_many()
Cache::delete_many( string[] $keys ): void
Busts 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()
Cache::remember( string $key, callable $callback, int $ttl = 0 )
Cache::remember_object( string $key, callable $callback, int $ttl = 0 ): ?object
Standard 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()
Cache::flush(): void
Flushes 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()
Cache::is_persistent(): bool
Wraps wp_using_ext_object_cache().
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()
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
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.