Pending increments keyed by metric ('hit'|'miss'). * Flushed to the transient on shutdown. */ private static $pending = array( 'hit' => 0, 'miss' => 0 ); /** * @var bool Whether the shutdown flush is already registered. */ private static $shutdown_registered = false; public static function record_hit(): void { ++self::$pending['hit']; self::ensure_shutdown_flush(); } public static function record_miss(): void { ++self::$pending['miss']; self::ensure_shutdown_flush(); } /** * Returns up to MAX_BUCKETS most-recent hourly buckets oldest → * newest. Each bucket: [ts => unix hour-start, hits => int, misses * => int ]. * * @return array */ public static function buckets(): array { $buf = get_transient( self::TRANSIENT_KEY ); if ( ! is_array( $buf ) ) { return array(); } // Defensive — strip anything not shaped right. $out = array(); foreach ( $buf as $b ) { if ( is_array( $b ) && isset( $b['ts'], $b['hits'], $b['misses'] ) ) { $out[] = array( 'ts' => (int) $b['ts'], 'hits' => (int) $b['hits'], 'misses' => (int) $b['misses'], ); } } return $out; } /** * Totals over the last 24h (sum across all buckets). * * @return array{hits:int,misses:int,ratio:float} */ public static function totals_24h(): array { $buckets = self::buckets(); $hits = 0; $misses = 0; foreach ( $buckets as $b ) { $hits += $b['hits']; $misses += $b['misses']; } $total = $hits + $misses; return array( 'hits' => $hits, 'misses' => $misses, 'ratio' => $total > 0 ? round( $hits / $total, 4 ) : 0.0, ); } public static function reset(): void { delete_transient( self::TRANSIENT_KEY ); self::$pending = array( 'hit' => 0, 'miss' => 0 ); } /** * One-shot register on first record_* call this request. */ private static function ensure_shutdown_flush(): void { if ( self::$shutdown_registered ) { return; } self::$shutdown_registered = true; register_shutdown_function( array( __CLASS__, 'flush_pending' ) ); } /** * Flush in-process counters into the transient. Bucketed by current * hour. New hour → append a bucket and drop the oldest if we exceed * MAX_BUCKETS. */ public static function flush_pending(): void { $pending = self::$pending; if ( 0 === $pending['hit'] && 0 === $pending['miss'] ) { return; } self::$pending = array( 'hit' => 0, 'miss' => 0 ); $hour = (int) ( time() - ( time() % 3600 ) ); $buf = self::buckets(); $last = end( $buf ); $updated = false; if ( $last && $last['ts'] === $hour ) { $buf[ count( $buf ) - 1 ]['hits'] += $pending['hit']; $buf[ count( $buf ) - 1 ]['misses'] += $pending['miss']; $updated = true; } if ( ! $updated ) { $buf[] = array( 'ts' => $hour, 'hits' => $pending['hit'], 'misses' => $pending['miss'], ); while ( count( $buf ) > self::MAX_BUCKETS ) { array_shift( $buf ); } } set_transient( self::TRANSIENT_KEY, $buf, self::TTL ); } }