<?php
/**
* Plugin cache handling.
*
* @package System
* @author Pierre Lannoy <https://pierre.lannoy.fr/>.
* @since 1.0.0
* @noinspection PhpCSValidationInspection
*/
namespace Vibes\System;
use Vibes\System\Conversion;
use Vibes\System\Option;
/**
* The class responsible to handle cache management.
*
* @package System
* @author Pierre Lannoy <https://pierre.lannoy.fr/>.
* @since 1.0.0
*/
class Cache {
/**
* The pool's name, specific to the calling plugin.
*
* @since 1.0.0
* @var string $pool_name The pool's name.
*/
private static $pool_name = VIBES_SLUG;
/**
* The apcu pool's prefix, specific to the WordPress instance.
*
* @since 1.0.0
* @var string $apcu_pool_prefix The pool's name.
*/
private static $apcu_pool_prefix = '';
/**
* Available TTLs.
*
* @since 1.0.0
* @var array $ttls The TTLs array.
*/
private static $ttls = [];
/**
* Default TTL.
*
* @since 1.0.0
* @var integer $default_ttl The default TTL in seconds.
*/
private static $default_ttl = 3600;
/**
* Is APCu available.
*
* @since 1.0.0
* @var boolean $apcu_available Is APCu available.
*/
private static $apcu_available = false;
/**
* Initializes the class and set its properties.
*
* @since 1.0.0
*/
public function __construct() {
self::init();
}
/**
* Verify if cache is in memory.
*
* @since 1.0.0
*/
public static function is_memory() {
return wp_using_ext_object_cache() || self::$apcu_available;
}
/**
* Get cache analytics.
*
* @return array The cache analytics.
* @since 1.0.0
*/
public static function get_analytics() {
$result = [];
if ( wp_using_ext_object_cache() ) {
$result['type'] = 'object_cache';
} elseif ( self::$apcu_available ) {
$result['type'] = 'apcu';
} else {
$result['type'] = 'db_transient';
}
return $result;
}
/**
* Initializes properties.
*
* @since 1.0.0
*/
public static function init() {
self::$ttls = [
'ephemeral' => 1 * MINUTE_IN_SECONDS,
'infinite' => 10 * YEAR_IN_SECONDS,
'diagnosis' => HOUR_IN_SECONDS,
'plugin-statistics' => DAY_IN_SECONDS,
];
if ( wp_using_ext_object_cache() ) {
wp_cache_add_global_groups( self::$pool_name );
}
if ( ! defined( 'APCU_CACHE_PREFIX' ) ) {
define( 'APCU_CACHE_PREFIX', '_' . md5( ABSPATH ) . '_' );
}
self::$apcu_pool_prefix = APCU_CACHE_PREFIX;
self::$apcu_available = function_exists( 'apcu_delete' ) && function_exists( 'apcu_fetch' ) && function_exists( 'apcu_store' );
}
/**
* Get an ID for caching.
*
* @since 1.0.0
*/
public static function id( $args, $path = 'data/' ) {
if ( '/' === $path[0] ) {
$path = substr( $path, 1 );
}
if ( '/' !== $path[ strlen( $path ) - 1 ] ) {
$path = $path . '/';
}
return $path . md5( (string) $args );
}
/**
* Full item name.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @param boolean $blog_aware Optional. Has the name must take care of blog.
* @param boolean $locale_aware Optional. Has the name must take care of locale.
* @param boolean $user_aware Optional. Has the name must take care of user.
* @return string The full item name.
* @since 1.0.0
*/
private static function full_item_name( $item_name, $blog_aware = false, $locale_aware = false, $user_aware = false ) {
$name = '';
if ( $blog_aware ) {
$name .= (string) get_current_blog_id() . '/';
}
if ( $locale_aware ) {
$name .= (string) L10n::get_display_locale() . '/';
}
if ( $user_aware ) {
$name .= (string) User::get_current_user_id() . '/';
}
$name .= $item_name;
return substr( trim( $name ), 0, 172 - strlen( self::$apcu_pool_prefix . self::$pool_name ) );
}
/**
* Normalized item name.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @return string The normalized item name.
* @since 1.0.0
*/
private static function normalized_item_name( $item_name ) {
if ( '/' === $item_name[0] ) {
$item_name = substr( $item_name, 1 );
}
while ( 0 !== substr_count( $item_name, '//' ) ) {
$item_name = str_replace( '//', '/', $item_name );
}
$item_name = str_replace( '/', '_', $item_name );
return strtolower( $item_name );
}
/**
* Get the value of a fully named cache item.
*
* If the item does not exist, does not have a value, or has expired,
* then the return value will be false.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @return mixed Value of item.
* @since 1.0.0
*/
private static function get_for_full_name( $item_name ) {
$item_name = self::normalized_item_name( $item_name );
$found = false;
if ( self::$apcu_available && Option::network_get( 'use_apcu', true ) ) {
$result = apcu_fetch( self::$pool_name . self::$apcu_pool_prefix . $item_name, $found );
} elseif ( wp_using_ext_object_cache() ) {
$result = wp_cache_get( $item_name, self::$pool_name, false, $found );
} else {
$result = get_transient( self::$pool_name . '_' . $item_name );
$found = false !== $result;
}
if ( $found ) {
return $result;
} else {
return null;
}
}
/**
* Get the value of a global cache item.
*
* If the item does not exist, does not have a value, or has expired,
* then the return value will be false.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @return mixed Value of item.
* @since 1.0.0
*/
public static function get_global( $item_name ) {
return self::get_for_full_name( self::full_item_name( $item_name ) );
}
/**
* Get the value of a standard cache item.
*
* If the item does not exist, does not have a value, or has expired,
* then the return value will be false.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @param boolean $blog_aware Optional. Has the name must take care of blog.
* @param boolean $locale_aware Optional. Has the name must take care of locale.
* @param boolean $user_aware Optional. Has the name must take care of user.
* @return mixed Value of item.
* @since 1.0.0
*/
public static function get( $item_name, $blog_aware = false, $locale_aware = false, $user_aware = false ) {
return self::get_for_full_name( self::full_item_name( $item_name, $blog_aware, $locale_aware, $user_aware ) );
}
/**
* Set the value of a fully named cache item.
*
* You do not need to serialize values. If the value needs to be serialized, then
* it will be serialized before it is set.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @param mixed $value Item value. Must be serializable if non-scalar.
* Expected to not be SQL-escaped.
* @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
* The ttl value in seconds if it's and integer.
* @return bool False if value was not set and true if value was set.
* @since 1.0.0
*/
private static function set_for_full_name( $item_name, $value, $ttl = 'default' ) {
$item_name = self::normalized_item_name( $item_name );
$expiration = self::$default_ttl;
if ( is_string( $ttl ) && array_key_exists( $ttl, self::$ttls ) ) {
$expiration = self::$ttls[ $ttl ];
}
if ( is_integer( $ttl ) && 0 < (int) $ttl ) {
$expiration = (int) $ttl;
}
if ( $expiration > 0 ) {
if ( self::$apcu_available && Option::network_get( 'use_apcu', true ) ) {
$result = apcu_store( self::$pool_name . self::$apcu_pool_prefix . $item_name, $value, $expiration );
} elseif ( wp_using_ext_object_cache() ) {
$result = wp_cache_set( $item_name, $value, self::$pool_name, $expiration );
} else {
$result = set_transient( self::$pool_name . '_' . $item_name, $value, $expiration );
}
} else {
$result = false;
}
return $result;
}
/**
* Set the value of a global cache item.
*
* You do not need to serialize values. If the value needs to be serialized, then
* it will be serialized before it is set.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @param mixed $value Item value. Must be serializable if non-scalar.
* Expected to not be SQL-escaped.
* @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
* The ttl value in seconds if it's and integer.
* @return bool False if value was not set and true if value was set.
* @since 1.0.0
*/
public static function set_global( $item_name, $value, $ttl = 'default' ) {
return self::set_for_full_name( self::full_item_name( $item_name ), $value, $ttl );
}
/**
* Set the value of a standard cache item.
*
* You do not need to serialize values. If the value needs to be serialized, then
* it will be serialized before it is set.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @param mixed $value Item value. Must be serializable if non-scalar.
* Expected to not be SQL-escaped.
* @param int|string $ttl Optional. The previously defined ttl @see self::init() if it's a string.
* The ttl value in seconds if it's and integer.
* @param boolean $blog_aware Optional. Has the name must take care of blog.
* @param boolean $locale_aware Optional. Has the name must take care of locale.
* @param boolean $user_aware Optional. Has the name must take care of user.
* @return bool False if value was not set and true if value was set.
* @since 1.0.0
*/
public static function set( $item_name, $value, $ttl = 'default', $blog_aware = false, $locale_aware = false, $user_aware = false ) {
return self::set_for_full_name( self::full_item_name( $item_name, $blog_aware, $locale_aware, $user_aware ), $value, $ttl );
}
/**
* Delete the value of a fully named cache item.
*
* This function accepts generic car "*" for transients.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @return integer Number of deleted items.
* @since 1.0.0
*/
private static function delete_for_ful_name( $item_name ) {
$item_name = self::normalized_item_name( $item_name );
$result = 0;
if ( self::$apcu_available && Option::network_get( 'use_apcu', true ) ) {
if ( strlen( $item_name ) - 1 === strpos( $item_name, '_*' ) ) {
return false;
} else {
return apcu_delete( self::$pool_name . self::$apcu_pool_prefix . $item_name );
}
}
if ( wp_using_ext_object_cache() ) {
if ( strlen( $item_name ) - 1 === strpos( $item_name, '_*' ) ) {
return false;
} else {
return wp_cache_delete( $item_name, self::$pool_name );
}
}
global $wpdb;
$item_name = self::$pool_name . '_' . $item_name;
if ( strlen( $item_name ) - 1 === strpos( $item_name, '_*' ) ) {
// phpcs:ignore
$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 ) . "';" );
} else {
// phpcs:ignore
$delete = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name = '_transient_timeout_" . $item_name . "';" );
}
foreach ( $delete as $transient ) {
$key = str_replace( '_transient_timeout_', '', $transient );
if ( delete_transient( $key ) ) {
++$result;
}
}
return $result;
}
/**
* Delete the full pool.
*
* @return integer Number of deleted items.
* @since 1.0.0
*/
public static function delete_pool() {
$result = 0;
if ( self::$apcu_available ) {
if ( function_exists( 'apcu_cache_info' ) && function_exists( 'apcu_delete' ) ) {
try {
$infos = apcu_cache_info( false );
if ( array_key_exists( 'cache_list', $infos ) && is_array( $infos['cache_list'] ) ) {
foreach ( $infos['cache_list'] as $script ) {
if ( 0 === strpos( $script['info'], self::$pool_name . self::$apcu_pool_prefix ) ) {
apcu_delete( $script['info'] );
$result++;
}
}
}
} catch ( \Throwable $e ) {
\DecaLog\Engine::eventsLogger( VIBES_SLUG )->error( sprintf( 'Unable to query APCu status: %s.', $e->getMessage() ), [ 'code' => $e->getCode() ] );
$result = 0;
}
}
} elseif ( wp_using_ext_object_cache() ) {
$result = 0;
} else {
$result = self::delete_global( '/*' );
}
return $result;
}
/**
* Delete the value of a global cache item.
*
* This function accepts generic car "*" for transients.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @return integer Number of deleted items.
* @since 1.0.0
*/
public static function delete_global( $item_name ) {
return self::delete_for_ful_name( self::full_item_name( $item_name ) );
}
/**
* Delete the value of a standard cache item.
*
* This function accepts generic car "*" for transients.
*
* @param string $item_name Item name. Expected to not be SQL-escaped.
* @param boolean $blog_aware Optional. Has the name must take care of blog.
* @param boolean $locale_aware Optional. Has the name must take care of locale.
* @param boolean $user_aware Optional. Has the name must take care of user.
* @return integer Number of deleted items.
* @since 1.0.0
*/
public static function delete( $item_name, $blog_aware = false, $locale_aware = false, $user_aware = false ) {
return self::delete_for_ful_name( self::full_item_name( $item_name, $blog_aware, $locale_aware, $user_aware ) );
}
/**
* Get the minimum value of a ttl time range.
*
* @param string $ttl_range The time range in seconds. May be something like '0', '200' or '15-600:15'.
* @return integer The ttl in seconds.
* @since 1.0.0
*/
public static function get_min( $ttl_range ) {
if ( ! is_string( $ttl_range) ) {
return 0;
}
$ttls = explode( '-', $ttl_range );
if ( 1 === count( $ttls ) ) {
return (int) $ttls[0];
}
if ( false !== strpos( $ttls[1], ':' ) ) {
$steps = explode( ':', $ttls[1] );
$ttls[1] = $steps[0];
}
return (int) min( (int) $ttls[0], (int) $ttls[1] );
}
/**
* Get the maximum value of a ttl time range.
*
* @param string $ttl_range The time range in seconds. May be something like '0', '200' or '15-600:15'.
* @return integer The ttl in seconds.
* @since 1.0.0
*/
public static function get_max( $ttl_range ) {
if ( ! is_string( $ttl_range) ) {
return 0;
}
$ttls = explode( '-', $ttl_range );
if ( 1 === count( $ttls ) ) {
return (int) $ttls[0];
}
if ( false !== strpos( $ttls[1], ':' ) ) {
$steps = explode( ':', $ttls[1] );
$ttls[1] = $steps[0];
}
return (int) max( (int) $ttls[0], (int) $ttls[1] );
}
/**
* Get the step of a ttl time range.
*
* @param string $ttl_range The time range in seconds. May be something like '0', '200' or '15-600:15'.
* @return integer The ttl in seconds.
* @since 1.0.0
*/
public static function get_step( $ttl_range ) {
if ( ! is_string( $ttl_range) ) {
return 0;
}
$ttls = explode( '-', $ttl_range );
if ( 1 === count( $ttls ) ) {
return 0;
}
if ( false !== strpos( $ttls[1], ':' ) ) {
$steps = explode( ':', $ttls[1] );
if ( 2 === count( $ttls ) ) {
return $steps[1];
}
}
return 1;
}
/**
* Get the medium value of a ttl time range.
*
* This function accepts generic car "*" for transients.
*
* @param string $ttl_range The time range in seconds. May be something like '5-600' or '200'.
* @return integer The ttl in seconds.
* @since 1.0.0
*/
public static function get_med( $ttl_range ) {
$min = self::get_min( $ttl_range );
$max = self::get_max( $ttl_range );
$step = self::get_step( $ttl_range );
$factor = $step * (int) round( ( $max - $min ) / ( 2 * $step ) );
return $min + (int) round( $factor );
}
/**
* Get the options infos for Site Health "info" tab.
*
* @since 1.0.0
*/
public static function debug_info() {
if ( self::$apcu_available ) {
$result['product'] = [
'label' => 'Product',
'value' => 'APCu',
];
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 ) {
$result[ 'directive_' . $key ] = [
'label' => '[Directive] ' . $key,
'value' => ini_get( 'apc.' . $key ),
];
}
if ( function_exists( 'apcu_sma_info' ) && function_exists( 'apcu_cache_info' ) ) {
$raw = apcu_sma_info();
foreach ( $raw as $key => $status ) {
if ( ! is_array( $status ) ) {
$result[ 'status_' . $key ] = [
'label' => '[Status] ' . $key,
'value' => $status,
];
}
}
$raw = apcu_cache_info();
foreach ( $raw as $key => $status ) {
if ( ! is_array( $status ) ) {
$result[ 'status_' . $key ] = [
'label' => '[Status] ' . $key,
'value' => $status,
];
}
}
}
} else {
$result['product'] = [
'label' => 'Product',
'value' => 'Database transients',
];
}
return $result;
}
}