# Caching Strategies Skill

> **Technology:** WordPress Transients API and object caching patterns

---

## Purpose

This skill covers caching strategies, transients usage, and cache invalidation for the Comments Press Zone plugin.

---

## WordPress Caching APIs

### Transients (Database-backed cache with expiration)

```php
// Set transient (expires in 1 hour)
set_transient('presszone_comments_stats', $stats_data, HOUR_IN_SECONDS);

// Get transient
$stats = get_transient('presszone_comments_stats');
if ($stats === false) {
    // Cache miss - regenerate data
    $stats = $this->calculate_stats();
    set_transient('presszone_comments_stats', $stats, HOUR_IN_SECONDS);
}

// Delete transient
delete_transient('presszone_comments_stats');
```

### Object Cache (RAM-based, fast, non-persistent by default)

```php
// Set cache (no expiration in default setup)
wp_cache_set('comment_votes_' . $comment_id, $votes, 'presszone_comments');

// Get cache
$votes = wp_cache_get('comment_votes_' . $comment_id, 'presszone_comments');
if ($votes === false) {
    // Cache miss
    $votes = $this->fetch_votes($comment_id);
    wp_cache_set('comment_votes_' . $comment_id, $votes, 'presszone_comments');
}

// Delete cache
wp_cache_delete('comment_votes_' . $comment_id, 'presszone_comments');

// Flush group
wp_cache_flush_group('presszone_comments');
```

---

## Caching Patterns

### Dashboard Stats (Expensive Query)

```php
public function get_dashboard_stats(): array {
    $cache_key = 'presszone_comments_dashboard_stats';
    
    $stats = get_transient($cache_key);
    if ($stats !== false) {
        return $stats;
    }
    
    global $wpdb;
    
    // Expensive aggregation query
    $stats = [
        'total_comments' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->comments}"),
        'total_votes' => (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->prefix}presszone_comments_likes"
        ),
        'active_users' => (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT user_id) 
             FROM {$wpdb->comments} 
             WHERE comment_date > %s",
            gmdate('Y-m-d H:i:s', strtotime('-30 days'))
        )),
    ];
    
    // Cache for 1 hour
    set_transient($cache_key, $stats, HOUR_IN_SECONDS);
    
    return $stats;
}
```

### Comment Votes (Per-Comment Cache)

```php
public function get_comment_votes(int $comment_id): array {
    $cache_key = 'comment_votes_' . $comment_id;
    
    $votes = wp_cache_get($cache_key, 'presszone_comments');
    if ($votes !== false) {
        return $votes;
    }
    
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_likes';
    
    $upvotes = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE comment_id = %d AND type = 'upvote'",
        $comment_id
    ));
    
    $downvotes = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE comment_id = %d AND type = 'downvote'",
        $comment_id
    ));
    
    $votes = [
        'upvotes' => $upvotes,
        'downvotes' => $downvotes,
    ];
    
    // Cache in object cache (no expiration)
    wp_cache_set($cache_key, $votes, 'presszone_comments');
    
    return $votes;
}
```

---

## Cache Invalidation

### Invalidate on Data Change

```php
public function toggle_vote(int $comment_id, string $type): array {
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_likes';
    
    // ... voting logic ...
    
    // Invalidate caches after vote change
    $this->invalidate_vote_cache($comment_id);
    $this->invalidate_dashboard_cache();
    
    return $this->get_comment_votes($comment_id);
}

private function invalidate_vote_cache(int $comment_id): void {
    wp_cache_delete('comment_votes_' . $comment_id, 'presszone_comments');
}

private function invalidate_dashboard_cache(): void {
    delete_transient('presszone_comments_dashboard_stats');
}
```

### Invalidate on Comment Post

```php
public function on_comment_post(int $comment_id): void {
    // Invalidate related caches
    delete_transient('presszone_comments_dashboard_stats');
    delete_transient('presszone_comments_recent_activity');
    
    // Invalidate parent comment cache if this is a reply
    $comment = get_comment($comment_id);
    if ($comment && $comment->comment_parent > 0) {
        wp_cache_delete('comment_replies_' . $comment->comment_parent, 'presszone_comments');
    }
}
```

