PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / class / class-mainwp-cache-helper.php

class-mainwp-cache-helper.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at class/class-mainwp-cache-helper.php

530 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Caching Helper
4 *
5 * @package MainWP/Dashboard
6 *
7 * @since 5.5
8 */
9
10 namespace MainWP\Dashboard;
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class MainWP_Cache_Helper
19 *
20 * @package MainWP\Dashboard
21 */
22 final class MainWP_Cache_Helper { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
23
24 const CACHE_TTL = 12 * HOUR_IN_SECONDS;
25
26 const GROUP_VER = '1';
27
28 /**
29 * Cache Groups consts.
30 */
31 const CGR_SITES = 'manage_sites';
32 const CGR_UPDATES = 'updates';
33 const CGR_SYNC_DATA = 'sync_sites';
34
35 /**
36 * Cache hits counter.
37 *
38 * @static
39 *
40 * @var mixed Default null
41 */
42 private static $metrics_list = array();
43
44 /**
45 * Cache hits counter.
46 *
47 * @static
48 *
49 * @var mixed Default null
50 */
51 private static $hits = 0;
52
53 /**
54 * Cache misses counter.
55 *
56 * @static
57 *
58 * @var mixed Default null
59 */
60 private static $misses = 0;
61
62 /**
63 * Cache sets counter.
64 *
65 * @static
66 *
67 * @var mixed Default null
68 */
69 private static $sets = 0;
70
71 /**
72 * Private static variable to hold the single instance of the class.
73 *
74 * @static
75 *
76 * @var mixed Default null
77 */
78 private static $instance = null;
79
80 /**
81 * Method instance()
82 *
83 * Create public static instance.
84 *
85 * @static
86 * @return MainWP_Cache_Helper
87 */
88 public static function instance() {
89 if ( null === self::$instance ) {
90 self::$instance = new self();
91 }
92 return self::$instance;
93 }
94
95 /**
96 * Constructor.
97 *
98 * Run each time the class is called.
99 */
100 public function __construct() {
101 // contructor.
102 }
103
104 /**
105 * Get cache key.
106 *
107 * @param string $key_suffix Key suffix.
108 * @param string $group Group of cache to get.
109 * @param string $filters Filters params.
110 *
111 * @return string Cache key.
112 */
113 public static function get_cache_key( $key_suffix, $group = 'default', $filters = false ) {
114 $cache_key = self::get_cache_prefix( $group, $filters ) . $key_suffix;
115 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( 'Get cache key :: [key_suffix=%s] :: [group=%s] :: [cache_key=%s]', $key_suffix, $group, $cache_key ) );
116 return $cache_key;
117 }
118
119 /**
120 * Get group cache prefix.
121 *
122 * @param string $group Group of cache to get.
123 *
124 * @return string Group prefix.
125 */
126 public static function get_key_group( $group ) {
127 return 'mainwp_cache_' . $group . '_' . self::GROUP_VER . '_prefix';
128 }
129
130 /**
131 * Get prefix allows all cache in a group to be invalidated at once.
132 *
133 * @param string $group Group of cache to get.
134 * @param string $filters Filters params.
135 *
136 * @return string Prefix.
137 */
138 public static function get_cache_prefix( $group, $filters = false ) {
139
140 $key_group = self::get_key_group( $group );
141
142 // Get cache key - to invalidate when needed.
143 $prefix = wp_cache_get( $key_group, $group );
144
145 if ( false === $prefix ) {
146 $prefix = get_transient( $key_group );
147 }
148
149 if ( false === $prefix ) {
150 $prefix = microtime( true );
151 set_transient( $key_group, $prefix, DAY_IN_SECONDS );
152 wp_cache_set( $key_group, $prefix, $group );
153 }
154
155 // Dynamic filters prefix checks.
156 $filters_prefix = '';
157 if ( ! empty( $filters ) && is_array( $filters ) ) {
158 $filters_prefix = '_' . self::get_normalized_params( $filters );
159 }
160
161 $userid = 0;
162 if ( function_exists( '\get_current_user_id' ) ) {
163 $userid = (int) get_current_user_id();
164 }
165
166 return 'mainwp_cache_' . $userid . '_' . $prefix . $filters_prefix . '_';
167 }
168
169 /**
170 * Get cache group.
171 *
172 * @param string $group Group of cache to clear.
173 */
174 public static function get_group_cache( $group = 'default' ) {
175 switch ( $group ) {
176 case 'manage_sites':
177 $cache = 'manage_sites';
178 break;
179 case 'updates':
180 $cache = 'updates';
181 break;
182 default:
183 $cache = 'default';
184 break;
185 }
186 return $cache;
187 }
188
189 /**
190 * Invalidate cache group.
191 *
192 * @param string $group Group of cache to clear.
193 */
194 public static function invalidate_cache_group( $group ) {
195 $mctime = microtime( true );
196 $key_group = self::get_key_group( $group );
197 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( 'Invalidate cache group :: [group=%s] :: [key_group=%s]', $group, $key_group ) );
198 wp_cache_set( $key_group, $mctime, $group );
199 set_transient( $key_group, $mctime, DAY_IN_SECONDS );
200 }
201
202 /**
203 * Add cache.
204 *
205 * @param string $key Cache key.
206 * @param string $group Group of cache.
207 * @param mixed $data Cache data.
208 * @param int $ttl TTL.
209 * @param int $object_cache_ttl Object cache ttl.
210 */
211 public static function add_cache(
212 $key,
213 $group,
214 $data = false,
215 $ttl = null,
216 $object_cache_ttl = null
217 ) {
218 $ttl = $ttl ?? self::CACHE_TTL;
219 $object_cache_ttl = $object_cache_ttl ?? $ttl;
220 $transient_key = $key;
221 wp_cache_set( $key, $data, $group, $object_cache_ttl );
222 set_transient( $transient_key, $data, $ttl );
223 self::record_metrics( 'set', $key, $group );
224 }
225
226
227 /**
228 * Method get_params_key.
229 *
230 * @param array $params Params to get prefix.
231 *
232 * @return string Prefix.
233 */
234 public static function get_params_key( $params = array() ) {
235 return self::get_normalized_params( $params );
236 }
237
238 /**
239 * Method get_normalized_params.
240 *
241 * @param array $params Params to get prefix.
242 *
243 * @return string Prefix.
244 */
245 private static function get_normalized_params( $params = array() ) {
246 $normalized = wp_json_encode( self::clean_and_sort_params( $params ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
247 return sha1( $normalized ); // NOSONAR - is sufficient for keys.
248 }
249
250 /**
251 * Smart clean & sort for parameter arrays.
252 *
253 * Features:
254 * - Removes nulls, empty strings, and (optionally) empty arrays
255 * - Trims strings
256 * - Coerces "true"/"false"/"on"/"off"/"yes"/"no" to bool
257 * - Coerces numeric strings to int/float
258 * - Sorts associative arrays by key (stable)
259 * - Sorts list arrays by value (optional) or keeps original order
260 * - Can ignore specific keys (e.g., nonce/timestamp) on associative arrays
261 *
262 * @param array $params Params to clean.
263 * @param array $opts Clean opts.
264 * {
265 * coerce_types?: bool, // default true
266 * numeric_to_number?: bool, // default true (only if coerce_types)
267 * sort_lists?: bool, // default true (order-insensitive lists)
268 * drop_empty_arrays?: bool, // default true
269 * ignore_keys?: string[] // default []
270 * }
271 * @return array
272 */
273 private static function clean_and_sort_params( $params, $opts = array() ) { // phpcs:ignore -- NOSONAR -complex method.
274
275 $default_igkeys = array( 'maybe_cache' );
276
277 $coerceTypes = $opts['coerce_types'] ?? true;
278 $numericToNumber = $opts['numeric_to_number'] ?? true;
279 $sortLists = $opts['sort_lists'] ?? true;
280 $dropEmptyArrays = $opts['drop_empty_arrays'] ?? true;
281
282 $ignoreKeys = $opts['ignore_keys'] ?? array();
283 $ignoreKeys = is_array( $ignoreKeys ) ? array_merge( $ignoreKeys, $default_igkeys ) : $ignoreKeys;
284 $ignoreKeys = array_flip( $ignoreKeys );
285
286 $isAssoc = static function ( array $a ) {
287 $i = 0;
288 foreach ( $a as $k => $_ ) {
289 if ( $k !== $i++ ) {
290 return true;
291 }
292 }
293 return false;
294 };
295
296 $coerce = static function ( $v ) use ( $coerceTypes, $numericToNumber ) {
297 if ( ! $coerceTypes ) {
298 if ( is_string( $v ) ) {
299 $s = trim( $v );
300 return '' === $s ? null : $s;
301 }
302 return $v;
303 }
304
305 if ( is_string( $v ) ) {
306 $s = trim( $v );
307 if ( '' === $s ) {
308 return null;
309 }
310
311 $ls = strtolower( $s );
312 if ( in_array( $ls, array( 'true', 'yes', 'on' ), true ) ) {
313 return true;
314 }
315 if ( in_array( $ls, array( 'false', 'no', 'off' ), true ) ) {
316 return false;
317 }
318
319 if ( $numericToNumber && is_numeric( $s ) ) {
320 // keep integers as int; others as float.
321 return ctype_digit( ltrim( $s, '+-' ) ) ? (int) $s : (float) $s;
322 }
323 return $s;
324 }
325 return $v; // bool/int/float/null unchanged.
326 };
327
328 $walk = null;
329 $walk = function ( $value ) use ( &$walk, $coerce, $isAssoc, $ignoreKeys, $sortLists, $dropEmptyArrays ) {
330 if ( ! is_array( $value ) ) {
331 return $coerce( $value );
332 }
333
334 if ( $isAssoc( $value ) ) {
335 // Remove ignored keys first.
336 if ( ! empty( $ignoreKeys ) ) {
337 foreach ( $ignoreKeys as $key => $_ ) {
338 if ( array_key_exists( $key, $value ) ) {
339 unset( $value[ $key ] );
340 }
341 }
342 }
343
344 $out = array();
345 foreach ( $value as $k => $v ) {
346 $nv = $walk( $v );
347
348 // drop null.
349 if ( null === $nv ) {
350 continue;
351 }
352
353 // optionally drop empty arrays.
354 if ( is_array( $nv ) && $dropEmptyArrays && count( $nv ) === 0 ) {
355 continue;
356 }
357
358 $out[ $k ] = $nv;
359 }
360 ksort( $out );
361 return $out;
362 }
363
364 // List (indexed) array.
365 $tmp = array();
366 foreach ( $value as $v ) {
367 $nv = $walk( $v );
368 if ( $nv === null ) {
369 continue;
370 }
371 if ( is_array( $nv ) && $dropEmptyArrays && count( $nv ) === 0 ) {
372 continue;
373 }
374 $tmp[] = $nv;
375 }
376
377 // If requested, make lists order-insensitive:
378 // - If all scalars, sort naturally (case-insensitive for strings)
379 // - If mixed/nested, JSON-stable sorting using string cast.
380 if ( $sortLists && count( $tmp ) > 1 ) {
381 $allScalars = true;
382 foreach ( $tmp as $item ) {
383 if ( is_array( $item ) || is_object( $item ) ) {
384 $allScalars = false;
385 break;
386 }
387 }
388
389 if ( $allScalars ) {
390 sort( $tmp, SORT_NATURAL | SORT_FLAG_CASE );
391 } else {
392 usort(
393 $tmp,
394 static function ( $a, $b ) {
395 $sa = is_scalar( $a ) ? (string) $a : wp_json_encode( $a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
396 $sb = is_scalar( $b ) ? (string) $b : wp_json_encode( $b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
397 return $sa <=> $sb;
398 }
399 );
400 }
401 }
402
403 return array_values( $tmp );
404 };
405
406 return $walk( $params ) ?? array();
407 }
408
409
410 /**
411 * Get data with fast object-cache + DB transient fallback.
412 *
413 * Flow:
414 * 1) Try object cache (wp_cache_get).
415 * 2) Fallback to transient (get_transient) and warm object cache if found.
416 * 3) Otherwise compute via $generator(), store in both layers, and return.
417 *
418 * @param string $key Logical cache key (no group prefix).
419 * @param string $group Object cache group name.
420 * @param callable $generator Function that returns the fresh data when not cached.
421 * @param array $args Args for generator
422 * @param int $ttl Transient TTL in seconds (also used for object cache if $object_cache_ttl is null).
423 * @param int|null $object_cache_ttl Optional TTL for object cache layer; defaults to $ttl.
424 * @param string|null $prefix Optional prefix to avoid collisions (e.g., your plugin slug). If null, uses $group.
425 *
426 * @return mixed
427 */
428 public function get_cache(
429 $key,
430 $group,
431 $generator = false,
432 $args = array(),
433 $ttl = null,
434 $object_cache_ttl = null
435 ) {
436 $ttl = $ttl ?? self::CACHE_TTL;
437 $object_cache_ttl = $object_cache_ttl ?? $ttl;
438 $transient_key = $key;
439
440 $data = wp_cache_get( $key, $group );
441
442 if ( false !== $data ) {
443 self::record_metrics( 'hit', $key, $group );
444 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( 'Get Cache :: wp :: hit :: [key=%s] :: [group=%s]', $key, $group ) );
445 return $data;
446 }
447
448 $data = get_transient( $transient_key );
449
450 if ( false !== $data ) {
451 self::record_metrics( 'hit', $key, $group );
452 wp_cache_set( $key, $data, $group, $object_cache_ttl );
453 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( 'Get Cache :: transient :: hit :: [key=%s] :: [group=%s]', $key, $group ) );
454 return $data;
455 }
456
457 self::record_metrics( 'miss', $key, $group );
458 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( 'Get Cache :: miss :: [key=%s] :: [group=%s]', $key, $group ) );
459
460 if ( $generator && is_callable( $generator ) ) {
461 // Pass args to the generator.
462 $data = call_user_func_array( $generator, $args );
463 self::record_metrics( 'set', $key, $group );
464 wp_cache_set( $key, $data, $group, $object_cache_ttl );
465 set_transient( $transient_key, $data, $ttl );
466 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( 'Get Cache :: set :: [key=%s] :: [group=%s]', $key, $group ) );
467 return $data;
468 }
469
470 return '_get_cache_false';
471 }
472
473
474 /**
475 * Delete both layers for a key - invalidation.
476 *
477 * @param string $key Cache key to delete.
478 * @param string $group Cache group to delete.
479 * @return void
480 */
481 public function delete_cache( $key, $group ) {
482 wp_cache_delete( $key, $group );
483 delete_transient( $key );
484 }
485
486 /**
487 * Method record_metrics().
488 *
489 * @param string $metric Cache metric.
490 * @param string $key Cache key.
491 * @param string $group Cache group.
492 *
493 * @return void
494 */
495 public static function record_metrics( $metric, $key = '', $group = '' ) {
496
497 self::$metrics_list[] = $group . '|' . $key;
498
499 switch ( $metric ) {
500 case 'set':
501 ++self::$sets;
502 break;
503 case 'miss':
504 ++self::$misses;
505 break;
506 case 'hit':
507 ++self::$hits;
508 break;
509 default:
510 break;
511 }
512 }
513
514 /**
515 * Method log_metrics().
516 *
517 * @return void
518 */
519 public static function log_metrics() {
520
521 if ( empty( self::$metrics_list ) ) {
522 return false;
523 }
524
525 $sec = MainWP_Execution_Helper::get_run_time();
526 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[hits=%d] :: [misses=%d] :: [sets=%d] :: [execution time=%s]', self::$hits, self::$misses, self::$sets, round( $sec, 4 ) ) );
527 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[cache_keys=%s]', implode( "\n", self::$metrics_list ) ) );
528 }
529 }
530