PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.1.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / ai / class-cache-manager.php

class-cache-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.1.0, at includes/ai/class-cache-manager.php

304 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Cache Manager
4 *
5 * Handles caching of AI responses to reduce API costs
6 *
7 * @package ThinkRank\AI
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\AI;
14
15 // Prevent direct access
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20 /**
21 * Cache Manager Class
22 *
23 * Single Responsibility: Manage AI response caching
24 *
25 * @since 1.0.0
26 */
27 class Cache_Manager {
28
29 /**
30 * Cache group for AI responses
31 */
32 private const CACHE_GROUP = 'thinkrank_ai';
33
34 /**
35 * Default cache duration in seconds
36 *
37 * @var int
38 */
39 private int $default_duration;
40
41 /**
42 * Constructor
43 *
44 * @param int $default_duration Default cache duration
45 */
46 public function __construct(int $default_duration = 3600) {
47 $this->default_duration = $default_duration;
48 }
49
50 /**
51 * Get cached response
52 *
53 * @param string $key Cache key
54 * @return array|null Cached data or null if not found
55 */
56 public function get(string $key): ?array {
57 $cache_key = $this->build_cache_key($key);
58 $cached_data = wp_cache_get($cache_key, self::CACHE_GROUP);
59
60 if (false === $cached_data) {
61 // Try to get from database cache
62 $cached_data = $this->get_from_database($cache_key);
63
64 if ($cached_data !== null) {
65 // Store back in memory cache
66 wp_cache_set($cache_key, $cached_data, self::CACHE_GROUP, $this->default_duration);
67 }
68 }
69
70 return $cached_data ?: null;
71 }
72
73 /**
74 * Store response in cache
75 *
76 * @param string $key Cache key
77 * @param array $data Data to cache
78 * @param int|null $duration Cache duration (null for default)
79 * @return bool True on success
80 */
81 public function set(string $key, array $data, ?int $duration = null): bool {
82 $cache_key = $this->build_cache_key($key);
83 $duration = $duration ?? $this->default_duration;
84
85 // Add metadata
86 $cache_data = [
87 'data' => $data,
88 'cached_at' => time(),
89 'expires_at' => time() + $duration,
90 ];
91
92 // Store in memory cache
93 $memory_cached = wp_cache_set($cache_key, $cache_data, self::CACHE_GROUP, $duration);
94
95 // Store in database for persistence
96 $db_cached = $this->set_in_database($cache_key, $cache_data, $duration);
97
98 return $memory_cached && $db_cached;
99 }
100
101 /**
102 * Delete cached response
103 *
104 * @param string $key Cache key
105 * @return bool True on success
106 */
107 public function delete(string $key): bool {
108 $cache_key = $this->build_cache_key($key);
109
110 // Delete from memory cache
111 wp_cache_delete($cache_key, self::CACHE_GROUP);
112
113 // Delete from database
114 return $this->delete_from_database($cache_key);
115 }
116
117 /**
118 * Clear all AI cache
119 *
120 * @return bool True on success
121 */
122 public function clear_all(): bool {
123 global $wpdb;
124
125 // Clear memory cache (if using object cache)
126 wp_cache_flush_group(self::CACHE_GROUP);
127
128 // Clear database cache
129 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
130 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is properly constructed from controlled prefix, cache clearing requires direct database access
131 $result = $wpdb->query("DELETE FROM {$table_name}");
132
133 return $result !== false;
134 }
135
136 /**
137 * Clean expired cache entries
138 *
139 * @return int Number of entries cleaned
140 */
141 public function clean_expired(): int {
142 global $wpdb;
143
144 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
145 $current_time = time();
146
147 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache cleanup requires direct database access
148 $result = $wpdb->query(
149 $wpdb->prepare(
150 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly constructed from controlled prefix
151 "DELETE FROM {$table_name} WHERE expires_at < %d",
152 $current_time
153 )
154 );
155
156 return (int) $result;
157 }
158
159 /**
160 * Get cache statistics
161 *
162 * @return array Cache statistics
163 */
164 public function get_stats(): array {
165 global $wpdb;
166
167 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
168 $current_time = time();
169
170 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name is properly constructed from controlled prefix, cache stats require direct database access
171 $total_entries = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
172 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache stats require direct database access
173 $expired_entries = $wpdb->get_var(
174 $wpdb->prepare(
175 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly constructed from controlled prefix
176 "SELECT COUNT(*) FROM {$table_name} WHERE expires_at < %d",
177 $current_time
178 )
179 );
180
181 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache stats require direct database access
182 $cache_size = $wpdb->get_var(
183 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly constructed from controlled prefix
184 "SELECT SUM(LENGTH(cache_data)) FROM {$table_name}"
185 );
186
187 return [
188 'total_entries' => (int) $total_entries,
189 'active_entries' => (int) $total_entries - (int) $expired_entries,
190 'expired_entries' => (int) $expired_entries,
191 'cache_size_bytes' => (int) $cache_size,
192 'cache_size_mb' => round((int) $cache_size / 1024 / 1024, 2),
193 ];
194 }
195
196 /**
197 * Generate cache key for content
198 *
199 * @param string $content Content to generate key for
200 * @param array $options Additional options affecting the key
201 * @return string Cache key
202 */
203 public function generate_content_key(string $content, array $options = []): string {
204 $key_data = [
205 'content_hash' => md5($content),
206 'options' => $options,
207 'version' => THINKRANK_VERSION,
208 ];
209
210 return md5(wp_json_encode($key_data));
211 }
212
213 /**
214 * Build cache key with prefix
215 *
216 * @param string $key Original key
217 * @return string Prefixed cache key
218 */
219 private function build_cache_key(string $key): string {
220 return 'ai_response_' . $key;
221 }
222
223 /**
224 * Get cached data from database
225 *
226 * @param string $cache_key Cache key
227 * @return array|null Cached data or null
228 */
229 private function get_from_database(string $cache_key): ?array {
230 global $wpdb;
231
232 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
233 $current_time = time();
234
235 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache retrieval requires direct database access
236 $cached_row = $wpdb->get_row(
237 $wpdb->prepare(
238 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is properly constructed from controlled prefix
239 "SELECT cache_data, expires_at FROM {$table_name}
240 WHERE cache_key = %s AND expires_at > %d",
241 $cache_key,
242 $current_time
243 )
244 );
245
246 if (!$cached_row) {
247 return null;
248 }
249
250 $cache_data = maybe_unserialize($cached_row->cache_data);
251
252 return is_array($cache_data) ? $cache_data : null;
253 }
254
255 /**
256 * Store data in database cache
257 *
258 * @param string $cache_key Cache key
259 * @param array $cache_data Data to cache
260 * @param int $duration Cache duration
261 * @return bool True on success
262 */
263 private function set_in_database(string $cache_key, array $cache_data, int $duration): bool {
264 global $wpdb;
265
266 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
267
268 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache storage requires direct database access
269 $result = $wpdb->replace(
270 $table_name,
271 [
272 'cache_key' => $cache_key,
273 'cache_data' => maybe_serialize($cache_data),
274 'expires_at' => time() + $duration,
275 'created_at' => current_time('mysql'),
276 ],
277 ['%s', '%s', '%d', '%s']
278 );
279
280 return $result !== false;
281 }
282
283 /**
284 * Delete data from database cache
285 *
286 * @param string $cache_key Cache key
287 * @return bool True on success
288 */
289 private function delete_from_database(string $cache_key): bool {
290 global $wpdb;
291
292 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
293
294 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache deletion requires direct database access
295 $result = $wpdb->delete(
296 $table_name,
297 ['cache_key' => $cache_key],
298 ['%s']
299 );
300
301 return $result !== false;
302 }
303 }
304