---

## Cache Expiration Times

```php
// WordPress constants
MINUTE_IN_SECONDS  // 60
HOUR_IN_SECONDS    // 3600
DAY_IN_SECONDS     // 86400
WEEK_IN_SECONDS    // 604800
MONTH_IN_SECONDS   // 2592000
YEAR_IN_SECONDS    // 31536000

// Example usage by data type
set_transient('presszone_comments_stats', $stats, HOUR_IN_SECONDS);        // Stats: 1 hour
set_transient('presszone_comments_settings', $settings, DAY_IN_SECONDS);   // Settings: 1 day
set_transient('presszone_comments_users', $users, 5 * MINUTE_IN_SECONDS);  // User list: 5 min
```

---

## Cache Key Patterns

### Consistent Naming

```php
// Good patterns
"presszone_comments_{feature}_{identifier}"
"presszone_comments_votes_{comment_id}"
"presszone_comments_stats_dashboard"
"presszone_comments_user_{user_id}_reputation"

// Include variables in key when data is specific
$cache_key = sprintf('presszone_comments_comments_%d_page_%d', $post_id, $page);
```

---

## Fragment Caching (Template Parts)

```php
public function render_comment_list(int $post_id): void {
    $cache_key = 'presszone_comments_list_' . $post_id;
    
    $html = get_transient($cache_key);
    if ($html !== false) {
        echo $html;
        return;
    }
    
    ob_start();
    
    // Generate HTML
    $comments = get_comments(['post_id' => $post_id]);
    foreach ($comments as $comment) {
        include PRESSZONE_COMMENTS_PATH . 'templates/partials/comment-item.php';
    }
    
    $html = ob_get_clean();
    
    // Cache for 5 minutes
    set_transient($cache_key, $html, 5 * MINUTE_IN_SECONDS);
    
    echo $html;
}
```

---

## Cache Busting Strategies

### Version-Based Cache Keys

```php
// Include version in cache key
$cache_key = sprintf(
    'presszone_comments_data_%s_%d',
    PRESSZONE_COMMENTS_VERSION,
    $resource_id
);

// Flush all caches on plugin update
public function on_plugin_update(): void {
    global $wpdb;
    
    // Delete all plugin transients
    $wpdb->query(
        "DELETE FROM {$wpdb->options} 
         WHERE option_name LIKE '_transient_presszone_comments_%' 
         OR option_name LIKE '_transient_timeout_presszone_comments_%'"
    );
    
    // Flush object cache group
    wp_cache_flush_group('presszone_comments');
}
```

---

## Cache Warming (Preload)

```php
public function warm_cache(): void {
    // Preload expensive queries
    $this->get_dashboard_stats();
    $this->get_top_commenters();
    $this->get_recent_activity();
}

// Schedule cache warming
add_action('presszone_comments_warm_cache', [$this, 'warm_cache']);

if (!wp_next_scheduled('presszone_comments_warm_cache')) {
    wp_schedule_event(time(), 'hourly', 'presszone_comments_warm_cache');
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Not invalidating cache on data change | Always invalidate after writes |
| Using transients for frequently changing data | Use object cache instead |
| Cache keys without version/identifier | Include version or unique ID |
| No cache expiration | Always set expiration time |
| Caching user-specific data globally | Include user ID in cache key |
| Not checking cache return value | Check `=== false` for cache miss |

---

## When to Cache

**Cache these:**
- Expensive database queries (joins, aggregations)
- External API calls
- Complex calculations
- Dashboard statistics
- List of items (with pagination key)

**Don't cache these:**
- User-specific data (unless keyed by user ID)
- Real-time data (votes, likes if displayed live)
- Frequently changing data
- Small, fast queries (single row by ID)

---

## Testing Checklist

- [ ] Expensive queries cached with transients
- [ ] Cache invalidated on data changes
- [ ] Cache keys unique and descriptive
- [ ] Expiration times appropriate for data type
- [ ] Cache warming scheduled for critical data
- [ ] Cache cleared on plugin update
- [ ] User-specific caches keyed by user ID
- [ ] Cache miss handling implemented
