group = $group ?: 'jvb_default'; $this->cache_ttl = $ttl ?: 3600; // Check if Redis/Memcached is available if (is_null(static::$use_object_cache)) { static::$use_object_cache = !is_null(wp_using_ext_object_cache()); // error_log((static::$use_object_cache) ? 'Using Object Cache' : 'Not using Object Cache'); } } /** * Get a value from the cache * @param string|array $key The key to look up (auto-generates key from array of key=>values) * @param string|null $group The group to get from. Defaults to current group * @return mixed */ public function get(string|array $key, ?string $group = null): mixed { $group = $group ?: $this->group; $key = $this->normalizeKey($key); $cache_key = $this->buildKey($key); // Use appropriate cache method if (static::$use_object_cache) { $value = wp_cache_get($cache_key, $group); } else { // Fallback to transients for local development $value = get_transient($this->getTransientKey($cache_key, $group)); } return (is_array($value) && array_key_exists('data', $value)) ? $value['data'] : $value; } public function getTimestamp(string|array $key, ?string $group = null): mixed { $group = $group ?: $this->group; $key = $this->normalizeKey($key); $cache_key = $this->buildKey($key); // Use appropriate cache method if (static::$use_object_cache) { $value = wp_cache_get($cache_key, $group); } else { // Fallback to transients for local development $value = get_transient($this->getTransientKey($cache_key, $group)); } return (is_array($value) && array_key_exists('last_modified', $value)) ? $value['last_modified'] : false; } /** * Store a value in cache * @param string|array $key The key to look up (auto-generates key from array of key=>values) * @param mixed $value The Value to set * @param int|null $ttl The ttl (defaults to current set ttl) * @param string|null $group The group to add cache to (defaults to current group)) * @return bool */ public function set(string|array $key, mixed $value, ?int $ttl = null, ?string $group = null): bool { $ttl = $ttl ?: $this->cache_ttl; $group = $group ?: $this->group; $key = $this->normalizeKey($key); $cache_key = $this->buildKey($key); $temp = [ 'data' => $value, 'last_modified' => time(), ]; $value = $temp; // Use appropriate cache method if (static::$use_object_cache) { return wp_cache_set($cache_key, $value, $group, $ttl); } else { // Fallback to transients return set_transient($this->getTransientKey($cache_key, $group), $value, $ttl); } } /** * Delete a cached value * @param string|array $key The key to look up (auto-generates key from array of key=>values) * @param string|null $group The group to delete from (defaults to current group) * @return bool */ public function delete(string|array $key, ?string $group = null): bool { $group = $group ?: $this->group; $key = $this->normalizeKey($key); $cache_key = $this->buildKey($key); // Use appropriate cache method if (static::$use_object_cache) { return wp_cache_delete($cache_key, $group); } else { return delete_transient($this->getTransientKey($cache_key, $group)); } } public function clear():bool { try { if (static::$use_object_cache) { // With Redis, this could be implemented with SCAN command // but wp_cache_* doesn't expose this, so we'd need direct Redis access // For now, just flush the group as a nuclear option if (function_exists('wp_cache_flush_group')) { wp_cache_flush_group($this->group); return true; } return false; } else { // For transients, search and delete global $wpdb; $prefix = self::getTransientPrefix($this->group); $sql = "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name LIKE %s"; $keys = $wpdb->get_col($wpdb->prepare( $sql, '_transient_' . $prefix . '%' )); foreach ($keys as $key) { $transient_key = str_replace('_transient_', '', $key); delete_transient($transient_key); } return true; } } catch (\Exception $e) { } finally { return false; } } /** * Alias for delete() for backwards compatibility * @param string $key The key to look up (auto-generates key from array of key=>values) * @param string|null $group The group to delete from (defaults to current group)) * @return void */ public function invalidate(string $key, ?string $group = null): void { $this->delete($key, $group); } /** * Clear all cache entries for a group * @param string $group The group to clear * @return bool */ public static function invalidateGroup(string $group): bool { $group = jvbNoBase($group); if (wp_using_ext_object_cache()) { // With Redis/Memcached, use native group flush if (function_exists('wp_cache_flush_group')) { return wp_cache_flush_group($group); } else { // Fallback for older WP versions - flush everything (not ideal) return wp_cache_flush(); } } else { // For transients, we need to delete them from database global $wpdb; $prefix = self::getTransientPrefix($group); // Delete transients and their timeouts $sql = "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s"; $result = $wpdb->query($wpdb->prepare( $sql, '_transient_' . $prefix . '%', '_transient_timeout_' . $prefix . '%' )); return $result !== false; } } /** * Clear cache entries by pattern (only works efficiently with Redis) * @param string $pattern * @return int */ public function clearPattern(string $pattern): int { $count = 0; if (static::$use_object_cache) { // With Redis, this could be implemented with SCAN command // but wp_cache_* doesn't expose this, so we'd need direct Redis access // For now, just flush the group as a nuclear option if (function_exists('wp_cache_flush_group')) { wp_cache_flush_group($this->group); return $count; } } else { // For transients, search and delete global $wpdb; $prefix = self::getTransientPrefix($this->group); $sql = "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name LIKE %s"; $keys = $wpdb->get_col($wpdb->prepare( $sql, '_transient_' . $prefix . '%', '%' . $pattern . '%' )); foreach ($keys as $key) { $transient_key = str_replace('_transient_', '', $key); delete_transient($transient_key); $count++; } } return $count; } /** * Helper to generateKey from array if applicable * @param string|array $key * @return string */ private function normalizeKey(string|array $key): string { return is_array($key) ? $this->generateKey($key) : $key; } /** * Generate a cache key from parameters * @param array $params An array of key/values that differentiates this cache item from others * @return string */ public function generateKey(array $params): string { // Sort params for consistent key generation ksort($params); return md5(serialize($params)); } /** * The workhorse shorthand of CacheManager. Tests the cache, and calls the callback if nothing is found. * @param string|array $key The key to look up (auto-generates key from array of key=>values) * @param callable $callback The callback to generate the value for this key * @param int|null $ttl The time-to-live for the cache. Defaults to constructor * @param string|null $group The group to save cache to. Defaults to constructor * @return mixed */ public function remember(string|array $key, callable $callback, ?int $ttl = null, ?string $group = null): mixed { $group = $group ?: $this->group; $ttl = $ttl ?: $this->cache_ttl; $key = $this->normalizeKey($key); $value = $this->get($key, $group); if ($value === false) { $value = $callback(); if ($value !== false) { $value = [ 'data' => $value, 'last_modified' => time(), ]; $this->set($key, $value, $ttl, $group); } } return (is_array($value) && array_key_exists('data', $value)) ? $value['data']: $value; } /** * Build the cache key * @param string $key * @return string */ private function buildKey(string $key): string { return $this->prefix . $key; } /** * Get transient key for fallback mode * @param string $key * @param string $group * @return string */ private function getTransientKey(string $key, string $group): string { // Transients have a 172 character limit $full_key = $group . '_' . $key; if (strlen($full_key) > 160) { // Use hash for long keys, but keep group prefix for clearPattern() return substr($group, 0, 20) . '_' . md5($full_key); } return $full_key; } /** * Get transient prefix for a group */ private static function getTransientPrefix(string $group): string { return $group . '_jvb_'; } /** * Check if using object cache */ public function isUsingObjectCache(): bool { return static::$use_object_cache; } /** * Cleanup expired transients (maintenance method for non-Redis environments) */ public static function cleanupExpiredTransients(): int { if (wp_using_ext_object_cache()) { return 0; // Not needed with Redis } global $wpdb; // Delete expired transients $sql = "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b WHERE a.option_name LIKE '_transient_%' AND a.option_name NOT LIKE '_transient_timeout_%' AND b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12)) AND b.option_value < %d"; return $wpdb->query($wpdb->prepare($sql, time())); } }