| 1 |
<?php |
| 2 |
/** |
| 3 |
* Cache repository implementation. |
| 4 |
* |
| 5 |
* @package SeQura/WC |
| 6 |
* @subpackage SeQura/WC/Repositories |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace SeQura\WC\Repositories; |
| 10 |
|
| 11 |
/** |
| 12 |
* Two-level cache: static in-memory array (per-request fast path) |
| 13 |
* + WordPress object cache (persistent when a backend is configured). |
| 14 |
* |
| 15 |
* The static array is checked first to avoid repeated wp_cache_get() calls |
| 16 |
* (each of which may involve a network round-trip to Redis/Memcached). |
| 17 |
* On a cache hit from the WordPress object cache, the value is promoted to |
| 18 |
* the static array for the remainder of the request. |
| 19 |
*/ |
| 20 |
class Cache_Repository implements Interface_Cache_Repository { |
| 21 |
|
| 22 |
/** |
| 23 |
* Per-request in-memory cache, keyed by group then by key. |
| 24 |
* |
| 25 |
* Public to allow test suites to reset the static state between tests |
| 26 |
* without requiring Reflection. |
| 27 |
* |
| 28 |
* @var array<string, array<string, mixed>> |
| 29 |
*/ |
| 30 |
public static $static_cache = array(); |
| 31 |
|
| 32 |
/** |
| 33 |
* Get a value from the cache. |
| 34 |
* |
| 35 |
* @param string $key Cache key. |
| 36 |
* @param string $group Cache group. |
| 37 |
* @param bool $found Whether the key was found in the cache. Passed by reference. |
| 38 |
* |
| 39 |
* @return mixed Cached value, or false on miss. |
| 40 |
*/ |
| 41 |
public function get( $key, $group, &$found = false ) { |
| 42 |
// Level 1: static array — no serialization, no network. |
| 43 |
if ( \array_key_exists( $group, self::$static_cache ) |
| 44 |
&& \array_key_exists( $key, self::$static_cache[ $group ] ) ) { |
| 45 |
$found = true; |
| 46 |
return self::$static_cache[ $group ][ $key ]; |
| 47 |
} |
| 48 |
|
| 49 |
// Level 2: WordPress object cache (persistent if Redis/Memcached configured). |
| 50 |
$wp_found = false; |
| 51 |
$value = \wp_cache_get( $key, $group, false, $wp_found ); |
| 52 |
if ( $wp_found ) { |
| 53 |
// Promote to static cache for the remainder of the request. |
| 54 |
self::$static_cache[ $group ][ $key ] = $value; |
| 55 |
$found = true; |
| 56 |
return $value; |
| 57 |
} |
| 58 |
|
| 59 |
$found = false; |
| 60 |
return false; |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Set a value in the cache. |
| 65 |
* |
| 66 |
* @param string $key Cache key. |
| 67 |
* @param mixed $value Value to store. Must be serializable. |
| 68 |
* @param string $group Cache group. |
| 69 |
* @param int $ttl Time to live in seconds. 0 means no expiration. |
| 70 |
*/ |
| 71 |
public function set( $key, $value, $group, $ttl = 0 ): bool { |
| 72 |
self::$static_cache[ $group ][ $key ] = $value; |
| 73 |
//phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined |
| 74 |
return \wp_cache_set( $key, $value, $group, $ttl ); |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* Delete a value from the cache. |
| 79 |
* |
| 80 |
* @param string $key Cache key. |
| 81 |
* @param string $group Cache group. |
| 82 |
*/ |
| 83 |
public function delete( $key, $group ): bool { |
| 84 |
unset( self::$static_cache[ $group ][ $key ] ); |
| 85 |
return \wp_cache_delete( $key, $group ); |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Atomically increment a numeric value in the cache. |
| 90 |
* |
| 91 |
* @param string $key Cache key. |
| 92 |
* @param string $group Cache group. |
| 93 |
* @param int $ttl TTL used only when the key is first created. |
| 94 |
* |
| 95 |
* @return int New value after increment. |
| 96 |
*/ |
| 97 |
public function increment( $key, $group, $ttl = 0 ): int { |
| 98 |
// wp_cache_incr() is atomic on Redis/Memcached; it returns false if the key does not exist. |
| 99 |
$new_value = \wp_cache_incr( $key, 1, $group ); |
| 100 |
if ( false === $new_value ) { |
| 101 |
// Key did not exist; use add() (a no-op if another request created it concurrently) |
| 102 |
// then retry the atomic incr() to avoid two requests both writing 1. |
| 103 |
//phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined |
| 104 |
\wp_cache_add( $key, 0, $group, $ttl ); |
| 105 |
$new_value = \wp_cache_incr( $key, 1, $group ); |
| 106 |
if ( false === $new_value ) { |
| 107 |
// Fallback for backends where incr after add still fails (e.g. default WP array cache without the key). |
| 108 |
//phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined |
| 109 |
\wp_cache_set( $key, 1, $group, $ttl ); |
| 110 |
$new_value = 1; |
| 111 |
} |
| 112 |
} |
| 113 |
// Keep static cache in sync so subsequent get() calls in this request see the new value. |
| 114 |
self::$static_cache[ $group ][ $key ] = $new_value; |
| 115 |
return (int) $new_value; |
| 116 |
} |
| 117 |
|
| 118 |
/** |
| 119 |
* Flush all cached data from both the static in-memory array and the WordPress object cache. |
| 120 |
* |
| 121 |
* Uses wp_cache_flush_group() per seQura group when available (WP 6.1+) to avoid flushing |
| 122 |
* the entire object cache — which would evict entries for other sites in a multisite network. |
| 123 |
* Falls back to wp_cache_flush() on older WordPress versions. |
| 124 |
*/ |
| 125 |
public function flush(): void { |
| 126 |
self::$static_cache = array(); |
| 127 |
if ( \function_exists( 'wp_cache_flush_group' ) ) { |
| 128 |
\wp_cache_flush_group( Repository::TABLE_EXISTS_CACHE_GROUP ); |
| 129 |
\wp_cache_flush_group( Repository::DATA_CACHE_GROUP ); |
| 130 |
} else { |
| 131 |
\wp_cache_flush(); |
| 132 |
} |
| 133 |
} |
| 134 |
} |
| 135 |
|