Jake Vanderwerf
2026-01-25 b38f03c0e7218762d90fa5092696b127f24f36db
inc/managers/CacheManagerOld.php
@@ -2,30 +2,296 @@
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;
   }
   /**
@@ -37,39 +303,17 @@
   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;
   }
   /**
@@ -84,25 +328,21 @@
   {
      $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)
@@ -112,147 +352,60 @@
   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
         )
      );
   }
   /**
@@ -289,22 +442,18 @@
   {
      $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;
   }
   /**
@@ -318,59 +467,390 @@
   }
   /**
    * 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);
   }
}