| | |
| | | namespace JVBase\managers; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * 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 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; |
| | | |
| | | /** |
| | | * @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(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 with automatic cascade |
| | | * |
| | | * @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 |
| | | * @return void |
| | | */ |
| | | public static function invalidateAll(string $type, $context = null, $specific_keys = null): 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 !== null) { |
| | | $instance = self::for($type); |
| | | if (is_array($specific_keys)) { |
| | | foreach ($specific_keys as $key) { |
| | | $instance->delete($key); |
| | | } |
| | | } else { |
| | | $instance->delete($specific_keys); |
| | | } |
| | | } else { |
| | | // No specific keys - 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); |
| | | } |
| | | |
| | | do_action('jvb_cache_invalidated', $type, $context); |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | * Allows chaining: CacheManager::for('tattoo')->invalidate()->clear() |
| | | * |
| | | * @param mixed $context Optional context for cascade |
| | | * @param string|array|null $specific_keys Optional specific key(s) |
| | | * @return self For chaining |
| | | */ |
| | | public function invalidate($context = null, $specific_keys = null): self |
| | | { |
| | | self::invalidateAll($this->group, $context, $specific_keys); |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | | * Get a value from the cache |
| | | * @param string|array $key The key to look up (auto-generates key from array of key=>values) |
| | | * @param string|null $group The group to get from. Defaults to current group |
| | |
| | | public function get(string|array $key, ?string $group = null): mixed |
| | | { |
| | | $group = $group ?: $this->group; |
| | | |
| | | $key = $this->normalizeKey($key); |
| | | |
| | | $cache_key = $this->buildKey($key); |
| | | |
| | | // Use appropriate cache method |
| | | if (static::$use_object_cache) { |
| | | $value = wp_cache_get($cache_key, $group); |
| | | } else { |
| | | // Fallback to transients for local development |
| | | $value = get_transient($this->getTransientKey($cache_key, $group)); |
| | | } |
| | | |
| | | return (is_array($value) && array_key_exists('data', $value)) ? $value['data'] : $value; |
| | | } |
| | | |
| | | public function getTimestamp(string|array $key, ?string $group = null): mixed |
| | | { |
| | | $group = $group ?: $this->group; |
| | | |
| | | $key = $this->normalizeKey($key); |
| | | |
| | | $cache_key = $this->buildKey($key); |
| | | |
| | | // Use appropriate cache method |
| | | if (static::$use_object_cache) { |
| | | $value = wp_cache_get($cache_key, $group); |
| | | } else { |
| | | // Fallback to transients for local development |
| | | $value = get_transient($this->getTransientKey($cache_key, $group)); |
| | | } |
| | | |
| | | return (is_array($value) && array_key_exists('last_modified', $value)) ? $value['last_modified'] : false; |
| | | return wp_cache_get($cache_key, $group); |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | $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) { |
| | | // Update timestamp when setting new data |
| | | self::updateTimestamp($this->group); |
| | | |
| | | return wp_cache_set($cache_key, $value, $group, $ttl); |
| | | } else { |
| | | // Fallback to transients |
| | | return set_transient($this->getTransientKey($cache_key, $group), $value, $ttl); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | 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)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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); |
| | | self::updateTimestamp($this->group); |
| | | return true; |
| | | } |
| | | return false; |
| | | } else { |
| | | // For transients, search and delete |
| | | global $wpdb; |
| | | |
| | | $prefix = self::getTransientPrefix($this->group); |
| | | $sql = "SELECT option_name FROM {$wpdb->options} |
| | | WHERE option_name LIKE %s |
| | | AND option_name LIKE %s"; |
| | | |
| | | $keys = $wpdb->get_col($wpdb->prepare( |
| | | $sql, |
| | | '_transient_' . $prefix . '%' |
| | | )); |
| | | |
| | | foreach ($keys as $key) { |
| | | $transient_key = str_replace('_transient_', '', $key); |
| | | delete_transient($transient_key); |
| | | } |
| | | return true; |
| | | } |
| | | } catch (\Exception $e) { |
| | | |
| | | } finally { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Alias for delete() for backwards compatibility |
| | | * @param string $key The key to look up (auto-generates key from array of key=>values) |
| | | * @param string|null $group The group to delete from (defaults to current group)) |
| | | * @return void |
| | | */ |
| | | public function invalidate(string $key, ?string $group = null): void |
| | | { |
| | | $this->delete($key, $group); |
| | | } |
| | | |
| | | /** |
| | | * Clear all cache entries for a group |
| | | * @param string $group The group to clear |
| | | * @return bool |
| | | */ |
| | | public static function invalidateGroup(string $group): bool |
| | | { |
| | | $group = jvbNoBase($group); |
| | | |
| | | if (wp_using_ext_object_cache()) { |
| | | // With Redis/Memcached, use native group flush |
| | | if (function_exists('wp_cache_flush_group')) { |
| | | return wp_cache_flush_group($group); |
| | | } else { |
| | | // Fallback for older WP versions - flush everything (not ideal) |
| | | return wp_cache_flush(); |
| | | } |
| | | } else { |
| | | // For transients, we need to delete them from database |
| | | global $wpdb; |
| | | |
| | | $prefix = self::getTransientPrefix($group); |
| | | |
| | | // Delete transients and their timeouts |
| | | $sql = "DELETE FROM {$wpdb->options} |
| | | WHERE option_name LIKE %s |
| | | OR option_name LIKE %s"; |
| | | |
| | | $result = $wpdb->query($wpdb->prepare( |
| | | $sql, |
| | | '_transient_' . $prefix . '%', |
| | | '_transient_timeout_' . $prefix . '%' |
| | | )); |
| | | |
| | | return $result !== false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Clear cache entries by pattern (only works efficiently with Redis) |
| | | * @param string $pattern |
| | | * @return int |
| | | */ |
| | | public function clearPattern(string $pattern): int |
| | | { |
| | | $count = 0; |
| | | |
| | | if (static::$use_object_cache) { |
| | | // With Redis, this could be implemented with SCAN command |
| | | // but wp_cache_* doesn't expose this, so we'd need direct Redis access |
| | | // For now, just flush the group as a nuclear option |
| | | if (function_exists('wp_cache_flush_group')) { |
| | | wp_cache_flush_group($this->group); |
| | | return $count; |
| | | } |
| | | } else { |
| | | // For transients, search and delete |
| | | global $wpdb; |
| | | |
| | | $prefix = self::getTransientPrefix($this->group); |
| | | $sql = "SELECT option_name FROM {$wpdb->options} |
| | | WHERE option_name LIKE %s |
| | | AND option_name LIKE %s"; |
| | | |
| | | $keys = $wpdb->get_col($wpdb->prepare( |
| | | $sql, |
| | | '_transient_' . $prefix . '%', |
| | | '%' . $pattern . '%' |
| | | )); |
| | | |
| | | foreach ($keys as $key) { |
| | | $transient_key = str_replace('_transient_', '', $key); |
| | | delete_transient($transient_key); |
| | | $count++; |
| | | } |
| | | } |
| | | |
| | | return $count; |
| | | } |
| | | |
| | | /** |
| | | * Helper to generateKey from array if applicable |
| | | * @param string|array $key |
| | | * @return string |
| | |
| | | { |
| | | $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; |
| | | |
| | | if (strlen($full_key) > 160) { |
| | | // Use hash for long keys, but keep group prefix for clearPattern() |
| | | return substr($group, 0, 20) . '_' . md5($full_key); |
| | | return $this->group; |
| | | } |
| | | |
| | | return $full_key; |
| | | // ===== RELATIONSHIP MANAGEMENT ===== |
| | | |
| | | /** |
| | | * Register cache relationship |
| | | * When $type is invalidated, these related types are also invalidated |
| | | * |
| | | * @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 |
| | | */ |
| | | public static function registerRelationship(string $type, array $config): void |
| | | { |
| | | $type = jvbNoBase($type); |
| | | |
| | | // Merge with existing relationships |
| | | self::$relationships[$type] = array_merge( |
| | | self::$relationships[$type] ?? [], |
| | | $config |
| | | ); |
| | | |
| | | // Build reverse relationships for bidirectional linking |
| | | self::buildReverseRelationships($type, $config); |
| | | } |
| | | |
| | | /** |
| | | * Get transient prefix for a group |
| | | * 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 getTransientPrefix(string $group): string |
| | | private static function buildReverseRelationships(string $type, array $config): void |
| | | { |
| | | return $group . '_jvb_'; |
| | | // 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] |
| | | )); |
| | | } |
| | | } |
| | | |
| | | // 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] |
| | | )); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check if using object cache |
| | | * Load relationships from JVB_CONTENT and JVB_TAXONOMY |
| | | */ |
| | | public function isUsingObjectCache(): bool |
| | | private static function loadRelationships(): void |
| | | { |
| | | return static::$use_object_cache; |
| | | if (self::$relationships_loaded) { |
| | | return; |
| | | } |
| | | |
| | | // Load post type relationships |
| | | if (defined('JVB_CONTENT')) { |
| | | foreach (JVB_CONTENT as $slug => $config) { |
| | | $relationships = []; |
| | | |
| | | // Author relationship |
| | | if (!($config['no_author'] ?? false)) { |
| | | $relationships['author'] = true; |
| | | } |
| | | |
| | | // 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); |
| | | } |
| | | |
| | | /** |
| | | * Cleanup expired transients (maintenance method for non-Redis environments) |
| | | * Get relationships for a type (for debugging) |
| | | * |
| | | * @param string|null $type Specific type or null for all |
| | | * @return array Relationships |
| | | */ |
| | | public static function cleanupExpiredTransients(): int |
| | | public static function getRelationships(?string $type = null): array |
| | | { |
| | | if (wp_using_ext_object_cache()) { |
| | | return 0; // Not needed with Redis |
| | | self::loadRelationships(); |
| | | |
| | | if ($type !== null) { |
| | | return self::$relationships[jvbNoBase($type)] ?? []; |
| | | } |
| | | |
| | | global $wpdb; |
| | | return self::$relationships; |
| | | } |
| | | |
| | | // 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"; |
| | | /** |
| | | * 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(); |
| | | |
| | | return $wpdb->query($wpdb->prepare($sql, time())); |
| | | $relationships = self::$relationships[$type] ?? []; |
| | | if (empty($relationships)) { |
| | | return; |
| | | } |
| | | |
| | | $data = self::extractContext($context); |
| | | |
| | | // 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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | */ |
| | | private static function extractUserIds(array $data, $config): array |
| | | { |
| | | $user_ids = []; |
| | | |
| | | // Simple case: 'author' => true |
| | | if ($config === true) { |
| | | if (!empty($data['post_author'])) { |
| | | $user_ids[] = $data['post_author']; |
| | | } |
| | | return array_filter($user_ids); |
| | | } |
| | | |
| | | // 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))); |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | */ |
| | | private static function extractContext($context): array |
| | | { |
| | | if (is_array($context)) { |
| | | return $context; |
| | | } |
| | | |
| | | if ($context instanceof \WP_Post) { |
| | | return [ |
| | | 'ID' => $context->ID, |
| | | 'post_author' => $context->post_author, |
| | | 'post_type' => $context->post_type, |
| | | 'post_status' => $context->post_status, |
| | | ]; |
| | | } |
| | | |
| | | if ($context instanceof \WP_Term) { |
| | | return [ |
| | | 'term_id' => $context->term_id, |
| | | 'taxonomy' => $context->taxonomy, |
| | | 'parent' => $context->parent, |
| | | ]; |
| | | } |
| | | |
| | | if (is_numeric($context)) { |
| | | $post = get_post($context); |
| | | if ($post) { |
| | | return self::extractContext($post); |
| | | } |
| | | } |
| | | |
| | | return []; |
| | | } |
| | | } |