From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
inc/managers/Cache.php | 613 +++++++++++++++++++------------------------------------
1 files changed, 214 insertions(+), 399 deletions(-)
diff --git a/inc/managers/Cache.php b/inc/managers/Cache.php
index 2f62928..fc03cb8 100644
--- a/inc/managers/Cache.php
+++ b/inc/managers/Cache.php
@@ -9,23 +9,78 @@
{
private string $group;
private int $ttl;
- private static array $timestamps = [];
- private const TS_GROUP = 'jvb_http_ts';
-
- private const CONNECTIONS_OPTION = 'jvb_cache_connections';
- private static ?array $connections = null;
- protected array $tags = [];
private static array $instances = [];
private bool $hasRedis;
private bool $varyByAuth = false;
private bool $varyByUser = false;
- private function __construct(string $group, int $ttl)
+ private array $index=[];
+ private ?string $target = null;
+
+ private function __construct(string $group, ?string $target = null, int $ttl = WEEK_IN_SECONDS)
{
+ if (!is_null($target) && empty($this->index)) {
+ $this->target = null;
+ $this->index = get_option(BASE."cache_{$target}_index",[]);
+ }
$this->group = $group;
$this->ttl = $ttl;
- $this->hasRedis = (bool) wp_using_ext_object_cache();
+ $this->logGroup();
+ $this->hasRedis = (bool)wp_using_ext_object_cache();
+ }
+
+ protected function logGroup():void
+ {
+ $changed = false;
+ $groups = get_option(BASE."cache_groups", []);
+ if (!array_key_exists($this->group, $groups)) {
+ $groups[$this->group] = [
+ 'auth' => $this->varyByAuth,
+ 'user' => $this->varyByUser,
+ 'target'=> $this->target,
+ 'ttl' => $this->ttl,
+ ];
+ $changed = true;
+ }
+
+ if ($groups[$this->group]['auth'] && !$this->varyByAuth) {
+ error_log('[Cache]Group '.$this->group.' previously defined as varying by auth, but is not set. Setting it now.');
+ $this->auth();
+ }
+ if ($groups[$this->group]['user'] && !$this->varyByUser) {
+ error_log('[Cache]Group '.$this->group.' previously defined as varying by user, but is not set. Setting it now.');
+ $this->user();
+ }
+ if (is_null($this->target) && !is_null($groups[$this->group]['target'])) {
+ error_log('[Cache]Group '.$this->group. ' previously defined with target '.$groups[$this->group]['target'].'... Setting it now.');
+ $this->target = $groups[$this->group]['target'];
+ } elseif ($this->target !== $groups[$this->group]['target']) {
+ error_log('[Cache]WARNING Group previously defined with target '.$groups[$this->group]['target'].' - but is currently defined with '.$this->target. '. You may want to look into this.');
+ }
+
+ if ($this->ttl !== $groups[$this->group]['ttl']) {
+ error_log('[Cache]Group '. $this->group. ' previously defined with ttl '.$groups[$this->group]['ttl']. ' - but is currently defined with '. $this->ttl);
+ $groups[$this->group]['ttl'] = $this->ttl;
+ $changed = true;
+ }
+
+ if ($this->varyByAuth && !$groups[$this->group]['auth']) {
+ $groups[$this->group]['auth'] = true;
+ $changed = true;
+ }
+ if ($this->varyByUser && !$groups[$this->group]['user']) {
+ $groups[$this->group]['user'] = true;
+ $changed = true;
+ }
+ if ($this->target !== $groups[$this->group]['target']) {
+ $groups[$this->group]['target'] = $this->target;
+ $changed = true;
+ }
+
+ if ($changed) {
+ update_option(BASE."cache_groups", $groups, false);
+ }
}
public static function registerHooks(): void
@@ -45,9 +100,9 @@
add_action('delete_term', [self::class, 'onTermDelete'], 10, 3);
// Term meta updates
- add_action('updated_term_meta', [self::class, 'onTermMetaChange'], 10, 2);
- add_action('added_term_meta', [self::class, 'onTermMetaChange'], 10, 2);
- add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 2);
+// add_action('updated_term_meta', [self::class, 'onTermMetaChange'], 10, 2);
+// add_action('added_term_meta', [self::class, 'onTermMetaChange'], 10, 2);
+// add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 2);
// User updates
add_action('profile_update', [self::class, 'onUserChange'], 10, 2);
@@ -55,9 +110,9 @@
add_action('deleted_user', [self::class, 'onUserDelete']);
// User meta updates
- add_action('updated_user_meta', [self::class, 'onUserMetaChange'], 10, 2);
- add_action('added_user_meta', [self::class, 'onUserMetaChange'], 10, 2);
- add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 2);
+// add_action('updated_user_meta', [self::class, 'onUserMetaChange'], 10, 2);
+// add_action('added_user_meta', [self::class, 'onUserMetaChange'], 10, 2);
+// add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 2);
}
public function auth(): static
@@ -83,41 +138,48 @@
return (string) $key;
}
- /* ---------------------------------------------------------------------
- * Factory
- * ------------------------------------------------------------------- */
-
- public static function for(string $group, int $ttl = HOUR_IN_SECONDS): self
+ public static function for(string $group, ?string $target = null, int $ttl = HOUR_IN_SECONDS): self
{
$group = sanitize_key($group);
if (!isset(self::$instances[$group])) {
- self::$instances[$group] = new self($group, $ttl);
+ self::$instances[$group] = new self($group, $target, $ttl);
}
return self::$instances[$group];
}
+ /**
+ * To help facilitate cache flushing on object updates, we track the keys that are used for a particular id
+ * Generally, $this->target will either be 'post', 'user', 'term', or 'custom'
+ * @param int $ID
+ * @param string $key
+ * @return void
+ */
+ protected function trackID(int $ID, string $key):void
+ {
+ if (is_null($this->target)) {
+ return;
+ }
+ if (!array_key_exists($ID, $this->index[$this->target])) {
+ $this->index[$ID] = [];
+ }
+ if (!array_key_exists($this->group, $this->index[$ID])) {
+ $this->index[$ID][$this->group] = [];
+ }
+ if (!in_array($key, $this->index[$ID][$this->group])) {
+ $this->index[$this->target][$ID][$this->group][] = $key;
+ update_option(BASE."cache_{$this->target}_index", $this->index, false);
+ }
+ }
+
/* ---------------------------------------------------------------------
* Core operations
* ------------------------------------------------------------------- */
public function remember(int|string|array $key, callable $callback, ?int $ttl = null): mixed
{
- //TODO: Redis is fucking this all up for some reason. While we figure it out, let's just return the callback
- return $callback();
-
- if (is_array($key)) {
- $key = $this->generateKey($key);
- }
- if (!empty($this->tags)) {
- return $this->rememberTagged(
- $key,
- $this->tags,
- $callback,
- $ttl
- );
- }
+ $key = $this->generateKey($key);
$value = $this->get($key);
@@ -133,44 +195,55 @@
return $value;
}
-
- public function get(int|string|array $id): mixed
+ protected function transientKey(string $key):string
{
- if (is_array($id)) {
- $id = $this->generateKey($id);
- }
- if ($this->hasRedis) {
- $value = wp_cache_get($id, $this->group);
- } else {
- $value = get_transient("jvb_{$this->group}_{$id}");
- }
-
- return $value;
+ return BASE."{$this->group}_{$key}";
}
- public function set(int|string|array $id, mixed $value, ?int $ttl = null): void
+ public function get(int|string|array $key): mixed
{
- if (is_array($id)) {
- $id = $this->generateKey($id);
- }
- $ttl = $ttl ?? $this->ttl;
+ $key = $this->generateKey($key, false);
+
+ $value = $this->hasRedis ?
+ wp_cache_get($key, $this->group) :
+ get_transient($this->transientKey($key));
+
+ return $value['value']??false;
+ }
+ public function getTimestamp(int|string|array $key): mixed
+ {
+ $key = $this->generateKey($key, false);
+
+ $value = $this->hasRedis ?
+ wp_cache_get($key, $this->group) :
+ get_transient($this->transientKey($key));
+ return $value['modified']??false;
+ }
+
+ public function set(int|string|array $key, mixed $value, ?int $ttl = null): void
+ {
+ $key = $this->generateKey($key);
+ $ttl = $ttl??$this->ttl;
+ $store = [
+ 'value' => $value,
+ 'modified' => time()
+ ];
if ($this->hasRedis) {
- wp_cache_set($id, $value, $this->group, $ttl);
+ wp_cache_set($key, $store, $this->group, $ttl);
} else {
- set_transient("jvb_{$this->group}_{$id}", $value, $ttl);
+ set_transient($this->transientKey($key), $store, $ttl);
}
}
- public function forget(int|string|array $id): void
+ public function forget(int|string|array $key): void
{
- if (is_array($id)) {
- $id = $this->generateKey($id);
- }
+ $key = $this->generateKey($key);
if ($this->hasRedis) {
- wp_cache_delete($id, $this->group);
+ wp_cache_delete($key, $this->group);
} else {
- delete_transient("jvb_{$this->group}_{$id}");
+ delete_transient($this->transientKey($key));
}
+
}
public function flush(): void
@@ -186,266 +259,53 @@
}
}
- /* ---------------------------------------------------------------------
- * Invalidation
- * ------------------------------------------------------------------- */
-
- public static function invalidateItem(string $group, int|string|array $id): void
+ public static function clearPost(int $ID):void
{
- if (is_array($id)) {
- $id = self::for($group)->generateKey($id);
+ $index = get_option(BASE."cache_post_index", []);
+ if (!array_key_exists($ID, $index)) {
+ return;
}
- $group = sanitize_key($group);
-
- if (wp_using_ext_object_cache()) {
- wp_cache_delete($id, $group);
- } else {
- delete_transient("jvb_{$group}_{$id}");
- }
- self::touch($group);
-
- foreach (self::connections()[$group] ?? [] as $conn) {
- $target = $conn['target'] ?? $conn; // Backwards compat if still string
- $flush = $conn['flush'] ?? false;
-
- if ($flush) {
- // Flush entire target group
- self::invalidateGroup($target);
- } else {
- // Just delete this item ID
- if (wp_using_ext_object_cache()) {
- wp_cache_delete($id, $target);
- } else {
- delete_transient("jvb_{$target}_{$id}");
- }
- self::touch($target);
+ foreach ($index[$ID] as $group => $keys) {
+ $cache = self::for($group, 'post');
+ foreach ($keys as $key) {
+ $cache->forget($key);
}
}
- self::invalidateByTag($group, $id);
}
+ public static function clearUser(int $ID):void
+ {
+ $index = get_option(BASE."cache_user_index", []);
+ if (!array_key_exists($ID, $index)) {
+ return;
+ }
+ foreach ($index[$ID] as $group => $keys) {
+ $cache = self::for($group, 'user');
+ foreach ($keys as $key) {
+ $cache->forget($key);
+ }
+ }
+ }
+ public static function clearTerm(int $ID):void
+ {
+ $index = get_option(BASE."cache_term_index", []);
+ if (!array_key_exists($ID, $index)) {
+ return;
+ }
+ foreach ($index[$ID] as $group => $keys) {
+ $cache = self::for($group, 'term');
+ foreach ($keys as $key) {
+ $cache->forget($key);
+ }
+ }
+ }
+
public static function invalidateGroup(string $group): void
{
- $group = sanitize_key($group);
-
- if (wp_using_ext_object_cache()) {
- wp_cache_flush_group($group);
- } else {
- $instance = self::for($group);
- $instance->clearGroupTransients();
- }
-
- self::touch($group);
-
- foreach (self::connections()[$group] ?? [] as $conn) {
- $target = $conn['target'] ?? $conn; // Backwards compat
-
- // When flushing entire source group, always flush connected targets
- // (regardless of flush flag - we don't know which items to delete)
- if (wp_using_ext_object_cache()) {
- wp_cache_flush_group($target);
- } else {
- $instance = self::for($target);
- $instance->clearGroupTransients();
- }
- self::touch($target);
- }
- }
-
- public static function touch(string $group): int
- {
- $group = sanitize_key($group);
- $time = time();
-
- if (wp_using_ext_object_cache()) {
- wp_cache_set($group, $time, self::TS_GROUP, WEEK_IN_SECONDS);
- } else {
- set_transient('jvb_ts_' . $group, $time, WEEK_IN_SECONDS);
- }
-
- self::$timestamps[$group] = $time;
- return $time;
- }
-
- public static function lastModified(string|array $groups): int
- {
- if (is_array($groups)) {
- return max(array_map([self::class, 'lastModified'], $groups));
- }
-
- $group = sanitize_key($groups);
-
- if (isset(self::$timestamps[$group])) {
- return self::$timestamps[$group];
- }
-
- if (wp_using_ext_object_cache()) {
- $ts = (int) wp_cache_get($group, self::TS_GROUP);
- } else {
- $ts = (int) get_transient('jvb_ts_' . $group);
- }
-
- if (!$ts) {
- $ts = time();
- if (wp_using_ext_object_cache()) {
- wp_cache_set($group, $ts, self::TS_GROUP, WEEK_IN_SECONDS);
- } else {
- set_transient('jvb_ts_' . $group, $ts, WEEK_IN_SECONDS);
- }
- }
-
- return self::$timestamps[$group] = $ts;
- }
-
- public function getLastModifiedForTags(array $tags): ?int
- {
- if (!$this->hasRedis) {
- return null;
- }
- $redis = self::redis();
- if (!$redis) {
- return null;
- }
-
- $lastModified = 0;
-
- foreach ($tags as $tag) {
- $ts = $redis->get("jvb:tag:{$tag}:lastModified");
- if ($ts) {
- $lastModified = max($lastModified, (int) $ts);
- }
- }
-
- return $lastModified ?: null;
- }
-
- /****************************************************
- * CONNECTIONS
- ****************************************************/
- private static function connections(): array
- {
- if (self::$connections === null) {
- self::$connections = get_option(self::CONNECTIONS_OPTION, []);
- }
- return self::$connections;
+ self::for($group)->flush();
}
- public function connect(string $source, bool $flush = false): self
- {
- $source = sanitize_key($source);
- $target = $this->group;
-
- $all = self::connections();
- $all[$source] ??= [];
-
- $before = count($all[$source]);
-
- // Add the connection
- $all[$source][] = ['target' => $target, 'flush' => $flush];
-
- // Remove duplicates by serializing for comparison
- $all[$source] = array_values(array_unique($all[$source], SORT_REGULAR));
-
- // Only update if something actually changed
- if (count($all[$source]) !== $before) {
- update_option(self::CONNECTIONS_OPTION, $all, false);
- self::$connections = $all;
- }
-
- return $this;
- }
- /****************************************************
- * REDIS
- ****************************************************/
- private static function redis(): ?\Redis
- {
- global $wp_object_cache;
-
- return $wp_object_cache instanceof \WP_Object_Cache
- && isset($wp_object_cache->redis)
- ? $wp_object_cache->redis
- : null;
- }
-
- /**
- * Remember with tags for complex invalidation scenarios
- *
- * Example: Cache user favorites tagged by each post ID
- * When any post updates, this cache entry auto-invalidates
- *
- * @param int|string|array $key Cache key
- * @param array $tags Array of [group, id] pairs: [['post', 123], ['user', 456]]
- * @param callable $callback Function to generate value if cache miss
- * @return mixed Cached or generated value
- */
- public function rememberTagged(
- int|string|array $key,
- array $tags,
- callable $callback,
- ?int $ttl = null
- ): mixed {
- if (is_array($key)) {
- $key = $this->generateKey($key);
- }
- $instanceTags = array_map(fn($tag) => [$this->group, $tag], $this->getTags());
- $tags = array_unique(array_merge(
- $instanceTags,
- array_map(fn($tag) => [sanitize_key($tag[0]), $tag[1]], $tags)
- ), SORT_REGULAR);
-
-
- $value = wp_cache_get($key, $this->group);
- if ($value !== false) {
- return $value;
- }
-
- $value = $callback();
- if ($value === null || $value === false) {
- return $value;
- }
-
- wp_cache_set($key, $value, $this->group, $this->ttl);
-
- if ($redis = self::redis()) {
- foreach ($tags as [$tagGroup, $tagId]) {
- $redis->sAdd("tag:$tagGroup:$tagId", "{$this->group}:$key");
- }
- }
-
- return $value;
- }
-
- private static function invalidateByTag(string $group, int|string|array $id): void
- {
- if (is_array($id)) {
- $id = self::for($group)->generateKey($id);
- }
- if (!$redis = self::redis()) {
- return;
- }
-
- $key = "tag:$group:$id";
- $targets = $redis->sMembers($key);
-
- foreach ($targets as $target) {
- [$group, $id] = explode(':', $target, 2);
- wp_cache_delete($id, $group);
- }
-
- $redis->del($key);
- }
-
- public function tag(string $tag): static
- {
- $this->tags[] = sanitize_key($tag);
- return $this;
- }
-
- public function getTags(): array
- {
- return array_unique($this->tags);
- }
/****************************************************
* TRANSIENT HELPER
****************************************************/
@@ -453,8 +313,8 @@
{
global $wpdb;
- $pattern = '_transient_jvb_' . $this->group . '_%';
- $timeout_pattern = '_transient_timeout_jvb_' . $this->group . '_%';
+ $pattern = '_transient_' .BASE. $this->group . '%';
+ $timeout_pattern = '_transient_timeout_' . BASE . $this->group . '%';
// Remove LIMIT to avoid table locks, add retry for deadlocks
$attempts = 0;
@@ -494,13 +354,13 @@
}
// error_log('[Clearing cache for post change: '.$postId.']');
- self::invalidateItem('post', $postId);
+ self::clearPost($postId);
}
public static function onPostDelete(int $postId): void
{
// error_log('[Clearing cache for post delete: '.$postId.']');
- self::invalidateItem('post', $postId);
+ self::clearPost($postId);
}
// public static function onPostMetaChange(int $metaId, int $objectId): void
@@ -521,25 +381,25 @@
public static function onTermChange(int $termId, int $ttId, string $taxonomy): void
{
error_log('[Clearing cache for term change: '.$termId.']');
- self::invalidateItem('taxonomy', $termId);
+ self::clearTerm($termId);
}
public static function onTermDelete(int $termId): void
{
// error_log('[Clearing cache for term delete: '.$termId.']');
- self::invalidateItem('taxonomy', $termId);
+ self::clearTerm($termId);
}
public static function onTermMetaChange(int $metaId, int $objectId): void
{
// error_log('[Clearing cache for term meta change: '.$objectId.']');
- self::invalidateItem('taxonomy', $objectId);
+ self::clearTerm($objectId);
}
public static function onTermMetaDelete(array $metaIds, int $objectId): void
{
// error_log('[Clearing cache for term meta delete: '.$objectId.']');
- self::invalidateItem('taxonomy', $objectId);
+ self::clearTerm($objectId);
}
/****************************************************
@@ -548,25 +408,25 @@
public static function onUserChange(int $userId): void
{
// error_log('[Clearing cache for user change: '.$userId.']');
- self::invalidateItem('user', $userId);
+ self::clearUser($userId);
}
public static function onUserDelete(int $userId): void
{
// error_log('[Clearing cache for user delete: '.$userId.']');
- self::invalidateItem('user', $userId);
+ self::clearUser($userId);
}
public static function onUserMetaChange(int $metaId, int $objectId): void
{
// error_log('[Clearing cache for user meta change: '.$objectId.']');
- self::invalidateItem('user', $objectId);
+ self::clearUser($objectId);
}
public static function onUserMetaDelete(array $metaIds, int $objectId): void
{
// error_log('[Clearing cache for user meta delete: '.$objectId.']');
- self::invalidateItem('user', $objectId);
+ self::clearUser($objectId);
}
/***************************************************
* UTILITY
@@ -575,13 +435,33 @@
* Generate a cache key from parameters
* Useful for caching based on multiple variables
*
- * @param array $params Key-value pairs that uniquely identify this cache entry
+ * @param int|string|array $key Key-value pairs that uniquely identify this cache entry
* @return string MD5 hash of sorted parameters
*/
- public function generateKey(array $params): string
+ public function generateKey(int|string|array $key, bool $trackIt = true): string
{
- ksort($params);
- return md5(serialize($params));
+ $trackID = false;
+ $ID = false;
+
+ if (!is_null($this->target) && is_int($key)) {
+ //Likely an item id. Store it for reference
+ $trackID = true;
+ $ID = $key;
+ }
+ if (is_array($key)) {
+ ksort($key);
+ if (!is_null($this->target) && array_key_exists('id', $key)) {
+ $trackID = true;
+ $ID = $key['id'];
+ }
+ $key = md5(serialize($key));
+ }
+
+ $key = $this->resolveKey($key);
+ if ($trackIt && !is_null($this->target) && $ID && $trackID) {
+ $this->trackID($ID, $key);
+ }
+ return $key;
}
/**
@@ -592,90 +472,25 @@
*/
public static function flushAll(): int
{
- $all = self::connections();
- $groups = [];
-
- // Collect all unique groups from connections
- foreach ($all as $source => $targets) {
- $groups[$source] = true;
- foreach ($targets as $conn) {
- $target = $conn['target'] ?? $conn;
- $groups[$target] = true;
- }
- }
-
- // Add any instantiated groups not in connections
- foreach (array_keys(self::$instances) as $group) {
- $groups[$group] = true;
- }
-
- // Flush each group
+ $groups = get_option(BASE."cache_groups", []);
$count = 0;
- foreach (array_keys($groups) as $group) {
- self::invalidateGroup($group);
- $count++;
+ foreach ($groups as $group => $conf) {
+ $cache = Cache::for($group, $conf['target']??null, $conf['ttl']);
+ $cache->flush();
+ $count ++ ;
+
+ usleep(50000); // Wait 50ms before retry
}
-
- // Also flush timestamp cache
- if (wp_using_ext_object_cache()) {
- wp_cache_flush_group(self::TS_GROUP);
- }
-
- // Clear in-memory caches
- self::$timestamps = [];
-
return $count;
}
/**
- * Get all cache groups and their connections for admin display
+ * Get all cache groups
*
- * @return array Format: ['group' => ['connects_to' => [...], 'connected_from' => [...]]]
+ * @return array
*/
public static function getAllGroups(): array
{
- $connections = self::connections();
- $groups = [];
-
- // Build bidirectional view
- foreach ($connections as $source => $targets) {
- if (!isset($groups[$source])) {
- $groups[$source] = ['connects_to' => [], 'connected_from' => []];
- }
-
- foreach ($targets as $conn) {
- $target = $conn['target'] ?? $conn;
- $flush = $conn['flush'] ?? false;
-
- // Source connects to target
- $groups[$source]['connects_to'][] = [
- 'group' => $target,
- 'flush' => $flush
- ];
-
- // Target is connected from source
- if (!isset($groups[$target])) {
- $groups[$target] = ['connects_to' => [], 'connected_from' => []];
- }
- $groups[$target]['connected_from'][] = [
- 'group' => $source,
- 'flush' => $flush
- ];
- }
- }
-
- // Add any instantiated groups not in connections
- foreach (array_keys(self::$instances) as $group) {
- if (!isset($groups[$group])) {
- $groups[$group] = ['connects_to' => [], 'connected_from' => []];
- }
- }
-
- return $groups;
- }
-
- public function hasRedis():bool
- {
- return $this->hasRedis;
+ return get_option(BASE."cache_groups", []);
}
}
--
Gitblit v1.10.0