PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.6.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.6.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 1.11.0 All 47 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 2.6.0, at includes/api/traits/trait-api-cache.php

346 lines 10.8 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. The prefix is stored with a trailing underscore
221 // so the "_transient_{$this->cache_prefix}%" LIKE patterns above keep a
222 // word boundary; implode() supplies the separator here, so trim it off
223 // rather than emitting keys like `thinkrank_seo_analytics__totals_<md5>`.
224 $key_parts = [
225 rtrim($this->cache_prefix, '_'),
226 $endpoint,
227 md5(wp_json_encode($params))
228 ];
229
230 if ($user_id !== null) {
231 $key_parts[] = "user_{$user_id}";
232 }
233
234 return implode('_', $key_parts);
235 }
236
237 /**
238 * Set cache duration for this endpoint
239 *
240 * @since 1.0.0
241 *
242 * @param int $duration Cache duration in seconds
243 * @return void
244 */
245 protected function set_cache_duration(int $duration): void {
246 $this->default_cache_duration = $duration;
247 }
248
249 /**
250 * Set cache prefix for this endpoint
251 *
252 * @since 1.0.0
253 *
254 * @param string $prefix Cache key prefix
255 * @return void
256 */
257 protected function set_cache_prefix(string $prefix): void {
258 $this->cache_prefix = rtrim($prefix, '_') . '_';
259 }
260
261 /**
262 * Check if caching is enabled
263 *
264 * @since 1.0.0
265 *
266 * @return bool True if caching is enabled
267 */
268 protected function is_caching_enabled(): bool {
269 // Allow disabling cache via constant or filter
270 if (defined('THINKRANK_DISABLE_API_CACHE') && THINKRANK_DISABLE_API_CACHE) {
271 return false;
272 }
273
274 return apply_filters('thinkrank_api_cache_enabled', true);
275 }
276
277 /**
278 * Wrap endpoint response with caching
279 *
280 * @since 1.0.0
281 *
282 * @param string $endpoint Endpoint identifier
283 * @param callable $callback Callback to generate response
284 * @param array $params Request parameters
285 * @param int|null $duration Cache duration
286 * @param int|null $user_id User ID
287 * @return array Response data with cache metadata
288 */
289 protected function cached_response(
290 string $endpoint,
291 callable $callback,
292 array $params = [],
293 ?int $duration = null,
294 ?int $user_id = null
295 ): array {
296 if (!$this->is_caching_enabled()) {
297 $response = call_user_func($callback);
298 return array_merge($response, ['cached' => false]);
299 }
300
301 // Try to get cached response
302 $cached = $this->get_cached_response($endpoint, $params, $user_id);
303
304 if ($cached !== null) {
305 return array_merge($cached['data'], [
306 'cached' => true,
307 'cached_at' => $cached['cached_at']
308 ]);
309 }
310
311 // Generate fresh response
312 $response = call_user_func($callback);
313
314 // Cache the response — but never cache a failure payload. Caching
315 // `success => false` / error responses pins a transient upstream
316 // failure (e.g. a momentary Google API error) for the full TTL, which
317 // shows up to users as screens that stay empty long after the
318 // underlying issue resolved.
319 if ($this->is_cacheable_response($response)) {
320 $this->set_cached_response($endpoint, $response, $params, $duration, $user_id);
321 }
322
323 return array_merge($response, ['cached' => false]);
324 }
325
326 /**
327 * Whether a response payload represents a success that is safe to cache.
328 *
329 * @since 1.0.0
330 *
331 * @param array $response Response payload from the endpoint callback
332 * @return bool True when the payload should be cached
333 */
334 private function is_cacheable_response(array $response): bool {
335 if (array_key_exists('success', $response) && $response['success'] === false) {
336 return false;
337 }
338
339 if (!empty($response['error'])) {
340 return false;
341 }
342
343 return true;
344 }
345 }
346