PluginProbe
Sessions / 2.2.0
Sessions v2.2.0
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.2.0, at includes/system/class-cache.php

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