From 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 19 Jan 2026 16:29:41 +0000
Subject: [PATCH] =Refactored window.getTemplate into a full templating class window.jvbTemplates. Refactored CRUD.js, UploadManager.js, FormController.js, PopulateForm.js with that in mind

---
 inc/managers/CacheManager.php |  743 +++++++++++++++++++++++++++++++++++---------------------
 1 files changed, 466 insertions(+), 277 deletions(-)

diff --git a/inc/managers/CacheManager.php b/inc/managers/CacheManager.php
index c8a38b9..2583431 100644
--- a/inc/managers/CacheManager.php
+++ b/inc/managers/CacheManager.php
@@ -13,14 +13,15 @@
  */
 class CacheManager
 {
+	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 array $relationships = []; // Type => [related types]
-	private static bool $relationships_loaded = false;
+	private static ?CacheManager $singleton = null;
 
 	/**
 	 * Private constructor - use for() factory method instead
@@ -33,6 +34,76 @@
 		if (is_null(static::$use_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);
 	}
 
 	/**
@@ -127,21 +198,21 @@
 	}
 
 	/**
-	 * Invalidate cache for a content type with automatic cascade
+	 * Invalidate cache for a content type
 	 *
 	 * @param string $type Content type to invalidate
-	 * @param mixed $context Post/Term object or array with relationship data (for cascade)
 	 * @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, $context = null, $specific_keys = null): 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 (don't flush whole group)
+		// If specific keys provided, only delete those
 		if ($specific_keys !== null) {
 			$instance = self::for($type);
 			if (is_array($specific_keys)) {
@@ -152,21 +223,20 @@
 				$instance->delete($specific_keys);
 			}
 		} else {
-			// No specific keys - flush the entire group
+			// Flush the entire group
 			if (function_exists('wp_cache_flush_group')) {
 				wp_cache_flush_group($type);
 			} else {
-				// Fallback for older WP
 				wp_cache_flush();
 			}
 		}
 
-		// Cascade to related types if context provided
-		if ($context !== null) {
-			self::cascadeInvalidation($type, $context);
+		// Flush connected caches
+		if ($flush_connections) {
+			self::for($type)->connections();
 		}
 
-		do_action('jvb_cache_invalidated', $type, $context);
+		do_action('jvb_cache_invalidated', $type);
 	}
 
 	/**
@@ -192,15 +262,14 @@
 
 	/**
 	 * Fluent instance method to invalidate this cache type
-	 * Allows chaining: CacheManager::for('tattoo')->invalidate()->clear()
 	 *
-	 * @param mixed $context Optional context for cascade
 	 * @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($context = null, $specific_keys = null): self
+	public function invalidate($specific_keys = null, bool $flush_connections = true): self
 	{
-		self::invalidateAll($this->group, $context, $specific_keys);
+		self::invalidateAll($this->group, $specific_keys, $flush_connections);
 		return $this;
 	}
 
@@ -237,7 +306,14 @@
 		$key = $this->normalizeKey($key);
 		$cache_key = $this->buildKey($key);
 
-		return wp_cache_get($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 $value;
 	}
 
 	/**
@@ -255,12 +331,18 @@
 		$key = $this->normalizeKey($key);
 		$cache_key = $this->buildKey($key);
 
-		// Update timestamp when setting new data
 		self::updateTimestamp($this->group);
 
-		return wp_cache_set($cache_key, $value, $group, $ttl);
-	}
+		// 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)
@@ -273,9 +355,17 @@
 		$key = $this->normalizeKey($key);
 		$cache_key = $this->buildKey($key);
 
-		return wp_cache_delete($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;
 	}
 
+
 	/**
 	 * Clear all cache for this group
 	 * @return bool
@@ -285,16 +375,40 @@
 		try {
 			if (function_exists('wp_cache_flush_group')) {
 				wp_cache_flush_group($this->group);
-				self::updateTimestamp($this->group);
-				return true;
 			}
-			return false;
+
+			// 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;
 		}
 	}
 
 	/**
+	 * Clear all transients for this cache group
+	 */
+	private function clearGroupTransients(): void
+	{
+		global $wpdb;
+
+		$pattern = '_transient_' . $this->group . '_' . $this->prefix . '%';
+		$timeout_pattern = '_transient_timeout_' . $this->group . '_' . $this->prefix . '%';
+
+		$wpdb->query(
+			$wpdb->prepare(
+				"DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s",
+				$pattern,
+				$timeout_pattern
+			)
+		);
+	}
+
+	/**
 	 * Helper to generateKey from array if applicable
 	 * @param string|array $key
 	 * @return string
@@ -360,308 +474,383 @@
 		return $this->group;
 	}
 
-	// ===== RELATIONSHIP MANAGEMENT =====
 
+	/***************************************************************************
+	 * 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 = ??
+	***************************************************************************/
 	/**
-	 * Register cache relationship
-	 * When $type is invalidated, these related types are also invalidated
+	 * Define a connection between cache groups
+	 * Connected caches will have their ID-based keys deleted when this cache invalidates
 	 *
-	 * @param string $type Primary type
-	 * @param array $config Relationship configuration
-	 *   - 'author' => bool - Invalidate user content caches
-	 *   - 'taxonomies' => array - List of taxonomy types to invalidate
-	 *   - 'content_types' => array - List of content types to invalidate
-	 *   - 'related' => array - Generic related types to invalidate
-	 *   - 'cascade' => callable - Custom cascade function
+	 * @param string $type Grand overview ('post', 'taxonomy', 'user')
+	 * @param string $scope Type-specific constant, user role, or 'id'
+	 * @return self For chaining
 	 */
-	public static function registerRelationship(string $type, array $config): void
+	public function connectTo(string $type, string $scope = 'id'): self
 	{
-		$type = jvbNoBase($type);
+		//TODO: Handle connect to where $type === 'all'
+		$connections = self::getConnections();
 
-		// Merge with existing relationships
-		self::$relationships[$type] = array_merge(
-			self::$relationships[$type] ?? [],
-			$config
-		);
+		if (!isset($connections[$this->group])) {
+			$connections[$this->group] = [];
+		}
 
-		// Build reverse relationships for bidirectional linking
-		self::buildReverseRelationships($type, $config);
-	}
+		$new_connection = [
+			'parent' => $type,
+			'scope' => $scope
+		];
 
-	/**
-	 * Build reverse relationships (if A relates to B, B should know about A)
-	 *
-	 * @param string $type The type being registered
-	 * @param array $config Its relationship config
-	 */
-	private static function buildReverseRelationships(string $type, array $config): void
-	{
-		// If this type relates to taxonomies, those taxonomies should know about this type
-		if (!empty($config['taxonomies'])) {
-			foreach ($config['taxonomies'] as $taxonomy) {
-				$taxonomy = jvbNoBase($taxonomy);
-				self::$relationships[$taxonomy]['content_types'] =
-					array_unique(array_merge(
-						self::$relationships[$taxonomy]['content_types'] ?? [],
-						[$type]
-					));
+		// Check if already exists
+		foreach ($connections[$this->group] as $existing) {
+			if ($existing === $new_connection) {
+				return $this;
 			}
 		}
 
-		// If this type relates to content_types, those types should know about this taxonomy
-		if (!empty($config['content_types'])) {
-			foreach ($config['content_types'] as $content_type) {
-				$content_type = jvbNoBase($content_type);
-				self::$relationships[$content_type]['related'] =
-					array_unique(array_merge(
-						self::$relationships[$content_type]['related'] ?? [],
-						[$type]
-					));
+		$connections[$this->group][] = $new_connection;
+		update_option(self::CONNECTIONS_OPTION, $connections, false);
+		self::$connections_cache = $connections;
+
+		return $this;
+	}
+
+	/**
+	 * 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;
+		}
 	}
 
 	/**
-	 * Load relationships from JVB_CONTENT and JVB_TAXONOMY
+	 * Handle post save/update
 	 */
-	private static function loadRelationships(): void
+	public static function onPostSave(int $post_id, \WP_Post $post): void
 	{
-		if (self::$relationships_loaded) {
+		// Skip revisions and autosaves
+		if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
 			return;
 		}
 
-		// Load post type relationships
-		if (defined('JVB_CONTENT')) {
-			foreach (JVB_CONTENT as $slug => $config) {
-				$relationships = [];
+		$post_type = jvbNoBase($post->post_type);
 
-				// Author relationship
-				if (!($config['no_author'] ?? false)) {
-					$relationships['author'] = true;
-				}
+		// Invalidate post type cache
+		self::invalidateAll($post_type);
 
-				// Taxonomy relationships
-				if (!empty($config['taxonomies'])) {
-					$relationships['taxonomies'] = array_map('jvbNoBase', $config['taxonomies']);
-				}
-
-				// Custom relationships from config
-				if (!empty($config['cache_relationships'])) {
-					$relationships = array_merge($relationships, $config['cache_relationships']);
-				}
-
-				if (!empty($relationships)) {
-					self::registerRelationship($slug, $relationships);
-				}
-			}
-		}
-
-		// Load taxonomy relationships
-		if (defined('JVB_TAXONOMY')) {
-			foreach (JVB_TAXONOMY as $slug => $config) {
-				$relationships = [];
-
-				// Content type relationships
-				if (!empty($config['for_content'])) {
-					$relationships['content_types'] = array_map('jvbNoBase', $config['for_content']);
-				}
-
-				// Always include generic 'terms' cache
-				$relationships['related'] = ['terms'];
-
-				// Custom relationships from config
-				if (!empty($config['cache_relationships'])) {
-					$relationships = array_merge($relationships, $config['cache_relationships']);
-				}
-
-				if (!empty($relationships)) {
-					self::registerRelationship($slug, $relationships);
-				}
-			}
-		}
-
-		self::$relationships_loaded = true;
-
-		do_action('jvb_cache_relationships_loaded', self::$relationships);
+		// Invalidate specific post cache
+		self::invalidateAll($post_id);
+		// Clear WordPress core post object cache
+		clean_post_cache($post_id);
 	}
 
 	/**
-	 * Get relationships for a type (for debugging)
-	 *
-	 * @param string|null $type Specific type or null for all
-	 * @return array Relationships
+	 * Handle post deletion
 	 */
-	public static function getRelationships(?string $type = null): array
+	public static function onPostDelete(int $post_id): void
 	{
-		self::loadRelationships();
-
-		if ($type !== null) {
-			return self::$relationships[jvbNoBase($type)] ?? [];
-		}
-
-		return self::$relationships;
-	}
-
-	/**
-	 * Cascade invalidation to related types based on relationships
-	 *
-	 * @param string $type Primary type being invalidated
-	 * @param mixed $context Context with relationship data
-	 */
-	/**
-	 * Cascade invalidation to related types based on relationships
-	 */
-	private static function cascadeInvalidation(string $type, $context): void
-	{
-		self::loadRelationships();
-
-		$relationships = self::$relationships[$type] ?? [];
-		if (empty($relationships)) {
+		$post = get_post($post_id);
+		if (!$post) {
 			return;
 		}
 
-		$data = self::extractContext($context);
+		$post_type = jvbNoBase($post->post_type);
 
-		// Author relationship - SIMPLIFIED
-		if (!empty($relationships['author'])) {
-			$user_ids = self::extractUserIds($data, $relationships['author']);
-
-			foreach ($user_ids as $user_id) {
-				// Single clean call - handles content, profile, everything
-				self::invalidateAll("user_{$user_id}");
-			}
-		}
-
-		// Taxonomy relationships
-		if (!empty($relationships['taxonomies']) && !empty($data['ID'])) {
-			foreach ($relationships['taxonomies'] as $taxonomy) {
-				$taxonomy_full = jvbCheckBase($taxonomy);
-				$terms = wp_get_post_terms($data['ID'], $taxonomy_full, ['fields' => 'ids']);
-
-				if (!empty($terms) && !is_wp_error($terms)) {
-					self::updateTimestamp($taxonomy);
-					wp_cache_flush_group($taxonomy);
-				}
-			}
-		}
-
-		// Content type relationships (for taxonomies)
-		if (!empty($relationships['content_types'])) {
-			foreach ($relationships['content_types'] as $content_type) {
-				self::updateTimestamp($content_type);
-				wp_cache_flush_group($content_type);
-			}
-		}
-
-		// Generic related caches
-		if (!empty($relationships['related'])) {
-			foreach ($relationships['related'] as $related_type) {
-				self::updateTimestamp($related_type);
-				wp_cache_flush_group($related_type);
-			}
-		}
-
-		// Custom cascade function
-		if (!empty($relationships['cascade']) && is_callable($relationships['cascade'])) {
-			call_user_func($relationships['cascade'], $type, $data);
-		}
+		self::invalidateAll($post_type);
+		self::invalidateAll($post_id);
+		// Clear WordPress core post object cache
+		clean_post_cache($post_id);
 	}
 
 	/**
-	 * Extract user IDs from context based on relationship config
-	 * Supports multiple authors, contributors, etc.
-	 *
-	 * @param array $data Context data
-	 * @param mixed $config Author relationship config (bool or array)
-	 * @return array User IDs to invalidate
+	 * Handle term save/update
 	 */
-	private static function extractUserIds(array $data, $config): array
+	public static function onTermSave(int $term_id, int $tt_id, string $taxonomy): void
 	{
-		$user_ids = [];
+		// Clear WordPress core term cache
+		clean_term_cache($term_id, $taxonomy);
+		$taxonomy = jvbNoBase($taxonomy);
 
-		// Simple case: 'author' => true
-		if ($config === true) {
-			if (!empty($data['post_author'])) {
-				$user_ids[] = $data['post_author'];
-			}
-			return array_filter($user_ids);
-		}
+		// Invalidate taxonomy cache
+		self::invalidateAll($taxonomy);
 
-		// Advanced case: 'author' => ['post_author', 'contributors', 'linked_user']
-		if (is_array($config)) {
-			foreach ($config as $field) {
-				// Handle meta fields
-				if (str_starts_with($field, 'meta:') && !empty($data['ID'])) {
-					$meta_key = substr($field, 5);
-					$value = get_post_meta($data['ID'], BASE . $meta_key, true);
-
-					if (is_array($value)) {
-						$user_ids = array_merge($user_ids, $value);
-					} elseif ($value) {
-						$user_ids[] = $value;
-					}
-				}
-				// Handle direct data fields
-				elseif (!empty($data[$field])) {
-					if (is_array($data[$field])) {
-						$user_ids = array_merge($user_ids, $data[$field]);
-					} else {
-						$user_ids[] = $data[$field];
-					}
-				}
-			}
-		}
-
-		// Callable: 'author' => function($data) { return [...user_ids]; }
-		if (is_callable($config)) {
-			$result = call_user_func($config, $data);
-			if (is_array($result)) {
-				$user_ids = array_merge($user_ids, $result);
-			} elseif ($result) {
-				$user_ids[] = $result;
-			}
-		}
-
-		return array_unique(array_filter(array_map('intval', $user_ids)));
+		// Invalidate specific term cache
+		self::invalidateAll($term_id);
 	}
 
 	/**
-	 * Extract context data from various formats
-	 * Converts WP objects to arrays with relevant data
-	 *
-	 * @param mixed $context Post/Term object, array, or ID
-	 * @return array Normalized context data
+	 * Handle term deletion
 	 */
-	private static function extractContext($context): array
+	public static function onTermDelete(int $term_id, int $tt_id, string $taxonomy): void
 	{
-		if (is_array($context)) {
-			return $context;
-		}
+		// Clear WordPress core term cache
+		clean_term_cache($term_id, $taxonomy);
+		$taxonomy = jvbNoBase($taxonomy);
 
-		if ($context instanceof \WP_Post) {
-			return [
-				'ID' => $context->ID,
-				'post_author' => $context->post_author,
-				'post_type' => $context->post_type,
-				'post_status' => $context->post_status,
-			];
-		}
+		self::invalidateAll($taxonomy);
+		self::invalidateAll($term_id);
+	}
 
-		if ($context instanceof \WP_Term) {
-			return [
-				'term_id' => $context->term_id,
-				'taxonomy' => $context->taxonomy,
-				'parent' => $context->parent,
-			];
-		}
+	/**
+	 * 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);
 
-		if (is_numeric($context)) {
-			$post = get_post($context);
-			if ($post) {
-				return self::extractContext($post);
+		// 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);
+	}
 
-		return [];
+	/**
+	 * 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);
 	}
 }

--
Gitblit v1.10.0