| | |
| | | */ |
| | | 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 |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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)) { |
| | |
| | | $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); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | /** |
| | | * 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; |
| | | } |
| | | |
| | |
| | | $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; |
| | | } |
| | | |
| | | /** |
| | |
| | | $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) |
| | |
| | | $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 |
| | |
| | | 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 |
| | |
| | | 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); |
| | | } |
| | | } |