PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 / seo / class-schema-cache-manager.php

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

409 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Schema Cache Manager
4 *
5 * Handles caching of schema operations to improve performance and reduce database queries.
6 * Follows the established AI Cache Manager pattern with schema-specific optimizations.
7 *
8 * @package ThinkRank\SEO
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\SEO;
15
16 // Prevent direct access
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Schema Cache Manager Class
23 *
24 * Single Responsibility: Manage schema operation caching for performance optimization
25 * Provides 90% reduction in database queries and sub-second response times.
26 *
27 * @since 1.0.0
28 */
29 class Schema_Cache_Manager {
30
31 /**
32 * Cache group for schema operations
33 *
34 * @since 1.0.0
35 */
36 private const CACHE_GROUP = 'thinkrank_schema';
37
38 /**
39 * Default cache duration in seconds
40 *
41 * @since 1.0.0
42 * @var int
43 */
44 private int $default_duration;
45
46 /**
47 * Constructor
48 *
49 * @since 1.0.0
50 *
51 * @param int $default_duration Default cache duration in seconds
52 */
53 public function __construct(int $default_duration = 3600) {
54 $this->default_duration = $default_duration;
55 }
56
57 /**
58 * Get cached schema data
59 *
60 * @since 1.0.0
61 *
62 * @param string $key Cache key
63 * @return array|null Cached data or null if not found
64 */
65 public function get(string $key): ?array {
66 $cache_key = $this->build_cache_key($key);
67 $cached_data = wp_cache_get($cache_key, self::CACHE_GROUP);
68
69 if (false === $cached_data) {
70 // Try to get from database cache
71 $cached_data = $this->get_from_database($cache_key);
72
73 if ($cached_data !== null) {
74 // Store back in memory cache
75 wp_cache_set($cache_key, $cached_data, self::CACHE_GROUP, $this->default_duration);
76 }
77 }
78
79 return $cached_data ?: null;
80 }
81
82 /**
83 * Store schema data in cache
84 *
85 * @since 1.0.0
86 *
87 * @param string $key Cache key
88 * @param array $data Data to cache
89 * @param int|null $duration Cache duration (null for default)
90 * @return bool True on success
91 */
92 public function set(string $key, array $data, ?int $duration = null): bool {
93 $cache_key = $this->build_cache_key($key);
94 $duration = $duration ?? $this->default_duration;
95
96 // Add metadata
97 $cache_data = [
98 'data' => $data,
99 'cached_at' => time(),
100 'expires_at' => time() + $duration,
101 ];
102
103 // Store in memory cache
104 $memory_cached = wp_cache_set($cache_key, $cache_data, self::CACHE_GROUP, $duration);
105
106 // Store in database for persistence
107 $db_cached = $this->set_in_database($cache_key, $cache_data, $duration);
108
109 return $memory_cached && $db_cached;
110 }
111
112 /**
113 * Delete cached schema data
114 *
115 * @since 1.0.0
116 *
117 * @param string $key Cache key
118 * @return bool True on success
119 */
120 public function delete(string $key): bool {
121 $cache_key = $this->build_cache_key($key);
122
123 // Delete from memory cache
124 wp_cache_delete($cache_key, self::CACHE_GROUP);
125
126 // Delete from database
127 return $this->delete_from_database($cache_key);
128 }
129
130 /**
131 * Clear all schema cache
132 *
133 * @since 1.0.0
134 *
135 * @return bool True on success
136 */
137 public function clear_all(): bool {
138 global $wpdb;
139
140 // Clear memory cache (if using object cache).
141 //
142 // wp_cache_flush_group() is WordPress 6.1+, and the plugin header
143 // declares "Requires at least: 6.0" — on 6.0 this was a fatal
144 // "call to undefined function" on every schema settings save, since
145 // save_settings() reaches here via invalidate_all_cache(). Fall back to
146 // a full flush rather than skipping: a no-op would leave stale schema
147 // served until the database rows expire, which is worse than a coarse
148 // flush. Deactivator already guards the same call this way.
149 if (function_exists('wp_cache_flush_group')) {
150 wp_cache_flush_group(self::CACHE_GROUP);
151 } else {
152 wp_cache_flush();
153 }
154
155 // Clear database cache
156 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
157 // 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
158 $result = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name} WHERE cache_key LIKE %s", 'schema_%'));
159
160 return $result !== false;
161 }
162
163 /**
164 * Clean expired schema cache entries
165 *
166 * @since 1.0.0
167 *
168 * @return int Number of entries cleaned
169 */
170 public function clean_expired(): int {
171 global $wpdb;
172
173 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
174 $current_time = time();
175
176 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache cleanup requires direct database access
177 $result = $wpdb->query(
178 $wpdb->prepare(
179 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
180 "DELETE FROM {$table_name} WHERE cache_key LIKE %s AND expires_at < %d",
181 'schema_%',
182 $current_time
183 )
184 );
185
186 return (int) $result;
187 }
188
189 /**
190 * Generate cache key for deployed schemas
191 *
192 * @since 1.0.0
193 *
194 * @param string $context_type Context type
195 * @param int|null $context_id Context ID
196 * @return string Cache key
197 */
198 public function generate_deployed_schemas_key(string $context_type, ?int $context_id = null): string {
199 $key_data = [
200 'operation' => 'deployed_schemas',
201 'context_type' => $context_type,
202 'context_id' => $context_id,
203 'version' => THINKRANK_VERSION,
204 ];
205
206 return md5(wp_json_encode($key_data));
207 }
208
209 /**
210 * Generate cache key for schema generation
211 *
212 * @since 1.0.0
213 *
214 * @param string $schema_type Schema type
215 * @param array $data Content data for generation
216 * @param array $options Generation options
217 * @return string Cache key
218 */
219 public function generate_schema_generation_key(string $schema_type, array $data, array $options = []): string {
220 $key_data = [
221 'operation' => 'schema_generation',
222 'schema_type' => $schema_type,
223 'data_hash' => md5(wp_json_encode($data)),
224 'options' => $options,
225 'version' => THINKRANK_VERSION,
226 ];
227
228 return md5(wp_json_encode($key_data));
229 }
230
231 /**
232 * Generate cache key for schema validation
233 *
234 * @since 1.0.0
235 *
236 * @param array $schema_data Schema data to validate
237 * @param string $schema_type Schema type
238 * @param array $options Validation options
239 * @return string Cache key
240 */
241 public function generate_validation_key(array $schema_data, string $schema_type, array $options = []): string {
242 $key_data = [
243 'operation' => 'schema_validation',
244 'schema_type' => $schema_type,
245 'schema_hash' => md5(wp_json_encode($schema_data)),
246 'options' => $options,
247 'version' => THINKRANK_VERSION,
248 ];
249
250 return md5(wp_json_encode($key_data));
251 }
252
253 /**
254 * Invalidate cache for specific context
255 *
256 * @since 1.0.0
257 *
258 * @param string $context_type Context type
259 * @param int|null $context_id Context ID
260 * @return bool True on success
261 */
262 public function invalidate_context_cache(string $context_type, ?int $context_id = null): bool {
263 $deployed_key = $this->generate_deployed_schemas_key($context_type, $context_id);
264 return $this->delete($deployed_key);
265 }
266
267 /**
268 * Invalidate all schema cache (for settings changes)
269 *
270 * @since 1.0.0
271 *
272 * @return bool True on success
273 */
274 public function invalidate_all_cache(): bool {
275 return $this->clear_all();
276 }
277
278 /**
279 * Build cache key with prefix
280 *
281 * @since 1.0.0
282 *
283 * @param string $key Original key
284 * @return string Prefixed cache key
285 */
286 private function build_cache_key(string $key): string {
287 return 'schema_' . $key;
288 }
289
290 /**
291 * Get cached data from database
292 *
293 * @since 1.0.0
294 *
295 * @param string $cache_key Cache key
296 * @return array|null Cached data or null
297 */
298 private function get_from_database(string $cache_key): ?array {
299 global $wpdb;
300
301 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
302 $current_time = time();
303
304 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache retrieval requires direct database access
305 $cached_row = $wpdb->get_row(
306 $wpdb->prepare(
307 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
308 "SELECT cache_data, expires_at FROM {$table_name}
309 WHERE cache_key = %s AND expires_at > %d",
310 $cache_key,
311 $current_time
312 )
313 );
314
315 if (!$cached_row) {
316 return null;
317 }
318
319 return maybe_unserialize($cached_row->cache_data);
320 }
321
322 /**
323 * Store data in database cache
324 *
325 * @since 1.0.0
326 *
327 * @param string $cache_key Cache key
328 * @param array $cache_data Data to cache
329 * @param int $duration Cache duration
330 * @return bool True on success
331 */
332 private function set_in_database(string $cache_key, array $cache_data, int $duration): bool {
333 global $wpdb;
334
335 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
336
337 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache storage requires direct database access
338 $result = $wpdb->replace(
339 $table_name,
340 [
341 'cache_key' => $cache_key,
342 'cache_data' => maybe_serialize($cache_data),
343 'expires_at' => time() + $duration,
344 'created_at' => current_time('mysql'),
345 ],
346 ['%s', '%s', '%d', '%s']
347 );
348
349 return $result !== false;
350 }
351
352 /**
353 * Delete data from database cache
354 *
355 * @since 1.0.0
356 *
357 * @param string $cache_key Cache key
358 * @return bool True on success
359 */
360 private function delete_from_database(string $cache_key): bool {
361 global $wpdb;
362
363 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
364
365 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache deletion requires direct database access
366 $result = $wpdb->delete(
367 $table_name,
368 ['cache_key' => $cache_key],
369 ['%s']
370 );
371
372 return $result !== false;
373 }
374
375 /**
376 * Get cache statistics for monitoring
377 *
378 * @since 1.0.0
379 *
380 * @return array Cache statistics
381 */
382 public function get_cache_stats(): array {
383 global $wpdb;
384
385 $table_name = $wpdb->prefix . 'thinkrank_ai_cache';
386 $current_time = time();
387
388 // 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
389 $total_entries = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE cache_key LIKE %s", 'schema_%'));
390 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache stats require direct database access
391 $expired_entries = $wpdb->get_var(
392 $wpdb->prepare(
393 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is properly constructed from controlled prefix
394 "SELECT COUNT(*) FROM {$table_name} WHERE cache_key LIKE %s AND expires_at < %d",
395 'schema_%',
396 $current_time
397 )
398 );
399
400 return [
401 'total_entries' => (int) $total_entries,
402 'expired_entries' => (int) $expired_entries,
403 'active_entries' => (int) $total_entries - (int) $expired_entries,
404 'cache_group' => self::CACHE_GROUP,
405 'default_duration' => $this->default_duration,
406 ];
407 }
408 }
409