PluginProbe
Sessions / 2.3.1
Sessions v2.3.1
2.1.0 2.10.0 2.11.0 2.12.0 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.2.0 2.3.0 2.3.1 2.4.0 2.4.1 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.1.0 3.1.1 All 39 releases
sessions / includes / system / class-cache.php

class-cache.php in Sessions 2.3.1, at includes/system/class-cache.php

674 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin cache handling.
4 *
5 * @package System
6 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
7 * @since 1.0.0
8 * @noinspection PhpCSValidationInspection
9 */
10
11 namespace POSessions\System;
12
13 use POSessions\System\Conversion;
14
15 /**
16 * The class responsible to handle cache management.
17 *
18 * @package System
19 * @author Pierre Lannoy <https://pierre.lannoy.fr/>.
20 * @since 1.0.0
21 */
22 class Cache {
23
24 /**
25 * The pool's name, specific to the calling plugin.
26 *
27 * @since 1.0.0
28 * @var string $pool_name The pool's name.
29 */
30 private static $pool_name = POSE_SLUG;
31
32 /**
33 * Available TTLs.
34 *
35 * @since 1.0.0
36 * @var array $ttls The TTLs array.
37 */
38 private static $ttls = [];
39
40 /**
41 * Default TTL.
42 *
43 * @since 1.0.0
44 * @var integer $default_ttl The default TTL in seconds.
45 */
46 private static $default_ttl = 3600;
47
48 /**
49 * Is APCu available.
50 *
51 * @since 1.0.0
52 * @var boolean $apcu_available Is APCu available.
53 */
54 private static $apcu_available = false;
55
56 /**
57 * Hits values.
58 *
59 * @since 1.0.0
60 * @var array $hit Hits values.
61 */
62 private static $hit = [];
63
64 /**
65 * Miss values.
66 *
67 * @since 1.0.0
68 * @var array $miss Miss values.
69 */
70 private static $miss = [];
71
72 /**
73 * Current (temporary) values.
74 *
75 * @since 1.0.0
76 * @var array $current Current (temporary) values.
77 */
78 private static $current = [];
79
80 /**
81 * Initializes the class and set its properties.
82 *
83 * @since 1.0.0
84 */
85 public function __construct() {
86 self::init();
87 }
88
89 /**
90 * Verify if cache is in memory.
91 *
92 * @since 1.0.0
93 */
94 public static function is_memory() {
95 return wp_using_ext_object_cache() || self::$apcu_available;
96 }
97
98 /**
99 * Initializes properties.
100 *
101 * @since 1.0.0
102 */
103 public static function init() {
104 self::$ttls = [
105 'ephemeral' => 0,
106 'infinite' => 10 * YEAR_IN_SECONDS,
107 'diagnosis' => HOUR_IN_SECONDS,
108 'metrics' => 1 * MINUTE_IN_SECONDS,
109 'plugin-statistics' => DAY_IN_SECONDS,
110 ];
111 if ( wp_using_ext_object_cache() ) {
112 wp_cache_add_global_groups( self::$pool_name );
113 }
114 self::$apcu_available = function_exists( 'apcu_delete' ) && function_exists( 'apcu_fetch' ) && function_exists( 'apcu_store' );
115 add_action( 'shutdown', [ 'POSessions\System\Cache', 'log_debug' ], 10, 0 );
116 add_filter( 'perfopsone_icache_introspection', [ 'POSessions\System\Cache', 'introspection' ] );
117 }
118
119 /**
120 * Get the introspection endpoint.
121 *
122 * @since 1.0.0
123 */
124 public static function introspection( $endpoints ) {
125 $endpoints[ POSE_SLUG ] = [ 'name' => POSE_PRODUCT_NAME, 'version' => POSE_VERSION, 'endpoint' => [ 'POSessions\System\Cache', 'get_analytics' ] ];
126 return $endpoints;
127 }
128
129 /**
130 * Get an ID for caching.
131 *
132 * @since 1.0.0
133 */
134 public static function id( $args, $path = 'data/' ) {
135 if ( '/' === $path[0] ) {
136 $path = substr( $path, 1 );
137 }
138 if ( '/' !== $path[ strlen( $path ) - 1 ] ) {
139 $path = $path . '/';
140 }
141 return $path . md5( (string) $args );
142 }
143
144 /**
145 * Full item name.
146 *
147 * @param string $item_name Item name. Expected to not be SQL-escaped.
148 * @param boolean $blog_aware Optional. Has the name must take care of blog.
149 * @param boolean $locale_aware Optional. Has the name must take care of locale.
150 * @param boolean $user_aware Optional. Has the name must take care of user.
151 * @return string The full item name.
152 * @since 1.0.0
153 */
154 private static function full_item_name( $item_name, $blog_aware = false, $locale_aware = false, $user_aware = false ) {
155 $name = '';
156 if ( $blog_aware ) {
157 $name .= (string) get_current_blog_id() . '/';
158 }
159 if ( $locale_aware ) {
160 $name .= (string) L10n::get_display_locale() . '/';
161 }
162 if ( $user_aware ) {
163 $name .= (string) User::get_current_user_id() . '/';
164 }
165 $name .= $item_name;
166 return substr( trim( $name ), 0, 172 - strlen( self::$pool_name ) );
167 }
168
169 /**
170 * Normalized item name.
171 *
172 * @param string $item_name Item name. Expected to not be SQL-escaped.
173 * @return string The normalized item name.
174 * @since 1.0.0
175 */
176 private static function normalized_item_name( $item_name ) {
177 if ( '/' === $item_name[0] ) {
178 $item_name = substr( $item_name, 1 );
179 }
180 while ( 0 !== substr_count( $item_name, '//' ) ) {
181 $item_name = str_replace( '//', '/', $item_name );
182 }
183 $item_name = str_replace( '/', '_', $item_name );
184 return strtolower( $item_name );
185 }
186
187 /**
188 * Get the value of a fully named cache item.
189 *
190 * If the item does not exist, does not have a value, or has expired,
191 * then the return value will be false.
192 *
193 * @param string $item_name Item name. Expected to not be SQL-escaped.
194 * @return mixed Value of item.
195 * @since 1.0.0
196 */
197 private static function get_for_full_name( $item_name ) {
198 $chrono = microtime( true );
199 $item_name = self::normalized_item_name( $item_name );
200 $found = false;
201 if ( self::$apcu_available ) {
202 $result = apcu_fetch( self::$pool_name . '_' . $item_name, $found );
203 } else {
204 $result = get_transient( self::$pool_name . '_' . $item_name );
205 $found = false !== $result;
206 }
207 if ( $found ) {
208 self::$hit[] = [
209 'time' => microtime( true ) - $chrono,
210 'size' => strlen( serialize( $result ) ),
211 ];
212 return $result;
213 } else {
214 self::$current[ $item_name ] = $chrono;
215 return null;
216 }
217 }
218
219 /**
220 * Get the value of a shared cache item.
221 *
222 * If the item does not exist, does not have a value, or has expired,
223 * then the return value will be false.
224 *
225 * @param string $item_name Item name. Expected to not be SQL-escaped.
226 * @return mixed Value of item.
227 * @since 1.0.0
228 */
229 public static function get_shared( $item_name ) {
230 $save = self::$pool_name;
231 self::$pool_name = 'perfopsone';
232 $result = self::get_for_full_name( self::full_item_name( $item_name ) );
233 self::$pool_name = $save;
234 return $result;
235 }
236
237 /**
238 * Get the value of a global cache item.
239 *
240 * If the item does not exist, does not have a value, or has expired,
241 * then the return value will be false.
242 *
243 * @param string $item_name Item name. Expected to not be SQL-escaped.
244 * @return mixed Value of item.
245 * @since 1.0.0
246 */
247 public static function get_global( $item_name ) {
248 return self::get_for_full_name( self::full_item_name( $item_name ) );
249 }
250
251 /**
252 * Get the value of a standard cache item.
253 *
254 * If the item does not exist, does not have a value, or has expired,
255 * then the return value will be false.
256 *
257 * @param string $item_name Item name. Expected to not be SQL-escaped.
258 * @param boolean $blog_aware Optional. Has the name must take care of blog.
259 * @param boolean $locale_aware Optional. Has the name must take care of locale.
260 * @param boolean $user_aware Optional. Has the name must take care of user.
261 * @return mixed Value of item.
262 * @since 1.0.0
263 */
264 public static function get( $item_name, $blog_aware = false, $locale_aware = false, $user_aware = false ) {
265 return self::get_for_full_name( self::full_item_name( $item_name, $blog_aware, $locale_aware, $user_aware ) );
266 }
267
268 /**
269 * Set the value of a fully named cache item.
270 *
271 * You do not need to serialize values. If the value needs to be serialized, then
272 * it will be serialized before it is set.
273 *
274 * @param string $item_name Item name. Expected to not be SQL-escaped.
275 * @param mixed $value Item value. Must be serializable if non-scalar.
276 * Expected to not be SQL-escaped.
277 * @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
278 * The ttl value in seconds if it's and integer.
279 * @return bool False if value was not set and true if value was set.
280 * @since 1.0.0
281 */
282 private static function set_for_full_name( $item_name, $value, $ttl = 'default' ) {
283 $item_name = self::normalized_item_name( $item_name );
284 $expiration = self::$default_ttl;
285 if ( is_string( $ttl ) && array_key_exists( $ttl, self::$ttls ) ) {
286 $expiration = self::$ttls[ $ttl ];
287 }
288 if ( is_integer( $ttl ) && 0 < (int) $ttl ) {
289 $expiration = (int) $ttl;
290 }
291 if ( $expiration > 0 ) {
292 if ( self::$apcu_available ) {
293 $result = apcu_store( self::$pool_name . '_' . $item_name, $value, $expiration );
294 } else {
295 $result = set_transient( self::$pool_name . '_' . $item_name, $value, $expiration );
296 }
297 if ( array_key_exists( $item_name, self::$current ) ) {
298 self::$miss[] = [
299 'time' => microtime( true ) - self::$current[ $item_name ],
300 'size' => strlen( serialize( $result ) ),
301 ];
302 }
303 } else {
304 $result = false;
305 }
306 return $result;
307 }
308
309 /**
310 * Set the value of a shared cache item.
311 *
312 * You do not need to serialize values. If the value needs to be serialized, then
313 * it will be serialized before it is set.
314 *
315 * @param string $item_name Item name. Expected to not be SQL-escaped.
316 * @param mixed $value Item value. Must be serializable if non-scalar.
317 * Expected to not be SQL-escaped.
318 * @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
319 * The ttl value in seconds if it's and integer.
320 * @return bool False if value was not set and true if value was set.
321 * @since 1.0.0
322 */
323 public static function set_shared( $item_name, $value, $ttl = 'default' ) {
324 $save = self::$pool_name;
325 self::$pool_name = 'perfopsone';
326 $result = self::set_for_full_name( self::full_item_name( $item_name ), $value, $ttl );
327 self::$pool_name = $save;
328 return $result;
329 }
330
331 /**
332 * Set the value of a global cache item.
333 *
334 * You do not need to serialize values. If the value needs to be serialized, then
335 * it will be serialized before it is set.
336 *
337 * @param string $item_name Item name. Expected to not be SQL-escaped.
338 * @param mixed $value Item value. Must be serializable if non-scalar.
339 * Expected to not be SQL-escaped.
340 * @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
341 * The ttl value in seconds if it's and integer.
342 * @return bool False if value was not set and true if value was set.
343 * @since 1.0.0
344 */
345 public static function set_global( $item_name, $value, $ttl = 'default' ) {
346 return self::set_for_full_name( self::full_item_name( $item_name ), $value, $ttl );
347 }
348
349 /**
350 * Set the value of a standard cache item.
351 *
352 * You do not need to serialize values. If the value needs to be serialized, then
353 * it will be serialized before it is set.
354 *
355 * @param string $item_name Item name. Expected to not be SQL-escaped.
356 * @param mixed $value Item value. Must be serializable if non-scalar.
357 * Expected to not be SQL-escaped.
358 * @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
359 * The ttl value in seconds if it's and integer.
360 * @param boolean $blog_aware Optional. Has the name must take care of blog.
361 * @param boolean $locale_aware Optional. Has the name must take care of locale.
362 * @param boolean $user_aware Optional. Has the name must take care of user.
363 * @return bool False if value was not set and true if value was set.
364 * @since 1.0.0
365 */
366 public static function set( $item_name, $value, $ttl = 'default', $blog_aware = false, $locale_aware = false, $user_aware = false ) {
367 return self::set_for_full_name( self::full_item_name( $item_name, $blog_aware, $locale_aware, $user_aware ), $value, $ttl );
368 }
369
370 /**
371 * Delete the value of a fully named cache item.
372 *
373 * This function accepts generic car "*" for transients.
374 *
375 * @param string $item_name Item name. Expected to not be SQL-escaped.
376 * @return integer Number of deleted items.
377 * @since 1.0.0
378 */
379 private static function delete_for_ful_name( $item_name ) {
380 $item_name = self::normalized_item_name( $item_name );
381 $result = 0;
382 if ( self::$apcu_available ) {
383 if ( strlen( $item_name ) - 1 === strpos( $item_name, '_*' ) ) {
384 return false;
385 } else {
386 return apcu_delete( self::$pool_name . '_' . $item_name );
387 }
388 }
389 global $wpdb;
390 $item_name = self::$pool_name . '_' . $item_name;
391 if ( strlen( $item_name ) - 1 === strpos( $item_name, '_*' ) ) {
392 // phpcs:ignore
393 $delete = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name = '_transient_timeout_" . str_replace( '_*', '', $item_name ) . "' OR option_name LIKE '_transient_timeout_" . str_replace( '_*', '_%', $item_name ) . "';" );
394 } else {
395 // phpcs:ignore
396 $delete = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name = '_transient_timeout_" . $item_name . "';" );
397 }
398 foreach ( $delete as $transient ) {
399 $key = str_replace( '_transient_timeout_', '', $transient );
400 if ( delete_transient( $key ) ) {
401 ++$result;
402 }
403 }
404 return $result;
405 }
406
407 /**
408 * Delete the full pool.
409 *
410 * @return integer Number of deleted items.
411 * @since 1.0.0
412 */
413 public static function delete_pool() {
414 $result = 0;
415 if ( self::$apcu_available ) {
416 if ( function_exists( 'apcu_cache_info' ) && function_exists( 'apcu_delete' ) ) {
417 try {
418 $infos = apcu_cache_info( false );
419 if ( array_key_exists( 'cache_list', $infos ) && is_array( $infos['cache_list'] ) ) {
420 foreach ( $infos['cache_list'] as $script ) {
421 if ( 0 === strpos( $script['info'], self::$pool_name . '_' ) ) {
422 apcu_delete( $script['info'] );
423 $result++;
424 }
425 }
426 }
427 } catch ( \Throwable $e ) {
428 \DecaLog\Engine::eventsLogger( POSE_SLUG )->error( sprintf( 'Unable to query APCu status: %s.', $e->getMessage() ), [ 'code' => $e->getCode() ] );
429 }
430 }
431 } else {
432 $result = self::delete_global( '/*' );
433 }
434 return $result;
435 }
436
437 /**
438 * Delete the value of a shared cache item.
439 *
440 * This function accepts generic car "*" for transients.
441 *
442 * @param string $item_name Item name. Expected to not be SQL-escaped.
443 * @return integer Number of deleted items.
444 * @since 1.0.0
445 */
446 public static function delete_shared( $item_name ) {
447 $save = self::$pool_name;
448 self::$pool_name = 'perfopsone';
449 $result = self::delete_for_ful_name( self::full_item_name( $item_name ) );
450 self::$pool_name = $save;
451 return $result;
452 }
453
454 /**
455 * Delete the value of a global cache item.
456 *
457 * This function accepts generic car "*" for transients.
458 *
459 * @param string $item_name Item name. Expected to not be SQL-escaped.
460 * @return integer Number of deleted items.
461 * @since 1.0.0
462 */
463 public static function delete_global( $item_name ) {
464 return self::delete_for_ful_name( self::full_item_name( $item_name ) );
465 }
466
467 /**
468 * Delete the value of a standard cache item.
469 *
470 * This function accepts generic car "*" for transients.
471 *
472 * @param string $item_name Item name. Expected to not be SQL-escaped.
473 * @param boolean $blog_aware Optional. Has the name must take care of blog.
474 * @param boolean $locale_aware Optional. Has the name must take care of locale.
475 * @param boolean $user_aware Optional. Has the name must take care of user.
476 * @return integer Number of deleted items.
477 * @since 1.0.0
478 */
479 public static function delete( $item_name, $blog_aware = false, $locale_aware = false, $user_aware = false ) {
480 return self::delete_for_ful_name( self::full_item_name( $item_name, $blog_aware, $locale_aware, $user_aware ) );
481 }
482
483 /**
484 * Get the minimum value of a ttl time range.
485 *
486 * @param string $ttl_range The time range in seconds. May be something like '0', '200' or '15-600:15'.
487 * @return integer The ttl in seconds.
488 * @since 1.0.0
489 */
490 public static function get_min( $ttl_range ) {
491 if ( ! is_string( $ttl_range) ) {
492 return 0;
493 }
494 $ttls = explode( '-', $ttl_range );
495 if ( 1 === count( $ttls ) ) {
496 return (int) $ttls[0];
497 }
498 if ( false !== strpos( $ttls[1], ':' ) ) {
499 $steps = explode( ':', $ttls[1] );
500 $ttls[1] = $steps[0];
501 }
502 return (int) min( (int) $ttls[0], (int) $ttls[1] );
503 }
504
505 /**
506 * Get the maximum value of a ttl time range.
507 *
508 * @param string $ttl_range The time range in seconds. May be something like '0', '200' or '15-600:15'.
509 * @return integer The ttl in seconds.
510 * @since 1.0.0
511 */
512 public static function get_max( $ttl_range ) {
513 if ( ! is_string( $ttl_range) ) {
514 return 0;
515 }
516 $ttls = explode( '-', $ttl_range );
517 if ( 1 === count( $ttls ) ) {
518 return (int) $ttls[0];
519 }
520 if ( false !== strpos( $ttls[1], ':' ) ) {
521 $steps = explode( ':', $ttls[1] );
522 $ttls[1] = $steps[0];
523 }
524 return (int) max( (int) $ttls[0], (int) $ttls[1] );
525 }
526
527 /**
528 * Get the step of a ttl time range.
529 *
530 * @param string $ttl_range The time range in seconds. May be something like '0', '200' or '15-600:15'.
531 * @return integer The ttl in seconds.
532 * @since 1.0.0
533 */
534 public static function get_step( $ttl_range ) {
535 if ( ! is_string( $ttl_range) ) {
536 return 0;
537 }
538 $ttls = explode( '-', $ttl_range );
539 if ( 1 === count( $ttls ) ) {
540 return 0;
541 }
542 if ( false !== strpos( $ttls[1], ':' ) ) {
543 $steps = explode( ':', $ttls[1] );
544 if ( 2 === count( $ttls ) ) {
545 return $steps[1];
546 }
547 }
548 return 1;
549 }
550
551 /**
552 * Get the medium value of a ttl time range.
553 *
554 * This function accepts generic car "*" for transients.
555 *
556 * @param string $ttl_range The time range in seconds. May be something like '5-600' or '200'.
557 * @return integer The ttl in seconds.
558 * @since 1.0.0
559 */
560 public static function get_med( $ttl_range ) {
561 $min = self::get_min( $ttl_range );
562 $max = self::get_max( $ttl_range );
563 $step = self::get_step( $ttl_range );
564 $factor = $step * (int) round( ( $max - $min ) / ( 2 * $step ) );
565 return $min + (int) round( $factor );
566 }
567
568 /**
569 * Get cache analytics.
570 *
571 * @return array The cache analytics.
572 * @since 1.0.0
573 */
574 public static function get_analytics() {
575 $result = [];
576 $hit_time = 0;
577 $hit_count = count( self::$hit );
578 $hit_size = 0;
579 if ( 0 < $hit_count ) {
580 foreach ( self::$hit as $h ) {
581 $hit_time = $hit_time + $h['time'];
582 $hit_size = $hit_size + $h['size'];
583 }
584 $hit_time = $hit_time / $hit_count;
585 $hit_size = $hit_size / $hit_count;
586 }
587 $result['hit']['count'] = $hit_count;
588 $result['hit']['time'] = $hit_time;
589 $result['hit']['size'] = $hit_size;
590 $miss_time = 0;
591 $miss_count = count( self::$miss );
592 $miss_size = 0;
593 if ( 0 < $miss_count ) {
594 foreach ( self::$miss as $h ) {
595 $miss_time = $miss_time + $h['time'];
596 $miss_size = $miss_size + $h['size'];
597 }
598 $miss_time = $miss_time / $miss_count;
599 $miss_size = $miss_size / $miss_count;
600 }
601 $result['miss']['count'] = $miss_count;
602 $result['miss']['time'] = $miss_time;
603 $result['miss']['size'] = $miss_size;
604 if ( self::$apcu_available ) {
605 $result['type'] = 'apcu';
606 } else {
607 $result['type'] = 'db_transient';
608 }
609 return $result;
610 }
611
612 /**
613 * Logs the cache analytics.
614 *
615 * @since 1.0.0
616 */
617 public static function log_debug() {
618 $analytics = self::get_analytics();
619 $log = '[' . $analytics['type'] . ']';
620 $log .= ' Hit count: ' . $analytics['hit']['count'] . ' Hit time: ' . round($analytics['hit']['time'] * 1000, 3) . 'ms Hit size: ' . Conversion::data_shorten( (int) $analytics['hit']['size'] );
621 $log .= ' Miss count: ' . $analytics['miss']['count'] . ' Miss time: ' . round($analytics['miss']['time'] * 1000, 3) . 'ms Miss size: ' . Conversion::data_shorten( (int) $analytics['miss']['size'] );
622 if ( 0 !== (int) $analytics['hit']['count'] || 0 !== (int) $analytics['miss']['count'] ) {
623 \DecaLog\Engine::eventsLogger( POSE_SLUG )->debug( $log );
624 }
625 }
626
627 /**
628 * Get the options infos for Site Health "info" tab.
629 *
630 * @since 1.0.0
631 */
632 public static function debug_info() {
633 if ( self::$apcu_available ) {
634 $result['product'] = [
635 'label' => 'Product',
636 'value' => 'APCu',
637 ];
638 foreach ( [ 'enabled', 'shm_segments', 'shm_size', 'entries_hint', 'ttl', 'gc_ttl', 'mmap_file_mask', 'slam_defense', 'enable_cli', 'use_request_time', 'serializer', 'coredump_unmap', 'preload_path' ] as $key ) {
639 $result[ 'directive_' . $key ] = [
640 'label' => '[Directive] ' . $key,
641 'value' => ini_get( 'apc.' . $key ),
642 ];
643 }
644 if ( function_exists( 'apcu_sma_info' ) && function_exists( 'apcu_cache_info' ) ) {
645 $raw = apcu_sma_info();
646 foreach ( $raw as $key => $status ) {
647 if ( ! is_array( $status ) ) {
648 $result[ 'status_' . $key ] = [
649 'label' => '[Status] ' . $key,
650 'value' => $status,
651 ];
652 }
653 }
654 $raw = apcu_cache_info();
655 foreach ( $raw as $key => $status ) {
656 if ( ! is_array( $status ) ) {
657 $result[ 'status_' . $key ] = [
658 'label' => '[Status] ' . $key,
659 'value' => $status,
660 ];
661 }
662 }
663 }
664 } else {
665 $result['product'] = [
666 'label' => 'Product',
667 'value' => 'Database transients',
668 ];
669 }
670 return $result;
671 }
672
673 }
674