PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / trunk
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO vtrunk
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 1.11.0 All 47 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 trunk, at includes/ai/class-cache-manager.php

311 lines 10.7 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() is WP 6.1+ and the plugin supports 6.0; see
127 // Schema_Cache_Manager::clear_all() for the reasoning behind the
128 // fallback rather than a skip.
129 if (function_exists('wp_cache_flush_group')) {
130 wp_cache_flush_group(self::CACHE_GROUP);
131 } else {
132 wp_cache_flush();
133 }
134
135 // Clear database cache
136 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
137 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix, cache clearing requires direct database access
138 $result = $wpdb->query("DELETE FROM {$table_name}");
139
140 return $result !== false;
141 }
142
143 /**
144 * Clean expired cache entries
145 *
146 * @return int Number of entries cleaned
147 */
148 public function clean_expired(): int {
149 global $wpdb;
150
151 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
152 $current_time = time();
153
154 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache cleanup requires direct database access
155 $result = $wpdb->query(
156 $wpdb->prepare(
157 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
158 "DELETE FROM {$table_name} WHERE expires_at < %d",
159 $current_time
160 )
161 );
162
163 return (int) $result;
164 }
165
166 /**
167 * Get cache statistics
168 *
169 * @return array Cache statistics
170 */
171 public function get_stats(): array {
172 global $wpdb;
173
174 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
175 $current_time = time();
176
177 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix, cache stats require direct database access
178 $total_entries = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
179 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache stats require direct database access
180 $expired_entries = $wpdb->get_var(
181 $wpdb->prepare(
182 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
183 "SELECT COUNT(*) FROM {$table_name} WHERE expires_at < %d",
184 $current_time
185 )
186 );
187
188 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache stats require direct database access
189 $cache_size = $wpdb->get_var(
190 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
191 "SELECT SUM(LENGTH(cache_data)) FROM {$table_name}"
192 );
193
194 return [
195 'total_entries' => (int) $total_entries,
196 'active_entries' => (int) $total_entries - (int) $expired_entries,
197 'expired_entries' => (int) $expired_entries,
198 'cache_size_bytes' => (int) $cache_size,
199 'cache_size_mb' => round((int) $cache_size / 1024 / 1024, 2),
200 ];
201 }
202
203 /**
204 * Generate cache key for content
205 *
206 * @param string $content Content to generate key for
207 * @param array $options Additional options affecting the key
208 * @return string Cache key
209 */
210 public function generate_content_key(string $content, array $options = []): string {
211 $key_data = [
212 'content_hash' => md5($content),
213 'options' => $options,
214 'version' => THINKRANK_VERSION,
215 ];
216
217 return md5(wp_json_encode($key_data));
218 }
219
220 /**
221 * Build cache key with prefix
222 *
223 * @param string $key Original key
224 * @return string Prefixed cache key
225 */
226 private function build_cache_key(string $key): string {
227 return 'ai_response_' . $key;
228 }
229
230 /**
231 * Get cached data from database
232 *
233 * @param string $cache_key Cache key
234 * @return array|null Cached data or null
235 */
236 private function get_from_database(string $cache_key): ?array {
237 global $wpdb;
238
239 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
240 $current_time = time();
241
242 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache retrieval requires direct database access
243 $cached_row = $wpdb->get_row(
244 $wpdb->prepare(
245 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
246 "SELECT cache_data, expires_at FROM {$table_name}
247 WHERE cache_key = %s AND expires_at > %d",
248 $cache_key,
249 $current_time
250 )
251 );
252
253 if (!$cached_row) {
254 return null;
255 }
256
257 $cache_data = maybe_unserialize($cached_row->cache_data);
258
259 return is_array($cache_data) ? $cache_data : null;
260 }
261
262 /**
263 * Store data in database cache
264 *
265 * @param string $cache_key Cache key
266 * @param array $cache_data Data to cache
267 * @param int $duration Cache duration
268 * @return bool True on success
269 */
270 private function set_in_database(string $cache_key, array $cache_data, int $duration): bool {
271 global $wpdb;
272
273 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
274
275 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache storage requires direct database access
276 $result = $wpdb->replace(
277 $table_name,
278 [
279 'cache_key' => $cache_key,
280 'cache_data' => maybe_serialize($cache_data),
281 'expires_at' => time() + $duration,
282 'created_at' => current_time('mysql'),
283 ],
284 ['%s', '%s', '%d', '%s']
285 );
286
287 return $result !== false;
288 }
289
290 /**
291 * Delete data from database cache
292 *
293 * @param string $cache_key Cache key
294 * @return bool True on success
295 */
296 private function delete_from_database(string $cache_key): bool {
297 global $wpdb;
298
299 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
300
301 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache deletion requires direct database access
302 $result = $wpdb->delete(
303 $table_name,
304 ['cache_key' => $cache_key],
305 ['%s']
306 );
307
308 return $result !== false;
309 }
310 }
311