$now + max( 1, $seconds ), 'n' => 1, ); self::save( $all, $key ); return true; } /** * How many times a counter has been bumped inside its current window, 0 when none is open. * * @param string $key Counter name. * @param int|null $now Clock, for the tests. * @return int */ public static function count( string $key, ?int $now = null ): int { $all = self::load( $now ?? time() ); return isset( $all[ $key ] ) ? (int) $all[ $key ]['n'] : 0; } /** * Adds one to a counter, opening a window of $seconds when none is open. The window does not * slide: it ends $seconds after the first bump, as the transient it replaces did. * * @param string $key Counter name. * @param int $seconds Window length. * @param int|null $now Clock, for the tests. * @return int The count after this bump. */ public static function bump( string $key, int $seconds, ?int $now = null ): int { $now = $now ?? time(); $all = self::load( $now ); if ( isset( $all[ $key ] ) ) { ++$all[ $key ]['n']; } else { $all[ $key ] = array( 'until' => $now + max( 1, $seconds ), 'n' => 1, ); } self::save( $all, $key ); return (int) $all[ $key ]['n']; } /** * The open windows, expired ones dropped. A corrupt option reads as empty. * * @param int $now Clock. * @return array */ private static function load( int $now ): array { $raw = get_option( self::OPTION, array() ); $out = array(); foreach ( is_array( $raw ) ? $raw : array() as $k => $v ) { if ( is_array( $v ) && isset( $v['until'] ) && (int) $v['until'] > $now ) { $out[ (string) $k ] = array( 'until' => (int) $v['until'], 'n' => (int) ( $v['n'] ?? 1 ), ); } } return $out; } /** * Writes the windows back, keeping at most MAX_KEYS: the one just written always, then the * longest-lived. Dropping the new entry would reopen its window at once. * * @param array $all Windows. * @param string $keep Key written by this call. * @return void */ private static function save( array $all, string $keep ): void { if ( count( $all ) > self::MAX_KEYS ) { $mine = $all[ $keep ]; unset( $all[ $keep ] ); uasort( $all, static function ( array $a, array $b ): int { return $b['until'] <=> $a['until']; } ); $all = array_slice( $all, 0, self::MAX_KEYS - 1, true ); $all[ $keep ] = $mine; } update_option( self::OPTION, $all, false ); } }