| | |
| | | { |
| | | 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 |
| | |
| | | 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); |
| | |
| | | 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); |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * Factory |
| | | * ------------------------------------------------------------------- */ |
| | | public function auth(): static |
| | | { |
| | | $this->varyByAuth = true; |
| | | return $this; |
| | | } |
| | | |
| | | public static function for(string $group, int $ttl = HOUR_IN_SECONDS): self |
| | | public function user(): static |
| | | { |
| | | $this->varyByUser = true; |
| | | return $this; |
| | | } |
| | | |
| | | private function resolveKey(int|string $key): string |
| | | { |
| | | if ($this->varyByUser) { |
| | | return $key . ':u' . get_current_user_id(); |
| | | } |
| | | if ($this->varyByAuth) { |
| | | return $key . ':' . (is_user_logged_in() ? 'auth' : 'guest'); |
| | | } |
| | | return (string) $key; |
| | | } |
| | | |
| | | 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 |
| | | { |
| | | 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); |
| | | |
| | |
| | | $value = $callback(); |
| | | |
| | | if ($value !== null && $value !== false) { |
| | | $this->set($key, $value); |
| | | $this->set($key, $value, $ttl); |
| | | } |
| | | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | /* --------------------------------------------------------------------- |
| | | * 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)) { |
| | | $id = $this->generateKey($key); |
| | | } |
| | | $tags = array_unique(array_merge( |
| | | $this->getTags(), |
| | | array_map('sanitize_key', $tags) |
| | | )); |
| | | |
| | | |
| | | $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 |
| | | ****************************************************/ |
| | |
| | | { |
| | | 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; |
| | |
| | | } |
| | | |
| | | // 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 |
| | |
| | | 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); |
| | | } |
| | | |
| | | /**************************************************** |
| | |
| | | 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 |
| | |
| | | * 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | 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", []); |
| | | } |
| | | } |