PHPackages                             ryanhellyer/stale-cache - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. ryanhellyer/stale-cache

ActiveLibrary[Caching](/categories/caching)

ryanhellyer/stale-cache
=======================

A PHP implementation of the stale-while-revalidate caching pattern for WordPress

1.1(3mo ago)2069↓83.3%GPL-2.0-or-laterPHPPHP &gt;=8.2

Since Jan 5Pushed 3mo ago2 watchersCompare

[ Source](https://github.com/ryanhellyer/stale-cache)[ Packagist](https://packagist.org/packages/ryanhellyer/stale-cache)[ RSS](/packages/ryanhellyer-stale-cache/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (2)Dependencies (6)Versions (4)Used By (0)

StaleCache
==========

[](#stalecache)

A PHP implementation of the **stale-while-revalidate** caching pattern for WordPress.

Serve stale content instantly while asynchronously refreshing the cache — zero wait time for your users.

Overview
--------

[](#overview)

StaleCache brings the **stale-while-revalidate** caching strategy to WordPress, heavily inspired by Laravel's `Cache::flexible()`. When cached data expires, the library serves the existing (stale) content immediately while triggering a background refresh — eliminating the performance penalty of synchronous cache regeneration.

This pattern is especially valuable for expensive operations like API calls, complex database queries, or rendered template fragments where you cannot afford to block a request.

---

Features
--------

[](#features)

- **Stale-While-Revalidate** — Serve stale content instantly; refresh the cache asynchronously in the background.
- **Cache Stampede Prevention** — Atomic locking ensures only one process regenerates the cache at a time.
- **Async Refresh** — Leverages `fastcgi_finish_request()` to flush the response to the client before the cache update runs (PHP-FPM required).
- **Pluggable Architecture** — `CacheStore` and `HookManager` interfaces let you swap out the storage backend or hook system.
- **WordPress-Native** — Ships with `WordPressTransientStore` and `WordPressHookManager` for drop-in WordPress compatibility.
- **Type-Safe** — Written in strict PHP 8.2+ with full type declarations.
- **Tested** — Comprehensive test suite with PHPUnit.

---

Installation
------------

[](#installation)

```
composer require ryanhellyer/stale-cache
```

Requires **PHP 8.2+**.

---

Quick Start
-----------

[](#quick-start)

```
use RyanHellyer\StaleCache\StaleCache;

$data = StaleCache::get(
    'my_cache_key',
    [5, 3600],      // [stale_time, cache_duration, lock_duration (optional)]
    function () {
        return get_expensive_data();
    }
);
```

That's it. The first request populates the cache; subsequent requests within the stale window serve the cached value; once expired, stale content is served while the callback runs off the critical path.

---

How It Works
------------

[](#how-it-works)

```
 Request
    │
    ├── Cache hit & fresh?   ───→ Return cached data (instant)
    │
    ├── Cache hit & stale?   ───→ Return stale data + trigger
    │                              background refresh
    │
    └── Cache miss?          ───→ Run callback, store result,
                                  return new data (synchronous)

```

The cache transitions through three lifecycle states:

StateBehaviour**Fresh**Content is served directly from the cache. No overhead.**Stale**Content is served from the cache. A shutdown hook acquires a lock and asynchronously re-executes the callback.**Missing**No cached value exists. The callback runs synchronously, the result is stored, and the stale timestamp is set.### Locking

[](#locking)

When the cache enters the **stale** state, the first process to encounter it acquires a **refresh lock** (stored alongside the cache). Subsequent concurrent requests see the lock and serve stale content without attempting to regenerate — preventing the classic **cache stampede**.

---

API Reference
-------------

[](#api-reference)

### `StaleCache::get(string $key, array $times, callable $callback): mixed`

[](#stalecachegetstring-key-array-times-callable-callback-mixed)

Static facade for the simplest use case. Internally instantiates the class with WordPress defaults.

ParameterTypeDescription`$key``string`Unique cache key`$times``array``[stale_time, cache_duration, lock_duration?]` (in seconds)`$callback``callable`The expensive operation to execute and cache### `StaleCache::__construct(string $key, array $times, CacheStore $store, ?HookManager $hooks = null)`

[](#stalecache__constructstring-key-array-times-cachestore-store-hookmanager-hooks--null)

Dependency-injectable constructor for custom backends.

### `StaleCache::resolve(callable $callback): mixed`

[](#stalecacheresolvecallable-callback-mixed)

Orchestrates the full resolve cycle. Called automatically by `::get()`.

### Interfaces

[](#interfaces)

#### `CacheStore`

[](#cachestore)

```
interface CacheStore
{
    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl): bool;
    public function delete(string $key): bool;
}
```

#### `HookManager`

[](#hookmanager)

```
interface HookManager
{
    public function onShutdown(callable $callback): void;
}
```

### Included Implementations

[](#included-implementations)

ClassImplementsDescription`WordPressTransientStore``CacheStore`Stores values via `set_transient()` / `get_transient()``WordPressHookManager``HookManager`Registers shutdown callbacks via `add_action('shutdown', ...)`---

Configuration
-------------

[](#configuration)

The `$times` array accepts up to three integers:

```
[stale_time, cache_duration, lock_duration]
```

ParameterDefaultDescription`stale_time`**required**Seconds the cache is considered fresh`cache_duration`**required**Total TTL for the cached value`lock_duration``3600` (1 hour)How long the refresh lock is held> **Tip:** Set `lock_duration` high enough to cover the worst-case execution time of your callback. The lock is deleted automatically after the refresh completes.

---

Performance
-----------

[](#performance)

- **Zero-blocking reads** — Stale responses are served instantly while the refresh runs asynchronously after the response is flushed.
- **No cache stampede** — The distributed lock mechanism guarantees at most one concurrent regeneration.
- **Shutdown-based refresh** — By hooking into PHP's shutdown sequence (and calling `fastcgi_finish_request()` where available), the client receives the response before the expensive callback executes.

---

Development
-----------

[](#development)

### Requirements

[](#requirements)

- PHP 8.2+
- Composer

### Setup

[](#setup)

```
composer install
```

### Scripts

[](#scripts)

CommandDescription`composer test`Run the PHPUnit test suite`composer phpcs`Check PSR-12 coding standards`composer phpcs-fix`Auto-fix coding standards violations`composer phpstan`Run static analysis (Level 8)### Static Analysis

[](#static-analysis)

This project enforces **PHPStan Level 8** — the strictest level — ensuring complete type safety across the entire codebase.

---

Contributing
------------

[](#contributing)

Contributions are welcome. Please ensure your changes:

1. Pass all existing tests (`composer test`)
2. Meet PSR-12 coding standards (`composer phpcs`)
3. Pass PHPStan Level 8 (`composer phpstan`)

Submit a pull request and include a clear description of the change and any relevant issue numbers.

---

License
-------

[](#license)

This project is open-sourced software licensed under the [GPL v2](LICENSE) license.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance81

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~479 days

Total

2

Last Release

100d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/430544?v=4)[Ryan Hellyer](/maintainers/ryanhellyer)[@ryanhellyer](https://github.com/ryanhellyer)

---

Top Contributors

[![ryanhellyer](https://avatars.githubusercontent.com/u/430544?v=4)](https://github.com/ryanhellyer "ryanhellyer (39 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ryanhellyer-stale-cache/health.svg)

```
[![Health](https://phpackages.com/badges/ryanhellyer-stale-cache/health.svg)](https://phpackages.com/packages/ryanhellyer-stale-cache)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
