PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.31.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.31.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 / api / traits / trait-api-cache.php

trait-api-cache.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.31.0, at includes/api/traits/trait-api-cache.php

343 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * API Cache Trait
4 *
5 * Provides consistent caching functionality for API endpoints to improve performance.
6 * Implements WordPress transient caching with automatic cache key generation,
7 * expiration handling, and cache invalidation patterns.
8 *
9 * @package ThinkRank
10 * @subpackage API
11 * @since 1.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\API\Traits;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * API Cache Trait
25 *
26 * Provides caching functionality for API endpoints using WordPress transients.
27 * Includes automatic cache key generation, TTL management, and cache invalidation.
28 *
29 * @since 1.0.0
30 */
31 trait API_Cache {
32
33 /**
34 * Default cache duration in seconds (15 minutes)
35 *
36 * @since 1.0.0
37 * @var int
38 */
39 private int $default_cache_duration = 900;
40
41 /**
42 * Cache key prefix for this endpoint
43 *
44 * @since 1.0.0
45 * @var string
46 */
47 private string $cache_prefix = 'thinkrank_api_';
48
49 /**
50 * Get cached response
51 *
52 * @since 1.0.0
53 *
54 * @param string $endpoint Endpoint identifier
55 * @param array $params Request parameters for cache key generation
56 * @param int|null $user_id User ID for user-specific caching
57 * @return array|null Cached data or null if not found/expired
58 */
59 protected function get_cached_response(string $endpoint, array $params = [], ?int $user_id = null): ?array {
60 $cache_key = $this->generate_cache_key($endpoint, $params, $user_id);
61
62 $cached_data = get_transient($cache_key);
63
64 if (false === $cached_data) {
65 return null;
66 }
67
68 // Verify cache structure and expiration
69 if (!is_array($cached_data) || !isset($cached_data['data'], $cached_data['cached_at'])) {
70 delete_transient($cache_key);
71 return null;
72 }
73
74 return $cached_data;
75 }
76
77 /**
78 * Set cached response
79 *
80 * @since 1.0.0
81 *
82 * @param string $endpoint Endpoint identifier
83 * @param array $data Data to cache
84 * @param array $params Request parameters for cache key generation
85 * @param int|null $duration Cache duration in seconds (null for default)
86 * @param int|null $user_id User ID for user-specific caching
87 * @return bool True on success
88 */
89 protected function set_cached_response(
90 string $endpoint,
91 array $data,
92 array $params = [],
93 ?int $duration = null,
94 ?int $user_id = null
95 ): bool {
96 $cache_key = $this->generate_cache_key($endpoint, $params, $user_id);
97 $duration = $duration ?? $this->default_cache_duration;
98
99 $current_time = time();
100 $cache_data = [
101 'data' => $data,
102 'cached_at' => current_time('mysql'),
103 'expires_at' => gmdate('Y-m-d H:i:s', $current_time + $duration),
104 'cache_key' => $cache_key,
105 'endpoint' => $endpoint,
106 'user_id' => $user_id
107 ];
108
109 return set_transient($cache_key, $cache_data, $duration);
110 }
111
112 /**
113 * Delete cached response
114 *
115 * @since 1.0.0
116 *
117 * @param string $endpoint Endpoint identifier
118 * @param array $params Request parameters for cache key generation
119 * @param int|null $user_id User ID for user-specific caching
120 * @return bool True on success
121 */
122 protected function delete_cached_response(string $endpoint, array $params = [], ?int $user_id = null): bool {
123 $cache_key = $this->generate_cache_key($endpoint, $params, $user_id);
124 return delete_transient($cache_key);
125 }
126
127 /**
128 * Invalidate cache by pattern
129 *
130 * @since 1.0.0
131 *
132 * @param string $pattern Cache key pattern (supports wildcards)
133 * @return int Number of cache entries deleted
134 */
135 protected function invalidate_cache_pattern(string $pattern): int {
136 global $wpdb;
137
138 $deleted = 0;
139 $pattern = str_replace('*', '%', $pattern);
140
141 // Get matching transient keys
142 $transient_pattern = "_transient_{$pattern}";
143 $cache_prefix_pattern = "_transient_{$this->cache_prefix}%";
144
145 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache invalidation requires direct database access
146 $transients = $wpdb->get_col(
147 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- wpdb->options is WordPress core table
148 $wpdb->prepare(
149 "SELECT option_name FROM {$wpdb->options}
150 WHERE option_name LIKE %s
151 AND option_name LIKE %s",
152 $transient_pattern,
153 $cache_prefix_pattern
154 )
155 );
156
157 foreach ($transients as $transient) {
158 $key = str_replace('_transient_', '', $transient);
159 if (delete_transient($key)) {
160 $deleted++;
161 }
162 }
163
164 return $deleted;
165 }
166
167 /**
168 * Get cache statistics
169 *
170 * @since 1.0.0
171 *
172 * @return array Cache statistics
173 */
174 protected function get_cache_stats(): array {
175 global $wpdb;
176
177 // Count total cache entries for this endpoint
178 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache statistics require direct database access
179 $total_entries = $wpdb->get_var(
180 $wpdb->prepare(
181 "SELECT COUNT(*) FROM {$wpdb->options}
182 WHERE option_name LIKE %s",
183 "_transient_{$this->cache_prefix}%"
184 )
185 );
186
187 // Get cache size (approximate)
188 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache size calculation requires direct database access
189 $cache_size = $wpdb->get_var(
190 $wpdb->prepare(
191 "SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options}
192 WHERE option_name LIKE %s",
193 "_transient_{$this->cache_prefix}%"
194 )
195 );
196
197 return [
198 'total_entries' => (int) $total_entries,
199 'cache_size_bytes' => (int) $cache_size,
200 'cache_size_mb' => round((int) $cache_size / 1024 / 1024, 2),
201 'default_duration' => $this->default_cache_duration,
202 'cache_prefix' => $this->cache_prefix
203 ];
204 }
205
206 /**
207 * Generate cache key
208 *
209 * @since 1.0.0
210 *
211 * @param string $endpoint Endpoint identifier
212 * @param array $params Request parameters
213 * @param int|null $user_id User ID for user-specific caching
214 * @return string Generated cache key
215 */
216 private function generate_cache_key(string $endpoint, array $params = [], ?int $user_id = null): string {
217 // Sort parameters for consistent key generation
218 ksort($params);
219
220 // Build key components
221 $key_parts = [
222 $this->cache_prefix,
223 $endpoint,
224 md5(wp_json_encode($params))
225 ];
226
227 if ($user_id !== null) {
228 $key_parts[] = "user_{$user_id}";
229 }
230
231 return implode('_', $key_parts);
232 }
233
234 /**
235 * Set cache duration for this endpoint
236 *
237 * @since 1.0.0
238 *
239 * @param int $duration Cache duration in seconds
240 * @return void
241 */
242 protected function set_cache_duration(int $duration): void {
243 $this->default_cache_duration = $duration;
244 }
245
246 /**
247 * Set cache prefix for this endpoint
248 *
249 * @since 1.0.0
250 *
251 * @param string $prefix Cache key prefix
252 * @return void
253 */
254 protected function set_cache_prefix(string $prefix): void {
255 $this->cache_prefix = rtrim($prefix, '_') . '_';
256 }
257
258 /**
259 * Check if caching is enabled
260 *
261 * @since 1.0.0
262 *
263 * @return bool True if caching is enabled
264 */
265 protected function is_caching_enabled(): bool {
266 // Allow disabling cache via constant or filter
267 if (defined('THINKRANK_DISABLE_API_CACHE') && THINKRANK_DISABLE_API_CACHE) {
268 return false;
269 }
270
271 return apply_filters('thinkrank_api_cache_enabled', true);
272 }
273
274 /**
275 * Wrap endpoint response with caching
276 *
277 * @since 1.0.0
278 *
279 * @param string $endpoint Endpoint identifier
280 * @param callable $callback Callback to generate response
281 * @param array $params Request parameters
282 * @param int|null $duration Cache duration
283 * @param int|null $user_id User ID
284 * @return array Response data with cache metadata
285 */
286 protected function cached_response(
287 string $endpoint,
288 callable $callback,
289 array $params = [],
290 ?int $duration = null,
291 ?int $user_id = null
292 ): array {
293 if (!$this->is_caching_enabled()) {
294 $response = call_user_func($callback);
295 return array_merge($response, ['cached' => false]);
296 }
297
298 // Try to get cached response
299 $cached = $this->get_cached_response($endpoint, $params, $user_id);
300
301 if ($cached !== null) {
302 return array_merge($cached['data'], [
303 'cached' => true,
304 'cached_at' => $cached['cached_at']
305 ]);
306 }
307
308 // Generate fresh response
309 $response = call_user_func($callback);
310
311 // Cache the response — but never cache a failure payload. Caching
312 // `success => false` / error responses pins a transient upstream
313 // failure (e.g. a momentary Google API error) for the full TTL, which
314 // shows up to users as screens that stay empty long after the
315 // underlying issue resolved.
316 if ($this->is_cacheable_response($response)) {
317 $this->set_cached_response($endpoint, $response, $params, $duration, $user_id);
318 }
319
320 return array_merge($response, ['cached' => false]);
321 }
322
323 /**
324 * Whether a response payload represents a success that is safe to cache.
325 *
326 * @since 1.0.0
327 *
328 * @param array $response Response payload from the endpoint callback
329 * @return bool True when the payload should be cached
330 */
331 private function is_cacheable_response(array $response): bool {
332 if (array_key_exists('success', $response) && $response['success'] === false) {
333 return false;
334 }
335
336 if (!empty($response['error'])) {
337 return false;
338 }
339
340 return true;
341 }
342 }
343