| | |
| | | namespace JVBase\managers; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | exit; |
| | | } |
| | | |
| | | class CacheManagerOld |
| | | /** |
| | | * Manages HTTP cache timestamps and relationship-based invalidation |
| | | * |
| | | * Data caching: Use wrapper methods or wp_cache_get/set directly |
| | | * HTTP caching: This class manages timestamps for ETag/Last-Modified headers |
| | | */ |
| | | class CacheManager |
| | | { |
| | | private string $prefix = 'jvb_'; |
| | | private const CONNECTIONS_OPTION = BASE.'cache_connections'; |
| | | private static ?array $connections_cache = null; // Cache in memory |
| | | private string $prefix = BASE; |
| | | private string $group; |
| | | private int $cache_ttl; |
| | | private static ?bool $use_object_cache = null; |
| | | private static array $instances = []; // Cache instances per type |
| | | private static array $http_timestamps = []; // Request-level memory cache |
| | | private static ?CacheManager $singleton = null; |
| | | |
| | | /** |
| | | * @param string|null $group The group name for this cache instance |
| | | * @param int|null $ttl The default ttl for this instance |
| | | * Private constructor - use for() factory method instead |
| | | */ |
| | | public function __construct(?string $group = null, ?int $ttl = null) |
| | | private function __construct(string $group, ?int $ttl = null) |
| | | { |
| | | $this->group = $group ?: 'jvb_default'; |
| | | $this->group = jvbNoBase($group); |
| | | $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'); |
| | | static::$use_object_cache = wp_using_ext_object_cache(); |
| | | } |
| | | |
| | | add_action('init', [$this, 'registerHooks']); |
| | | } |
| | | |
| | | /** |
| | | * Get singleton instance (for general cache operations) |
| | | * For type-specific operations, use for() or forUser() instead |
| | | */ |
| | | public static function getInstance(): self |
| | | { |
| | | if (self::$singleton === null) { |
| | | self::$singleton = new self('global', HOUR_IN_SECONDS); |
| | | } |
| | | return self::$singleton; |
| | | } |
| | | |
| | | /** |
| | | * Get all cache connections (public accessor) |
| | | * |
| | | * @return array Array of cache group connections |
| | | */ |
| | | public static function getAllConnections(): array |
| | | { |
| | | return self::getConnections(); |
| | | } |
| | | |
| | | /** |
| | | * Get all registered cache groups |
| | | * |
| | | * @return array List of cache group names |
| | | */ |
| | | public static function getAllGroups(): array |
| | | { |
| | | $connections = self::getConnections(); |
| | | return array_keys($connections); |
| | | } |
| | | /** |
| | | * Register WordPress hooks for automatic cache invalidation |
| | | * Call this once during plugin initialization |
| | | */ |
| | | public static function registerHooks(): void |
| | | { |
| | | // Post updates (all post types including core) |
| | | add_action('save_post', [self::class, 'onPostSave'], 10, 2); |
| | | add_action('delete_post', [self::class, 'onPostDelete']); |
| | | // Meta updates (will catch MetaManager updates) |
| | | add_action('updated_post_meta', [self::class, 'onPostMetaUpdate'], 10, 4); |
| | | add_action('added_post_meta', [self::class, 'onPostMetaUpdate'], 10, 4); |
| | | add_action('deleted_post_meta', [self::class, 'onPostMetaDelete'], 10, 4); |
| | | // transition_post_status? |
| | | |
| | | // Term updates (all taxonomies) |
| | | add_action('edited_term', [self::class, 'onTermSave'], 10, 3); |
| | | add_action('create_term', [self::class, 'onTermSave'], 10, 3); |
| | | add_action('delete_term', [self::class, 'onTermDelete'], 10, 3); |
| | | |
| | | // Term meta updates |
| | | add_action('updated_term_meta', [self::class, 'onTermMetaUpdate'], 10, 4); |
| | | add_action('added_term_meta', [self::class, 'onTermMetaUpdate'], 10, 4); |
| | | add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 4); |
| | | |
| | | // User updates |
| | | add_action('profile_update', [self::class, 'onUserUpdate'], 10, 2); |
| | | add_action('user_register', [self::class, 'onUserUpdate'], 10, 1); |
| | | add_action('deleted_user', [self::class, 'onUserDelete']); |
| | | |
| | | // User meta updates |
| | | add_action('updated_user_meta', [self::class, 'onUserMetaUpdate'], 10, 4); |
| | | add_action('added_user_meta', [self::class, 'onUserMetaUpdate'], 10, 4); |
| | | add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 4); |
| | | } |
| | | |
| | | /** |
| | | * Get or create a cache manager instance for a content type |
| | | * |
| | | * @param string $type Content type (tattoo, style, etc.) |
| | | * @param int|null $ttl Optional TTL override |
| | | * @return self Fluent interface |
| | | */ |
| | | public static function for(string $type, ?int $ttl = null): self |
| | | { |
| | | $type = jvbNoBase($type); |
| | | $key = $type . ($ttl ? "_{$ttl}" : ''); |
| | | |
| | | if (!isset(self::$instances[$key])) { |
| | | self::$instances[$key] = new self($type, $ttl); |
| | | } |
| | | |
| | | return self::$instances[$key]; |
| | | } |
| | | |
| | | /** |
| | | * Get cache manager for a specific user |
| | | * Each user gets their own cache group for complete isolation |
| | | * |
| | | * @param int $user_id User ID |
| | | * @param int|null $ttl Optional TTL |
| | | * @return self |
| | | */ |
| | | public static function forUser(int $user_id, ?int $ttl = null): self |
| | | { |
| | | return self::for("user_{$user_id}", $ttl); |
| | | } |
| | | |
| | | /** |
| | | * Get HTTP cache timestamp for content type(s) |
| | | * Used for ETag and Last-Modified header generation |
| | | * |
| | | * @param string|array $types Single type or array of types |
| | | * @return int Latest timestamp (Unix time) |
| | | */ |
| | | public static function getTimestamp(string|array $types): int |
| | | { |
| | | // Multiple types - return latest |
| | | if (is_array($types)) { |
| | | $latest = 0; |
| | | foreach ($types as $type) { |
| | | $timestamp = self::getTimestamp($type); |
| | | if ($timestamp > $latest) { |
| | | $latest = $timestamp; |
| | | } |
| | | } |
| | | return $latest ?: time(); |
| | | } |
| | | |
| | | $type = jvbNoBase($types); |
| | | |
| | | // Check request-level cache |
| | | if (isset(self::$http_timestamps[$type])) { |
| | | return self::$http_timestamps[$type]; |
| | | } |
| | | |
| | | // Load from cache (Redis or transient - wp_cache handles it) |
| | | $timestamp = (int)wp_cache_get("http_ts_{$type}", 'jvb_timestamps') ?: time(); |
| | | |
| | | // Cache in memory for this request |
| | | self::$http_timestamps[$type] = $timestamp; |
| | | |
| | | return $timestamp; |
| | | } |
| | | |
| | | /** |
| | | * Update HTTP cache timestamp (marks content as modified) |
| | | * |
| | | * @param string $type Content type |
| | | * @return int The new timestamp |
| | | */ |
| | | public static function updateTimestamp(string $type): int |
| | | { |
| | | $type = jvbNoBase($type); |
| | | $timestamp = time(); |
| | | |
| | | // Store (Redis or transient - wp_cache handles it) |
| | | wp_cache_set("http_ts_{$type}", $timestamp, 'jvb_timestamps', WEEK_IN_SECONDS); |
| | | |
| | | // Update request cache |
| | | self::$http_timestamps[$type] = $timestamp; |
| | | |
| | | do_action('jvb_http_timestamp_updated', $type, $timestamp); |
| | | |
| | | return $timestamp; |
| | | } |
| | | |
| | | /** |
| | | * Invalidate cache for a content type |
| | | * |
| | | * @param string $type Content type to invalidate |
| | | * @param string|array|null $specific_keys Optional specific key(s) to delete without flushing group |
| | | * @param bool $flush_connections Whether to flush connected caches |
| | | * @return void |
| | | */ |
| | | public static function invalidateAll(string $type, $specific_keys = null, bool $flush_connections = true): void |
| | | { |
| | | $type = jvbNoBase($type); |
| | | |
| | | // Update HTTP timestamp |
| | | self::updateTimestamp($type); |
| | | |
| | | // If specific keys provided, only delete those |
| | | if ($specific_keys !== null) { |
| | | $instance = self::for($type); |
| | | if (is_array($specific_keys)) { |
| | | foreach ($specific_keys as $key) { |
| | | $instance->delete($key); |
| | | } |
| | | } else { |
| | | $instance->delete($specific_keys); |
| | | } |
| | | } else { |
| | | // Flush the entire group |
| | | if (function_exists('wp_cache_flush_group')) { |
| | | wp_cache_flush_group($type); |
| | | } else { |
| | | wp_cache_flush(); |
| | | } |
| | | } |
| | | |
| | | // Flush connected caches |
| | | if ($flush_connections) { |
| | | self::for($type)->connections(); |
| | | } |
| | | |
| | | do_action('jvb_cache_invalidated', $type); |
| | | } |
| | | |
| | | /** |
| | | * Invalidate only specific keys for a type (doesn't flush group or update timestamp) |
| | | * Use this when you want surgical cache invalidation |
| | | * |
| | | * @param string $type Content type |
| | | * @param string|array $keys Key(s) to delete |
| | | * @return void |
| | | */ |
| | | public static function invalidateKeys(string $type, string|array $keys): void |
| | | { |
| | | $instance = self::for($type); |
| | | |
| | | if (is_array($keys)) { |
| | | foreach ($keys as $key) { |
| | | $instance->delete($key); |
| | | } |
| | | } else { |
| | | $instance->delete($keys); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Fluent instance method to invalidate this cache type |
| | | * |
| | | * @param string|array|null $specific_keys Optional specific key(s) |
| | | * @param bool $flush_connections Whether to flush connected caches |
| | | * @return self For chaining |
| | | */ |
| | | public function invalidate($specific_keys = null, bool $flush_connections = true): self |
| | | { |
| | | self::invalidateAll($this->group, $specific_keys, $flush_connections); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Get the HTTP timestamp for this instance's type |
| | | * |
| | | * @return int |
| | | */ |
| | | public function timestamp(): int |
| | | { |
| | | return self::getTimestamp($this->group); |
| | | } |
| | | |
| | | /** |
| | | * Update the HTTP timestamp for this instance's type |
| | | * |
| | | * @return self For chaining |
| | | */ |
| | | public function touch(): self |
| | | { |
| | | self::updateTimestamp($this->group); |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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)); |
| | | $value = wp_cache_get($cache_key, $group); |
| | | |
| | | // Fallback to transient if no external object cache |
| | | if ($value === false && !wp_using_ext_object_cache()) { |
| | | $value = get_transient($group . '_' . $cache_key); |
| | | } |
| | | |
| | | 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; |
| | | return $value; |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | $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); |
| | | self::updateTimestamp($this->group); |
| | | |
| | | // Try object cache first |
| | | $result = wp_cache_set($cache_key, $value, $group, $ttl); |
| | | |
| | | // If no external object cache, also store in transient for persistence |
| | | if (!wp_using_ext_object_cache()) { |
| | | set_transient($group . '_' . $cache_key, $value, $ttl); |
| | | } |
| | | } |
| | | |
| | | return $result; |
| | | } |
| | | /** |
| | | * Delete a cached value |
| | | * @param string|array $key The key to look up (auto-generates key from array of key=>values) |
| | |
| | | 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)); |
| | | $result = wp_cache_delete($cache_key, $group); |
| | | |
| | | // Also delete transient if no external object cache |
| | | if (!wp_using_ext_object_cache()) { |
| | | delete_transient($group . '_' . $cache_key); |
| | | } |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | public function clear():bool |
| | | |
| | | /** |
| | | * Clear all cache for this group |
| | | * @return bool |
| | | */ |
| | | 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; |
| | | if (function_exists('wp_cache_flush_group')) { |
| | | wp_cache_flush_group($this->group); |
| | | } |
| | | } catch (\Exception $e) { |
| | | |
| | | } finally { |
| | | // Clear transients for this group if no external object cache |
| | | if (!wp_using_ext_object_cache()) { |
| | | $this->clearGroupTransients(); |
| | | } |
| | | |
| | | self::updateTimestamp($this->group); |
| | | return true; |
| | | } catch (\Exception $e) { |
| | | 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 |
| | | * Clear all transients for this cache group |
| | | */ |
| | | public function invalidate(string $key, ?string $group = null): void |
| | | private function clearGroupTransients(): void |
| | | { |
| | | $this->delete($key, $group); |
| | | } |
| | | global $wpdb; |
| | | |
| | | /** |
| | | * 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); |
| | | $pattern = '_transient_' . $this->group . '_' . $this->prefix . '%'; |
| | | $timeout_pattern = '_transient_timeout_' . $this->group . '_' . $this->prefix . '%'; |
| | | |
| | | 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; |
| | | $wpdb->query( |
| | | $wpdb->prepare( |
| | | "DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s", |
| | | $pattern, |
| | | $timeout_pattern |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | $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(), |
| | | ]; |
| | | if ($value !== false && $value !== null) { |
| | | $this->set($key, $value, $ttl, $group); |
| | | } |
| | | } |
| | | |
| | | return (is_array($value) && array_key_exists('data', $value)) ? $value['data']: $value; |
| | | return $value; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * Get transient key for fallback mode |
| | | * @param string $key |
| | | * @param string $group |
| | | * @return string |
| | | * Get instance group name (for debugging) |
| | | */ |
| | | private function getTransientKey(string $key, string $group): string |
| | | public function getGroup(): string |
| | | { |
| | | // Transients have a 172 character limit |
| | | $full_key = $group . '_' . $key; |
| | | return $this->group; |
| | | } |
| | | |
| | | if (strlen($full_key) > 160) { |
| | | // Use hash for long keys, but keep group prefix for clearPattern() |
| | | return substr($group, 0, 20) . '_' . md5($full_key); |
| | | |
| | | /*************************************************************************** |
| | | * CONNECTIONS |
| | | * Connect to other caches by instantiating and defining connection |
| | | * Ex: CacheManager::for('usernames')->connectTo($type, $scope = 'all', $keyPattern) |
| | | * Where: $type = content / taxonomy / user |
| | | * $scope = either 'id' for specific item, or the entire group (registered post type, taxonomy, or user role) |
| | | * $keyPattern = ?? |
| | | ***************************************************************************/ |
| | | /** |
| | | * Define a connection between cache groups |
| | | * Connected caches will have their ID-based keys deleted when this cache invalidates |
| | | * |
| | | * @param string $type Grand overview ('post', 'taxonomy', 'user') |
| | | * @param string $scope Type-specific constant, user role, or 'id' |
| | | * @return self For chaining |
| | | */ |
| | | public function connectTo(string $type, string $scope = 'id'): self |
| | | { |
| | | //TODO: Handle connect to where $type === 'all' |
| | | $connections = self::getConnections(); |
| | | |
| | | if (!isset($connections[$this->group])) { |
| | | $connections[$this->group] = []; |
| | | } |
| | | |
| | | return $full_key; |
| | | } |
| | | $new_connection = [ |
| | | 'parent' => $type, |
| | | 'scope' => $scope |
| | | ]; |
| | | |
| | | /** |
| | | * 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 |
| | | // Check if already exists |
| | | foreach ($connections[$this->group] as $existing) { |
| | | if ($existing === $new_connection) { |
| | | return $this; |
| | | } |
| | | } |
| | | |
| | | global $wpdb; |
| | | $connections[$this->group][] = $new_connection; |
| | | update_option(self::CONNECTIONS_OPTION, $connections, false); |
| | | self::$connections_cache = $connections; |
| | | |
| | | // 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 $this; |
| | | } |
| | | |
| | | return $wpdb->query($wpdb->prepare($sql, time())); |
| | | /** |
| | | * Get all registered connections (cached for performance) |
| | | * |
| | | * @param bool $refresh Force refresh from database |
| | | * @return array |
| | | */ |
| | | private static function getConnections(bool $refresh = false): array |
| | | { |
| | | if (self::$connections_cache === null || $refresh) { |
| | | self::$connections_cache = get_option(self::CONNECTIONS_OPTION, []); |
| | | } |
| | | |
| | | return self::$connections_cache; |
| | | } |
| | | |
| | | /** |
| | | * Flush all caches connected to this one |
| | | * |
| | | * @return self For chaining |
| | | */ |
| | | public function connections(): self |
| | | { |
| | | $all_connections = self::getConnections(); |
| | | |
| | | foreach ($all_connections as $cache_group => $connections) { |
| | | foreach ($connections as $conn) { |
| | | if ($this->matchesConnection($conn)) { |
| | | $this->flushConnection($cache_group, $conn); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return $this; |
| | | } |
| | | |
| | | /** |
| | | * Check if this cache group matches a connection definition |
| | | */ |
| | | private function matchesConnection(array $connection): bool |
| | | { |
| | | $parent = $connection['parent'] ?? ''; |
| | | $scope = $connection['scope'] ?? 'id'; |
| | | |
| | | // Grand overview match |
| | | if ($this->group === $parent) { |
| | | return true; |
| | | } |
| | | |
| | | // Type-specific match |
| | | if ($scope !== 'id') { |
| | | if ($this->group === jvbNoBase($scope)) { |
| | | return true; |
| | | } |
| | | |
| | | // Check constants |
| | | if ($parent === 'post' && defined('JVB_CONTENT')) { |
| | | return isset(JVB_CONTENT[$scope]) && jvbNoBase($scope) === $this->group; |
| | | } |
| | | |
| | | if ($parent === 'taxonomy' && defined('JVB_TAXONOMY')) { |
| | | return isset(JVB_TAXONOMY[$scope]) && jvbNoBase($scope) === $this->group; |
| | | } |
| | | } |
| | | |
| | | // ID-specific match: 'user_123' matches 'user' + 'id' |
| | | if ($scope === 'id' && str_starts_with($this->group, $parent . '_')) { |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Flush a connected cache group |
| | | * For ID-specific connections, deletes the specific ID key |
| | | * For type/overview connections, flushes entire group |
| | | */ |
| | | private function flushConnection(string $cache_group, array $connection): void |
| | | { |
| | | $scope = $connection['scope'] ?? 'id'; |
| | | |
| | | // ID-specific: delete specific key |
| | | if ($scope === 'id') { |
| | | $id = $this->extractIdFromGroup(); |
| | | |
| | | if ($id !== null) { |
| | | self::invalidateKeys($cache_group, $id); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // Type/overview: flush entire group |
| | | self::invalidateAll($cache_group, specific_keys: null, flush_connections: false); |
| | | } |
| | | |
| | | /** |
| | | * Extract ID from group name like 'user_123' -> '123' |
| | | * |
| | | * @return string|null |
| | | */ |
| | | private function extractIdFromGroup(): ?string |
| | | { |
| | | if (preg_match('/^[a-z]+_(\d+)$/', $this->group, $matches)) { |
| | | return $matches[1]; |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Register multiple connections at once |
| | | */ |
| | | public static function registerConnections(array $connections): void |
| | | { |
| | | $existing = self::getConnections(); |
| | | $changed = false; |
| | | |
| | | foreach ($connections as $cache_group => $configs) { |
| | | if (!isset($existing[$cache_group])) { |
| | | $existing[$cache_group] = []; |
| | | } |
| | | |
| | | foreach ($configs as $config) { |
| | | $duplicate = false; |
| | | foreach ($existing[$cache_group] as $existing_config) { |
| | | if ($existing_config === $config) { |
| | | $duplicate = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!$duplicate) { |
| | | $existing[$cache_group][] = $config; |
| | | $changed = true; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if ($changed) { |
| | | update_option(self::CONNECTIONS_OPTION, $existing, false); |
| | | self::$connections_cache = $existing; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle post save/update |
| | | */ |
| | | public static function onPostSave(int $post_id, \WP_Post $post): void |
| | | { |
| | | // Skip revisions and autosaves |
| | | if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { |
| | | return; |
| | | } |
| | | |
| | | $post_type = jvbNoBase($post->post_type); |
| | | |
| | | // Invalidate post type cache |
| | | self::invalidateAll($post_type); |
| | | |
| | | // Invalidate specific post cache |
| | | self::invalidateAll($post_id); |
| | | // Clear WordPress core post object cache |
| | | clean_post_cache($post_id); |
| | | } |
| | | |
| | | /** |
| | | * Handle post deletion |
| | | */ |
| | | public static function onPostDelete(int $post_id): void |
| | | { |
| | | $post = get_post($post_id); |
| | | if (!$post) { |
| | | return; |
| | | } |
| | | |
| | | $post_type = jvbNoBase($post->post_type); |
| | | |
| | | self::invalidateAll($post_type); |
| | | self::invalidateAll($post_id); |
| | | // Clear WordPress core post object cache |
| | | clean_post_cache($post_id); |
| | | } |
| | | |
| | | /** |
| | | * Handle term save/update |
| | | */ |
| | | public static function onTermSave(int $term_id, int $tt_id, string $taxonomy): void |
| | | { |
| | | // Clear WordPress core term cache |
| | | clean_term_cache($term_id, $taxonomy); |
| | | $taxonomy = jvbNoBase($taxonomy); |
| | | |
| | | // Invalidate taxonomy cache |
| | | self::invalidateAll($taxonomy); |
| | | |
| | | // Invalidate specific term cache |
| | | self::invalidateAll($term_id); |
| | | } |
| | | |
| | | /** |
| | | * Handle term deletion |
| | | */ |
| | | public static function onTermDelete(int $term_id, int $tt_id, string $taxonomy): void |
| | | { |
| | | // Clear WordPress core term cache |
| | | clean_term_cache($term_id, $taxonomy); |
| | | $taxonomy = jvbNoBase($taxonomy); |
| | | |
| | | self::invalidateAll($taxonomy); |
| | | self::invalidateAll($term_id); |
| | | } |
| | | |
| | | /** |
| | | * Handle user update |
| | | */ |
| | | public static function onUserUpdate(int $user_id, ?\WP_User $old_user_data = null): void |
| | | { |
| | | // Invalidate user-specific cache |
| | | self::invalidateAll($user_id); |
| | | |
| | | // Invalidate user role caches if roles changed |
| | | if ($old_user_data) { |
| | | $user = get_userdata($user_id); |
| | | if ($user && $user->roles !== $old_user_data->roles) { |
| | | foreach (array_merge($user->roles, $old_user_data->roles) as $role) { |
| | | self::invalidateAll($role); |
| | | } |
| | | } |
| | | } |
| | | // Clear WordPress core user cache |
| | | clean_user_cache($user_id); |
| | | } |
| | | |
| | | /** |
| | | * Handle user deletion |
| | | */ |
| | | public static function onUserDelete(int $user_id): void |
| | | { |
| | | self::invalidateAll($user_id); |
| | | // Clear WordPress core user cache |
| | | clean_user_cache($user_id); |
| | | } |
| | | |
| | | /** |
| | | * Handle post meta updates |
| | | */ |
| | | public static function onPostMetaUpdate(int $meta_id, int $post_id, string $meta_key, mixed $meta_value): void |
| | | { |
| | | if (!str_starts_with($meta_key, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $post = get_post($post_id); |
| | | if (!$post) { |
| | | return; |
| | | } |
| | | |
| | | self::onPostSave($post_id, $post); |
| | | } |
| | | public static function onPostMetaDelete(array $meta_ids, int $post_id, string $meta_key, mixed $meta_value):void |
| | | { |
| | | if (!str_starts_with($meta_key, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $post = get_post($post_id); |
| | | if (!$post) { |
| | | return; |
| | | } |
| | | |
| | | self::onPostSave($post_id, $post); |
| | | } |
| | | |
| | | /** |
| | | * Handle term meta updates |
| | | */ |
| | | public static function onTermMetaUpdate(int $meta_id, int $term_id, string $meta_key, mixed $meta_value): void |
| | | { |
| | | if (!str_starts_with($meta_key, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $term = get_term($term_id); |
| | | if (!$term || is_wp_error($term)) { |
| | | return; |
| | | } |
| | | |
| | | self::onTermSave($term_id, $term->term_taxonomy_id, $term->taxonomy); |
| | | } |
| | | |
| | | public static function onTermMetaDelete(array $meta_ids, int $term_id, string $meta_key, mixed $meta_value):void |
| | | { |
| | | if (!str_starts_with($meta_key, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $term = get_term($term_id); |
| | | if (!$term || is_wp_error($term)) { |
| | | return; |
| | | } |
| | | |
| | | self::onTermSave($term_id, $term->term_taxonomy_id, $term->taxonomy); |
| | | } |
| | | |
| | | /** |
| | | * Handle user meta updates |
| | | */ |
| | | public static function onUserMetaUpdate(int $meta_id, int $user_id, string $meta_key, mixed $meta_value): void |
| | | { |
| | | if (!str_starts_with($meta_key, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | self::onUserUpdate($user_id, null); |
| | | } |
| | | |
| | | public static function onUserMetaDelete(array $meta_ids, int $user_id, string $meta_key, mixed $meta_value):void |
| | | { |
| | | if (!str_starts_with($meta_key, BASE)) { |
| | | return; |
| | | } |
| | | |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | self::onUserUpdate($user_id, null); |
| | | } |
| | | } |