cacheManager = $cacheManager; $this->initializeDependencyGraph(); $this->registerHooks(); } /** * Register cache dependency relationship * * @param string $key Primary cache key * @param array $dependencies Dependent cache keys * @param array $options Dependency options * @return void * @since 1.0.0 */ public function registerDependency(string $key, array $dependencies, array $options = []): void { $this->dependencyGraph[$key] = [ 'dependencies' => $dependencies, 'type' => $options['type'] ?? 'cascade', 'delay' => $options['delay'] ?? 0, 'conditions' => $options['conditions'] ?? [] ]; } /** * Intelligent invalidation with dependency resolution * * @param string|array $keys Keys to invalidate * @param array $options Invalidation options * @return array Invalidation results * @since 1.0.0 */ public function invalidate($keys, array $options = []): array { $keys = is_array($keys) ? $keys : [$keys]; $startTime = microtime(true); $invalidationPlan = $this->buildInvalidationPlan($keys, $options); $results = $this->executeInvalidationPlan($invalidationPlan); $this->logInvalidation($keys, $results, microtime(true) - $startTime); return $results; } /** * Selective invalidation based on data changes * * @param string $entityType Type of entity changed (doctor, service, etc.) * @param mixed $entityId Entity identifier * @param array $changedFields Changed field names * @return array Invalidation results * @since 1.0.0 */ public function invalidateByEntity(string $entityType, $entityId, array $changedFields = []): array { $keysToInvalidate = $this->resolveEntityCacheKeys($entityType, $entityId, $changedFields); if (empty($keysToInvalidate)) { return ['invalidated' => 0, 'keys' => []]; } return $this->invalidate($keysToInvalidate, ['entity_based' => true]); } /** * Batch invalidation with performance optimization * * @param array $operations Batch of invalidation operations * @return array Batch results * @since 1.0.0 */ public function batchInvalidate(array $operations): array { $startTime = microtime(true); $allKeys = []; // Collect all keys to avoid duplicate invalidations foreach ($operations as $operation) { $keys = $operation['keys'] ?? []; $allKeys = array_merge($allKeys, is_array($keys) ? $keys : [$keys]); } // Remove duplicates and execute single invalidation $uniqueKeys = array_unique($allKeys); $results = $this->invalidate($uniqueKeys, ['batch' => true]); return [ 'operations_count' => count($operations), 'unique_keys_count' => count($uniqueKeys), 'execution_time' => (microtime(true) - $startTime) * 1000, 'results' => $results ]; } /** * Schedule delayed invalidation * * @param string|array $keys Keys to invalidate * @param int $delay Delay in seconds * @param array $options Scheduling options * @return string Schedule ID * @since 1.0.0 */ public function scheduleInvalidation($keys, int $delay, array $options = []): string { $scheduleId = uniqid('schedule_', true); $executeAt = time() + $delay; $this->scheduleInvalidations[$scheduleId] = [ 'keys' => is_array($keys) ? $keys : [$keys], 'execute_at' => $executeAt, 'options' => $options, 'created_at' => time() ]; // Schedule WordPress cron if delay is significant if ($delay > 300) { // 5 minutes wp_schedule_single_event($executeAt, 'care_book_ultimate_scheduled_invalidation', [$scheduleId]); } return $scheduleId; } /** * Cancel scheduled invalidation * * @param string $scheduleId Schedule identifier * @return bool Success status * @since 1.0.0 */ public function cancelScheduledInvalidation(string $scheduleId): bool { if (!isset($this->scheduleInvalidations[$scheduleId])) { return false; } $schedule = $this->scheduleInvalidations[$scheduleId]; // Remove from WordPress cron if scheduled wp_clear_scheduled_hook('care_book_ultimate_scheduled_invalidation', [$scheduleId]); unset($this->scheduleInvalidations[$scheduleId]); return true; } /** * Execute scheduled invalidations * * @return array Execution results * @since 1.0.0 */ public function executeScheduledInvalidations(): array { $currentTime = time(); $executed = []; foreach ($this->scheduleInvalidations as $scheduleId => $schedule) { if ($schedule['execute_at'] <= $currentTime) { $results = $this->invalidate($schedule['keys'], $schedule['options']); $executed[$scheduleId] = $results; unset($this->scheduleInvalidations[$scheduleId]); } } return $executed; } /** * Get invalidation statistics and metrics * * @return array Statistics * @since 1.0.0 */ public function getStatistics(): array { $recentInvalidations = array_slice($this->invalidationLog, -100); return [ 'total_invalidations' => count($this->invalidationLog), 'recent_invalidations' => count($recentInvalidations), 'average_execution_time' => $this->calculateAverageExecutionTime($recentInvalidations), 'dependency_graph_size' => count($this->dependencyGraph), 'scheduled_invalidations' => count($this->scheduleInvalidations), 'cache_efficiency' => $this->calculateCacheEfficiency() ]; } /** * Optimize dependency graph for performance * * @return array Optimization results * @since 1.0.0 */ public function optimizeDependencyGraph(): array { $originalSize = count($this->dependencyGraph); // Remove circular dependencies $this->removeCircularDependencies(); // Optimize dependency chains $this->optimizeDependencyChains(); // Remove unused dependencies $this->removeUnusedDependencies(); $optimizedSize = count($this->dependencyGraph); return [ 'original_size' => $originalSize, 'optimized_size' => $optimizedSize, 'reduction' => $originalSize - $optimizedSize, 'improvement_percentage' => $originalSize > 0 ? (($originalSize - $optimizedSize) / $originalSize) * 100 : 0 ]; } /** * Build invalidation plan with dependency resolution * * @param array $keys Initial keys to invalidate * @param array $options Planning options * @return array Invalidation plan * @since 1.0.0 */ private function buildInvalidationPlan(array $keys, array $options): array { $plan = [ 'immediate' => [], 'delayed' => [], 'conditional' => [] ]; $processed = []; $queue = $keys; while (!empty($queue)) { $key = array_shift($queue); if (in_array($key, $processed)) { continue; } $processed[] = $key; // Check if key has dependencies if (isset($this->dependencyGraph[$key])) { $dependency = $this->dependencyGraph[$key]; foreach ($dependency['dependencies'] as $depKey) { if (!in_array($depKey, $processed)) { $queue[] = $depKey; } } // Categorize by execution type if ($dependency['delay'] > 0) { $plan['delayed'][$key] = $dependency; } elseif (!empty($dependency['conditions'])) { $plan['conditional'][$key] = $dependency; } else { $plan['immediate'][] = $key; } } else { $plan['immediate'][] = $key; } } return $plan; } /** * Execute invalidation plan * * @param array $plan Invalidation plan * @return array Execution results * @since 1.0.0 */ private function executeInvalidationPlan(array $plan): array { $results = [ 'immediate' => [], 'delayed' => [], 'conditional' => [], 'total_invalidated' => 0 ]; // Execute immediate invalidations if (!empty($plan['immediate'])) { $success = $this->cacheManager->invalidate($plan['immediate']); $results['immediate'] = $plan['immediate']; $results['total_invalidated'] += count($plan['immediate']); } // Schedule delayed invalidations foreach ($plan['delayed'] as $key => $dependency) { $scheduleId = $this->scheduleInvalidation($key, $dependency['delay']); $results['delayed'][$key] = $scheduleId; } // Execute conditional invalidations foreach ($plan['conditional'] as $key => $dependency) { if ($this->evaluateConditions($dependency['conditions'])) { $success = $this->cacheManager->invalidate([$key]); $results['conditional'][$key] = true; $results['total_invalidated']++; } else { $results['conditional'][$key] = false; } } return $results; } /** * Resolve cache keys for specific entity changes * * @param string $entityType Entity type * @param mixed $entityId Entity ID * @param array $changedFields Changed fields * @return array Cache keys to invalidate * @since 1.0.0 */ private function resolveEntityCacheKeys(string $entityType, $entityId, array $changedFields): array { $keyMappings = [ 'doctor' => [ 'base_keys' => [ 'doctor_' . $entityId, 'doctor_list', 'appointment_availability' ], 'field_mappings' => [ 'status' => ['doctor_restrictions', 'booking_form_data'], 'specialties' => ['service_list', 'appointment_availability'], 'schedule' => ['appointment_availability', 'calendar_data'] ] ], 'service' => [ 'base_keys' => [ 'service_' . $entityId, 'service_list', 'appointment_availability' ], 'field_mappings' => [ 'status' => ['service_restrictions', 'booking_form_data'], 'duration' => ['appointment_availability', 'calendar_data'], 'price' => ['service_list', 'pricing_data'] ] ], 'appointment' => [ 'base_keys' => [ 'appointment_' . $entityId, 'appointment_availability' ], 'field_mappings' => [ 'status' => ['calendar_data', 'appointment_availability'], 'datetime' => ['appointment_availability', 'calendar_data'], 'doctor_id' => ['doctor_schedule', 'appointment_availability'] ] ] ]; if (!isset($keyMappings[$entityType])) { return []; } $mapping = $keyMappings[$entityType]; $keysToInvalidate = $mapping['base_keys']; // Add field-specific keys foreach ($changedFields as $field) { if (isset($mapping['field_mappings'][$field])) { $keysToInvalidate = array_merge($keysToInvalidate, $mapping['field_mappings'][$field]); } } return array_unique($keysToInvalidate); } /** * Initialize dependency graph with default relationships * * @return void * @since 1.0.0 */ private function initializeDependencyGraph(): void { // Doctor-related dependencies $this->registerDependency('doctor_restrictions', [ 'appointment_availability', 'booking_form_data', 'doctor_list' ]); // Service-related dependencies $this->registerDependency('service_restrictions', [ 'appointment_availability', 'service_list', 'booking_form_data' ]); // Global settings dependencies $this->registerDependency('global_settings', [ 'appointment_availability', 'booking_form_data', 'css_injection_cache' ], ['type' => 'cascade_all']); // CSS-related dependencies $this->registerDependency('css_injection_cache', [ 'critical_css', 'inline_styles' ], ['delay' => 1]); // Small delay to batch CSS updates } /** * Register WordPress hooks for automatic invalidation * * @return void * @since 1.0.0 */ private function registerHooks(): void { // KiviCare-specific hooks add_action('kivicare_doctor_status_changed', [$this, 'handleDoctorStatusChange'], 10, 2); add_action('kivicare_service_updated', [$this, 'handleServiceUpdate'], 10, 2); add_action('kivicare_appointment_booked', [$this, 'handleAppointmentBooked'], 10, 1); // WordPress core hooks add_action('updated_option', [$this, 'handleOptionUpdate'], 10, 3); // Scheduled invalidations add_action('care_book_ultimate_scheduled_invalidation', [$this, 'executeScheduledInvalidation'], 10, 1); // Cleanup hooks add_action('care_book_ultimate_daily_cleanup', [$this, 'cleanupInvalidationLog']); } /** * Handle doctor status changes * * @param int $doctorId Doctor ID * @param array $oldData Old doctor data * @return void * @since 1.0.0 */ public function handleDoctorStatusChange(int $doctorId, array $oldData): void { $this->invalidateByEntity('doctor', $doctorId, ['status']); } /** * Handle service updates * * @param int $serviceId Service ID * @param array $updateData Updated fields * @return void * @since 1.0.0 */ public function handleServiceUpdate(int $serviceId, array $updateData): void { $changedFields = array_keys($updateData); $this->invalidateByEntity('service', $serviceId, $changedFields); } /** * Handle appointment booking * * @param array $appointmentData Appointment data * @return void * @since 1.0.0 */ public function handleAppointmentBooked(array $appointmentData): void { $this->invalidateByEntity('appointment', $appointmentData['id'] ?? 0, ['status', 'datetime']); } /** * Handle WordPress option updates * * @param string $option Option name * @param mixed $oldValue Old value * @param mixed $newValue New value * @return void * @since 1.0.0 */ public function handleOptionUpdate(string $option, $oldValue, $newValue): void { // Only handle plugin-specific options if (strpos($option, 'care_book_ultimate_') !== 0) { return; } // Map option to cache keys $optionMappings = [ 'care_book_ultimate_global_settings' => ['global_settings'], 'care_book_ultimate_css_settings' => ['css_injection_cache'], 'care_book_ultimate_performance_settings' => ['performance_config'] ]; if (isset($optionMappings[$option])) { $this->invalidate($optionMappings[$option]); } } /** * Execute individual scheduled invalidation * * @param string $scheduleId Schedule ID * @return void * @since 1.0.0 */ public function executeScheduledInvalidation(string $scheduleId): void { if (isset($this->scheduleInvalidations[$scheduleId])) { $schedule = $this->scheduleInvalidations[$scheduleId]; $this->invalidate($schedule['keys'], $schedule['options']); unset($this->scheduleInvalidations[$scheduleId]); } } /** * Log invalidation operation * * @param array $keys Invalidated keys * @param array $results Operation results * @param float $executionTime Execution time in seconds * @return void * @since 1.0.0 */ private function logInvalidation(array $keys, array $results, float $executionTime): void { $logEntry = [ 'timestamp' => time(), 'keys' => $keys, 'results' => $results, 'execution_time' => $executionTime * 1000, // Convert to milliseconds 'memory_usage' => memory_get_usage(true) ]; $this->invalidationLog[] = $logEntry; // Keep only recent entries to prevent memory bloat if (count($this->invalidationLog) > 1000) { $this->invalidationLog = array_slice($this->invalidationLog, -500); } } /** * Evaluate conditions for conditional invalidation * * @param array $conditions Conditions to evaluate * @return bool True if all conditions are met * @since 1.0.0 */ private function evaluateConditions(array $conditions): bool { foreach ($conditions as $condition) { if (!$this->evaluateCondition($condition)) { return false; } } return true; } /** * Evaluate single condition * * @param array $condition Condition configuration * @return bool Condition result * @since 1.0.0 */ private function evaluateCondition(array $condition): bool { $type = $condition['type'] ?? 'always'; switch ($type) { case 'time_window': $start = $condition['start'] ?? 0; $end = $condition['end'] ?? 24; $currentHour = (int) date('H'); return $currentHour >= $start && $currentHour <= $end; case 'cache_size': $threshold = $condition['threshold'] ?? 100; $currentSize = $this->getCacheSize(); return $currentSize > $threshold; case 'load_average': if (!function_exists('sys_getloadavg')) { return true; // Fallback to true on unsupported systems } $load = sys_getloadavg()[0]; $threshold = $condition['threshold'] ?? 1.0; return $load < $threshold; default: return true; } } /** * Calculate average execution time from log entries * * @param array $logEntries Log entries * @return float Average execution time in milliseconds * @since 1.0.0 */ private function calculateAverageExecutionTime(array $logEntries): float { if (empty($logEntries)) { return 0.0; } $totalTime = array_sum(array_column($logEntries, 'execution_time')); return $totalTime / count($logEntries); } /** * Calculate cache efficiency based on invalidation patterns * * @return float Efficiency percentage * @since 1.0.0 */ private function calculateCacheEfficiency(): float { $cacheMetrics = $this->cacheManager->getMetrics(); $hitRate = $cacheMetrics['hit_rate'] ?? 0; // Adjust hit rate based on invalidation frequency $recentInvalidations = count(array_slice($this->invalidationLog, -10)); $invalidationPenalty = min($recentInvalidations * 2, 20); // Max 20% penalty return max(0, $hitRate - $invalidationPenalty); } /** * Remove circular dependencies from graph * * @return void * @since 1.0.0 */ private function removeCircularDependencies(): void { // Simple cycle detection and removal // This is a basic implementation - could be enhanced with more sophisticated algorithms foreach ($this->dependencyGraph as $key => $dependency) { if ($this->hasCircularDependency($key, $dependency['dependencies'])) { // Remove the circular dependency $this->dependencyGraph[$key]['dependencies'] = array_filter( $dependency['dependencies'], fn($dep) => !$this->createsCycle($key, $dep) ); } } } /** * Check if adding a dependency creates a cycle * * @param string $key Source key * @param string $dependency Target dependency * @return bool True if cycle detected * @since 1.0.0 */ private function createsCycle(string $key, string $dependency): bool { $visited = []; return $this->detectCycle($dependency, $key, $visited); } /** * Detect cycle in dependency graph * * @param string $current Current node * @param string $target Target node * @param array $visited Visited nodes * @return bool True if cycle found * @since 1.0.0 */ private function detectCycle(string $current, string $target, array &$visited): bool { if ($current === $target) { return true; } if (in_array($current, $visited)) { return false; } $visited[] = $current; if (isset($this->dependencyGraph[$current])) { foreach ($this->dependencyGraph[$current]['dependencies'] as $dep) { if ($this->detectCycle($dep, $target, $visited)) { return true; } } } return false; } /** * Check for circular dependency * * @param string $key Source key * @param array $dependencies Dependencies * @return bool True if circular dependency exists * @since 1.0.0 */ private function hasCircularDependency(string $key, array $dependencies): bool { foreach ($dependencies as $dep) { if ($this->createsCycle($key, $dep)) { return true; } } return false; } /** * Optimize dependency chains by flattening * * @return void * @since 1.0.0 */ private function optimizeDependencyChains(): void { // Flatten long dependency chains to improve performance foreach ($this->dependencyGraph as $key => &$dependency) { $flattenedDeps = $this->flattenDependencyChain($dependency['dependencies']); $dependency['dependencies'] = array_unique($flattenedDeps); } } /** * Flatten dependency chain recursively * * @param array $dependencies Dependencies to flatten * @param int $depth Current depth * @return array Flattened dependencies * @since 1.0.0 */ private function flattenDependencyChain(array $dependencies, int $depth = 0): array { if ($depth > 5) { // Prevent infinite recursion return $dependencies; } $flattened = []; foreach ($dependencies as $dep) { $flattened[] = $dep; if (isset($this->dependencyGraph[$dep])) { $subDeps = $this->flattenDependencyChain( $this->dependencyGraph[$dep]['dependencies'], $depth + 1 ); $flattened = array_merge($flattened, $subDeps); } } return array_unique($flattened); } /** * Remove unused dependencies from graph * * @return void * @since 1.0.0 */ private function removeUnusedDependencies(): void { $usedKeys = []; // Collect all referenced keys foreach ($this->dependencyGraph as $key => $dependency) { $usedKeys = array_merge($usedKeys, $dependency['dependencies']); } $usedKeys = array_unique($usedKeys); // Remove dependencies that are never referenced $this->dependencyGraph = array_filter( $this->dependencyGraph, fn($key) => in_array($key, $usedKeys), ARRAY_FILTER_USE_KEY ); } /** * Get approximate cache size * * @return int Cache size estimate * @since 1.0.0 */ private function getCacheSize(): int { // This is a simplified cache size estimation // In a real implementation, you'd query the actual cache storage return count($this->dependencyGraph) * 10; // Rough estimate } /** * Cleanup old invalidation log entries * * @return void * @since 1.0.0 */ public function cleanupInvalidationLog(): void { $cutoffTime = time() - (7 * 24 * 3600); // Keep 7 days $this->invalidationLog = array_filter( $this->invalidationLog, fn($entry) => $entry['timestamp'] > $cutoffTime ); } }